├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── datetimepicker ├── build.gradle.kts └── src │ ├── androidMain │ └── AndroidManifest.xml │ └── commonMain │ └── kotlin │ └── com │ └── kez │ └── picker │ ├── Picker.kt │ ├── PickerState.kt │ ├── date │ └── YearMonthPicker.kt │ ├── time │ └── TimePicker.kt │ └── util │ ├── TimeCalculation.kt │ └── TimeUtil.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── iosApp.xcodeproj │ └── project.pbxproj └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon-20@2x.png │ │ ├── AppIcon-20@2x~ipad.png │ │ ├── AppIcon-20@3x.png │ │ ├── AppIcon-20~ipad.png │ │ ├── AppIcon-29.png │ │ ├── AppIcon-29@2x.png │ │ ├── AppIcon-29@2x~ipad.png │ │ ├── AppIcon-29@3x.png │ │ ├── AppIcon-29~ipad.png │ │ ├── AppIcon-40@2x.png │ │ ├── AppIcon-40@2x~ipad.png │ │ ├── AppIcon-40@3x.png │ │ ├── AppIcon-40~ipad.png │ │ ├── AppIcon-60@2x~car.png │ │ ├── AppIcon-60@3x~car.png │ │ ├── AppIcon-83.5@2x~ipad.png │ │ ├── AppIcon@2x.png │ │ ├── AppIcon@2x~ipad.png │ │ ├── AppIcon@3x.png │ │ ├── AppIcon~ios-marketing.png │ │ ├── AppIcon~ipad.png │ │ └── Contents.json │ └── Contents.json │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iosApp.swift ├── sample ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── com │ │ └── kez │ │ └── picker │ │ └── sample │ │ └── MainActivity.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── kez │ │ └── picker │ │ └── sample │ │ ├── App.kt │ │ └── StringUtils.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── kez │ │ └── picker │ │ └── sample │ │ └── Main.kt │ ├── iosMain │ └── kotlin │ │ └── com │ │ └── kez │ │ └── picker │ │ └── sample │ │ └── main.kt │ ├── jsMain │ └── kotlin │ │ └── com │ │ └── kez │ │ └── picker │ │ └── sample │ │ └── main.kt │ └── jvmMain │ └── kotlin │ └── com │ └── kez │ └── picker │ └── sample │ └── main.kt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/android,kotlin,androidstudio 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=android,kotlin,androidstudio 3 | 4 | ### Android ### 5 | # Gradle files 6 | .gradle/ 7 | build/ 8 | local.properties 9 | .externalNativeBuild/ 10 | .cxx/ 11 | gradle-app.setting 12 | !gradle-wrapper.jar 13 | .gradletasknamecache 14 | gradle.properties 15 | 16 | # Local configuration file (sdk path, etc) 17 | local.properties 18 | 19 | # Log/OS Files 20 | *.log 21 | 22 | # Android Studio generated files and folders 23 | captures/ 24 | .externalNativeBuild/ 25 | .cxx/ 26 | *.apk 27 | output.json 28 | 29 | # IntelliJ 30 | *.iml 31 | *.iws 32 | *.ipr 33 | .idea/ 34 | out/ 35 | misc.xml 36 | deploymentTargetDropDown.xml 37 | render.experimental.xml 38 | 39 | # Keystore files 40 | *.jks 41 | *.keystore 42 | 43 | # Google Services (e.g. APIs or Firebase) 44 | google-services.json 45 | 46 | # Android Profiling 47 | *.hprof 48 | 49 | ### Android Patch ### 50 | gen-external-apklibs 51 | 52 | # Replacement of .externalNativeBuild directories introduced 53 | # with Android Studio 3.5. 54 | 55 | ### Kotlin ### 56 | # Compiled class file 57 | *.class 58 | 59 | # Log file 60 | 61 | # BlueJ files 62 | *.ctxt 63 | 64 | # Mobile Tools for Java (J2ME) 65 | .mtj.tmp/ 66 | 67 | # Package Files # 68 | *.jar 69 | *.war 70 | *.nar 71 | *.ear 72 | *.zip 73 | *.tar.gz 74 | *.rar 75 | 76 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 77 | hs_err_pid* 78 | replay_pid* 79 | 80 | ### AndroidStudio ### 81 | # Covers files to be ignored for android development using Android Studio. 82 | 83 | # Built application files 84 | *.ap_ 85 | *.aab 86 | 87 | # Files for the ART/Dalvik VM 88 | *.dex 89 | 90 | # Java class files 91 | 92 | # Generated files 93 | bin/ 94 | gen/ 95 | out/ 96 | 97 | # Gradle files 98 | .gradle 99 | 100 | # Signing files 101 | .signing/ 102 | 103 | # Local configuration file (sdk path, etc) 104 | 105 | # Proguard folder generated by Eclipse 106 | proguard/ 107 | 108 | # Log Files 109 | 110 | # Android Studio 111 | /*/build/ 112 | /*/local.properties 113 | /*/out 114 | /*/*/build 115 | /*/*/production 116 | .navigation/ 117 | *.ipr 118 | *~ 119 | *.swp 120 | 121 | # Keystore files 122 | 123 | # Google Services (e.g. APIs or Firebase) 124 | # google-services.json 125 | 126 | # Android Patch 127 | 128 | # External native build folder generated in Android Studio 2.2 and later 129 | .externalNativeBuild 130 | 131 | # NDK 132 | obj/ 133 | 134 | # IntelliJ IDEA 135 | *.iws 136 | /out/ 137 | 138 | # User-specific configurations 139 | .idea/caches/ 140 | .idea/libraries/ 141 | .idea/shelf/ 142 | .idea/workspace.xml 143 | .idea/tasks.xml 144 | .idea/.name 145 | .idea/compiler.xml 146 | .idea/copyright/profiles_settings.xml 147 | .idea/encodings.xml 148 | .idea/misc.xml 149 | .idea/modules.xml 150 | .idea/scopes/scope_settings.xml 151 | .idea/dictionaries 152 | .idea/vcs.xml 153 | .idea/jsLibraryMappings.xml 154 | .idea/datasources.xml 155 | .idea/dataSources.ids 156 | .idea/sqlDataSources.xml 157 | .idea/dynamic.xml 158 | .idea/uiDesigner.xml 159 | .idea/assetWizardSettings.xml 160 | .idea/gradle.xml 161 | .idea/jarRepositories.xml 162 | .idea/navEditor.xml 163 | 164 | # Legacy Eclipse project files 165 | .classpath 166 | .project 167 | .cproject 168 | .settings/ 169 | 170 | # Mobile Tools for Java (J2ME) 171 | 172 | # Package Files # 173 | 174 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 175 | 176 | ## Plugin-specific files: 177 | 178 | # mpeltonen/sbt-idea plugin 179 | .idea_modules/ 180 | 181 | # JIRA plugin 182 | atlassian-ide-plugin.xml 183 | 184 | # Mongo Explorer plugin 185 | .idea/mongoSettings.xml 186 | 187 | # Crashlytics plugin (for Android Studio and IntelliJ) 188 | com_crashlytics_export_strings.xml 189 | crashlytics.properties 190 | crashlytics-build.properties 191 | fabric.properties 192 | 193 | ### AndroidStudio Patch ### 194 | 195 | !/gradle/wrapper/gradle-wrapper.jar 196 | 197 | # Cursor 198 | .cursor 199 | 200 | # End of https://www.toptal.com/developers/gitignore/api/android,kotlin,androidstudio 201 | 202 | ## VSCode 203 | .vscode/ 204 | *.code-workspace 205 | .history/ 206 | 207 | ## Xcode 208 | xcuserdata/ 209 | *.xcodeproj/* 210 | !*.xcodeproj/project.pbxproj 211 | !*.xcodeproj/xcshareddata/ 212 | !*.xcworkspace/contents.xcworkspacedata 213 | **/xcshareddata/WorkspaceSettings.xcsettings 214 | *.xcscmblueprint 215 | *.xccheckout 216 | DerivedData/ 217 | .build/ 218 | 219 | # 코틀린 220 | .kotlin/ 221 | .kotlin-js-store/ 222 | kotlin-js-store/ 223 | 224 | # Compose Multiplatform 특화 225 | iosApp/Pods/ 226 | iosApp/Podfile.lock 227 | iosApp/iosApp.xcworkspace/ 228 | iosApp/iosApp.xcodeproj/xcuserdata/ 229 | iosApp/iosApp.xcodeproj/project.xcworkspace/ 230 | iosApp/iosApp/Config.xcconfig 231 | 232 | # 빌드 결과물 233 | */build/ 234 | */out/ 235 | *.aar 236 | *.ap_ 237 | *.apk 238 | *.aab 239 | *.dex 240 | *.class 241 | *.hprof 242 | 243 | # 기타 244 | .DS_Store 245 | Thumbs.db 246 | *.log 247 | hs_err_pid* 248 | .swiftpm/ 249 | 250 | # 커스텀 프로퍼티 파일 251 | *.properties 252 | !gradle.properties 253 | !gradle-wrapper.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Picker 제작 과정기 2 | [[Android/Compose] Picker, NumberPicker, DatePicker 제작 과정기 1부](https://velog.io/@kej_ad/AndroidCompose-Year-Month-DatePicker-%EB%A7%8C%EB%93%A4%EA%B8%B0) 3 | 4 | # Compose DateTimePicker 5 | 6 | Compose Multiplatform용 날짜 및 시간 선택기 라이브러리입니다. Android, iOS, Desktop(JVM) 및 Web을 지원합니다. 7 | 8 | ## 개요 9 | 10 | 이 라이브러리는 Compose Multiplatform을 사용하여 개발된 날짜 및 시간 선택 UI 컴포넌트를 제공합니다. 다양한 플랫폼에서 일관된 사용자 경험을 제공하면서도 각 플랫폼의 특성을 고려한 설계가 적용되었습니다. 11 | 12 | ### 주요 기능 13 | 14 | - **TimePicker**: 12시간제 및 24시간제를 지원하는 시간 선택기 15 | - **YearMonthPicker**: 연도와 월을 선택할 수 있는 날짜 선택기 16 | - **다양한 커스터마이징 옵션**: 글꼴, 색상, 크기 등을 사용자 지정 가능 17 | - **반응형 디자인**: 다양한 화면 크기에 대응 18 | - **표준 Compose 컴포넌트 호환**: 기존 Compose UI에 자연스럽게 통합 19 | 20 | ## 설치 방법 21 | 22 | ### Gradle 23 | 24 | build.gradle.kts (모듈 수준) 파일에 다음 의존성을 추가합니다: 25 | 26 | ```kotlin 27 | dependencies { 28 | implementation("io.github.kez-lab:compose-date-time-picker:0.2.0") 29 | } 30 | ``` 31 | 32 | ## 사용 방법 33 | 34 | ### TimePicker 35 | 36 | ```kotlin 37 | // 24시간제 시간 선택기 38 | TimePicker( 39 | hourPickerState = rememberPickerState(currentHour), 40 | minutePickerState = rememberPickerState(currentMinute), 41 | timeFormat = TimeFormat.HOUR_24 42 | ) 43 | 44 | // 12시간제 시간 선택기 45 | TimePicker( 46 | hourPickerState = rememberPickerState( 47 | if (currentHour > 12) currentHour - 12 else if (currentHour == 0) 12 else currentHour 48 | ), 49 | minutePickerState = rememberPickerState(currentMinute), 50 | periodPickerState = rememberPickerState(if (currentHour >= 12) TimePeriod.PM else TimePeriod.AM), 51 | timeFormat = TimeFormat.HOUR_12 52 | ) 53 | ``` 54 | 55 | ### YearMonthPicker 56 | 57 | ```kotlin 58 | YearMonthPicker( 59 | yearPickerState = rememberPickerState(currentDate.year), 60 | monthPickerState = rememberPickerState(currentDate.monthNumber) 61 | ) 62 | ``` 63 | 64 | ### 상태 관리 65 | 66 | ```kotlin 67 | // PickerState를 사용하여 상태 관리 68 | val hourState = rememberPickerState(currentHour) 69 | val minuteState = rememberPickerState(currentMinute) 70 | 71 | // 선택된 값 접근 72 | val selectedHour = hourState.selectedItem 73 | val selectedMinute = minuteState.selectedItem 74 | ``` 75 | 76 | ## 커스터마이징 77 | 78 | ```kotlin 79 | TimePicker( 80 | hourPickerState = rememberPickerState(currentHour), 81 | minutePickerState = rememberPickerState(currentMinute), 82 | timeFormat = TimeFormat.HOUR_24, 83 | textStyle = TextStyle(fontSize = 14.sp, color = Color.Gray), 84 | selectedTextStyle = TextStyle(fontSize = 18.sp, color = Color.Black, fontWeight = FontWeight.Bold), 85 | dividerColor = Color.Blue, 86 | visibleItemsCount = 5, 87 | pickerWidth = 80.dp 88 | ) 89 | ``` 90 | 91 | ## 프로젝트 구조 92 | 93 | ``` 94 | Compose-DateTimePicker/ 95 | ├── datetimepicker/ # 라이브러리 모듈 96 | │ └── src/ 97 | │ ├── commonMain/ # 공통 코드 98 | │ │ └── kotlin/com/kez/picker/ 99 | │ │ ├── date/ # 날짜 선택기 100 | │ │ ├── time/ # 시간 선택기 101 | │ │ └── util/ # 유틸리티 클래스 102 | │ ├── androidMain/ # Android 구현 103 | │ ├── iosMain/ # iOS 구현 104 | │ ├── desktopMain/ # Desktop(JVM) 구현 105 | │ └── jsMain/ # Web 구현 106 | └── sample/ # 샘플 앱 107 | └── src/ 108 | ├── commonMain/ # 공통 샘플 코드 109 | ├── androidMain/ # Android 샘플 진입점 110 | ├── iosMain/ # iOS 샘플 진입점 111 | ├── jvmMain/ # Desktop 샘플 진입점 112 | └── jsMain/ # Web 샘플 진입점 113 | ``` 114 | 115 | ## 라이선스 116 | 117 | ``` 118 | Copyright 2024 KEZ Lab 119 | 120 | Licensed under the Apache License, Version 2.0 (the "License"); 121 | you may not use this file except in compliance with the License. 122 | You may obtain a copy of the License at 123 | 124 | http://www.apache.org/licenses/LICENSE-2.0 125 | 126 | Unless required by applicable law or agreed to in writing, software 127 | distributed under the License is distributed on an "AS IS" BASIS, 128 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 129 | See the License for the specific language governing permissions and 130 | limitations under the License. 131 | ``` 132 | 133 | 134 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.multiplatform) apply false 5 | alias(libs.plugins.android.library) apply false 6 | alias(libs.plugins.compose) apply false 7 | alias(libs.plugins.compose.compiler) apply false 8 | } -------------------------------------------------------------------------------- /datetimepicker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.android.library) 6 | alias(libs.plugins.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.vanniktech.maven) 9 | alias(libs.plugins.kotlinx.serialization) 10 | } 11 | 12 | group = "io.github.kez-lab" 13 | version = "0.2.0" 14 | 15 | kotlin { 16 | jvmToolchain(17) 17 | 18 | androidTarget { 19 | publishLibraryVariants("release") 20 | compilations.all { 21 | kotlinOptions { 22 | jvmTarget = "17" 23 | } 24 | } 25 | } 26 | 27 | iosX64() 28 | iosArm64() 29 | iosSimulatorArm64() 30 | 31 | jvm("desktop") 32 | 33 | js(IR) { 34 | browser() 35 | } 36 | 37 | sourceSets { 38 | commonMain.dependencies { 39 | implementation(compose.runtime) 40 | implementation(compose.foundation) 41 | implementation(compose.material3) 42 | implementation(compose.ui) 43 | implementation(compose.components.resources) 44 | implementation(libs.kotlinx.datetime) 45 | implementation(libs.kotlinx.coroutines.core) 46 | } 47 | 48 | commonTest.dependencies { 49 | implementation(kotlin("test")) 50 | } 51 | 52 | androidMain.dependencies { 53 | // Android-specific dependencies if needed 54 | } 55 | 56 | iosMain.dependencies { 57 | // iOS-specific dependencies if needed 58 | } 59 | 60 | val desktopMain by getting { 61 | dependencies { 62 | implementation(compose.desktop.common) 63 | } 64 | } 65 | 66 | val jsMain by getting { 67 | dependencies { 68 | implementation(compose.web.core) 69 | } 70 | } 71 | } 72 | } 73 | 74 | android { 75 | namespace = "com.kez.picker" 76 | compileSdk = 35 77 | 78 | defaultConfig { 79 | minSdk = 24 80 | } 81 | 82 | buildTypes { 83 | release { 84 | isMinifyEnabled = false 85 | proguardFiles( 86 | getDefaultProguardFile("proguard-android-optimize.txt"), 87 | "proguard-rules.pro" 88 | ) 89 | } 90 | } 91 | 92 | compileOptions { 93 | sourceCompatibility = JavaVersion.VERSION_17 94 | targetCompatibility = JavaVersion.VERSION_17 95 | } 96 | 97 | buildFeatures { 98 | compose = true 99 | } 100 | 101 | composeOptions { 102 | kotlinCompilerExtensionVersion = libs.versions.compose.get() 103 | } 104 | } 105 | 106 | mavenPublishing { 107 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 108 | 109 | signAllPublications() 110 | 111 | coordinates("io.github.kez-lab", "compose-date-time-picker", "0.2.0") 112 | 113 | pom { 114 | name = "Compose-DateTimePicker" 115 | description = "Compose Multiplatform DateTimePicker library supporting Android, iOS, Desktop and Web" 116 | url = "https://github.com/kez-lab/Compose-DateTimePicker" 117 | inceptionYear = "2024" 118 | 119 | licenses { 120 | license { 121 | name = "The Apache License, Version 2.0" 122 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt" 123 | } 124 | } 125 | developers { 126 | developer { 127 | id = "KwakEuiJin" 128 | name = "KEZ" 129 | url = "https://github.com/kez-lab" 130 | } 131 | } 132 | 133 | scm { 134 | url.set("https://github.com/kez-lab/Compose-DateTimePicker") 135 | connection.set("scm:git:git://github.com/kez-lab/Compose-DateTimePicker.git") 136 | developerConnection.set("scm:git:ssh://git@github.com/kez-lab/Compose-DateTimePicker.git") 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /datetimepicker/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /datetimepicker/src/commonMain/kotlin/com/kez/picker/Picker.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.wrapContentHeight 10 | import androidx.compose.foundation.layout.wrapContentSize 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.foundation.lazy.rememberLazyListState 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material3.HorizontalDivider 15 | import androidx.compose.material3.LocalContentColor 16 | import androidx.compose.material3.LocalTextStyle 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.LaunchedEffect 20 | import androidx.compose.runtime.derivedStateOf 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.snapshotFlow 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.draw.drawWithContent 28 | import androidx.compose.ui.graphics.BlendMode 29 | import androidx.compose.ui.graphics.Brush 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.graphics.CompositingStrategy 32 | import androidx.compose.ui.graphics.Shape 33 | import androidx.compose.ui.graphics.graphicsLayer 34 | import androidx.compose.ui.graphics.lerp 35 | import androidx.compose.ui.platform.LocalDensity 36 | import androidx.compose.ui.text.TextStyle 37 | import androidx.compose.ui.text.style.TextAlign 38 | import androidx.compose.ui.text.style.TextOverflow 39 | import androidx.compose.ui.unit.Dp 40 | import androidx.compose.ui.unit.dp 41 | import androidx.compose.ui.unit.lerp 42 | import kotlinx.coroutines.flow.distinctUntilChanged 43 | import kotlinx.coroutines.flow.mapNotNull 44 | import kotlin.math.abs 45 | 46 | /** 47 | * A generic picker component that displays a list of items and allows the user to select one. 48 | * 49 | * @param items The list of items to display. 50 | * @param modifier The modifier to be applied to the picker. 51 | * @param state The state of the picker. 52 | * @param startIndex The initial index to display. 53 | * @param visibleItemsCount The number of items visible at once. 54 | * @param textModifier The modifier to be applied to the text. 55 | * @param textStyle The style of the text for unselected items. 56 | * @param selectedTextStyle The style of the text for the selected item. 57 | * @param dividerColor The color of the dividers. 58 | * @param itemPadding The padding around each item. 59 | * @param fadingEdgeGradient The gradient to use for fading edges. 60 | * @param horizontalAlignment The horizontal alignment of items. 61 | * @param itemTextAlignment The vertical alignment of the text within items. 62 | * @param dividerThickness The thickness of the dividers. 63 | * @param dividerShape The shape of the dividers. 64 | * @param isInfinity Whether the picker should loop infinitely. 65 | */ 66 | @Composable 67 | fun Picker( 68 | items: List, 69 | modifier: Modifier = Modifier, 70 | state: PickerState, 71 | startIndex: Int = 0, 72 | visibleItemsCount: Int = 3, 73 | textModifier: Modifier = Modifier, 74 | textStyle: TextStyle = LocalTextStyle.current, 75 | selectedTextStyle: TextStyle = LocalTextStyle.current, 76 | dividerColor: Color = LocalContentColor.current, 77 | itemPadding: PaddingValues = PaddingValues(8.dp), 78 | fadingEdgeGradient: Brush = Brush.verticalGradient( 79 | 0f to Color.Transparent, 80 | 0.5f to Color.Black, 81 | 1f to Color.Transparent 82 | ), 83 | horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, 84 | itemTextAlignment: Alignment.Vertical = Alignment.CenterVertically, 85 | dividerThickness: Dp = 1.dp, 86 | dividerShape: Shape = RoundedCornerShape(10.dp), 87 | isInfinity: Boolean = true 88 | ) { 89 | val density = LocalDensity.current 90 | val visibleItemsMiddle = remember { visibleItemsCount / 2 } 91 | 92 | val adjustedItems = if (!isInfinity) { 93 | listOf(null) + items + listOf(null) 94 | } else { 95 | items 96 | } 97 | 98 | val listScrollCount = if (isInfinity) { 99 | Int.MAX_VALUE 100 | } else { 101 | adjustedItems.size 102 | } 103 | 104 | val listScrollMiddle = remember { listScrollCount / 2 } 105 | val listStartIndex = remember { 106 | if (isInfinity) { 107 | listScrollMiddle - listScrollMiddle % adjustedItems.size - visibleItemsMiddle + startIndex 108 | } else { 109 | startIndex + 1 110 | } 111 | } 112 | 113 | fun getItem(index: Int) = adjustedItems[index % adjustedItems.size] 114 | 115 | val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex) 116 | val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) 117 | 118 | val itemHeight = with(density) { 119 | selectedTextStyle.fontSize.toDp() + itemPadding.calculateTopPadding() + itemPadding.calculateBottomPadding() 120 | } 121 | 122 | LaunchedEffect(listState) { 123 | snapshotFlow { listState.firstVisibleItemIndex } 124 | .mapNotNull { index -> getItem(index + visibleItemsMiddle) } 125 | .distinctUntilChanged() 126 | .collect { item -> state.selectedItem = item } 127 | } 128 | 129 | Box(modifier = modifier) { 130 | LazyColumn( 131 | state = listState, 132 | flingBehavior = flingBehavior, 133 | horizontalAlignment = horizontalAlignment, 134 | modifier = Modifier 135 | .align(Alignment.Center) 136 | .wrapContentSize() 137 | .height(itemHeight * visibleItemsCount) 138 | .fadingEdge(fadingEdgeGradient) 139 | ) { 140 | items( 141 | listScrollCount, 142 | key = { it }, 143 | ) { index -> 144 | val fraction by remember { 145 | derivedStateOf { 146 | val currentItem = 147 | listState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == index } 148 | currentItem?.offset?.let { offset -> 149 | val itemHeightPx = with(density) { itemHeight.toPx() } 150 | val fraction = 151 | (offset - itemHeightPx * visibleItemsMiddle) / itemHeightPx 152 | abs(fraction.coerceIn(-1f, 1f)) 153 | } ?: 0f 154 | } 155 | } 156 | 157 | val currentItemText by remember { 158 | mutableStateOf(if (getItem(index) == null) "" else getItem(index).toString()) 159 | } 160 | 161 | Text( 162 | text = currentItemText, 163 | maxLines = 1, 164 | overflow = TextOverflow.Ellipsis, 165 | style = textStyle.copy( 166 | fontSize = lerp( 167 | selectedTextStyle.fontSize, 168 | textStyle.fontSize, 169 | fraction 170 | ), 171 | color = lerp( 172 | selectedTextStyle.color, 173 | textStyle.color, 174 | fraction 175 | ) 176 | ), 177 | textAlign = TextAlign.Center, 178 | modifier = Modifier 179 | .height(itemHeight) 180 | .wrapContentHeight(align = itemTextAlignment) 181 | .fillMaxWidth() 182 | .then(textModifier) 183 | ) 184 | } 185 | } 186 | 187 | Box( 188 | modifier = Modifier 189 | .align(Alignment.Center) 190 | .fillMaxWidth() 191 | .height(itemHeight) 192 | ) { 193 | HorizontalDivider( 194 | color = dividerColor, 195 | thickness = dividerThickness, 196 | modifier = Modifier 197 | .fillMaxWidth() 198 | .background( 199 | color = dividerColor, 200 | shape = dividerShape 201 | ) 202 | .align(Alignment.TopCenter) 203 | ) 204 | 205 | HorizontalDivider( 206 | color = dividerColor, 207 | thickness = dividerThickness, 208 | modifier = Modifier 209 | .fillMaxWidth() 210 | .background( 211 | color = dividerColor, 212 | shape = dividerShape 213 | ) 214 | .align(Alignment.BottomCenter) 215 | ) 216 | } 217 | } 218 | } 219 | 220 | /** 221 | * Apply a fading edge effect to a modifier. 222 | * 223 | * @param brush The gradient brush to use for the fading effect. 224 | * @return The modified modifier with the fading edge effect. 225 | */ 226 | private fun Modifier.fadingEdge(brush: Brush) = this 227 | .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) 228 | .drawWithContent { 229 | drawContent() 230 | drawRect(brush = brush, blendMode = BlendMode.DstIn) 231 | } -------------------------------------------------------------------------------- /datetimepicker/src/commonMain/kotlin/com/kez/picker/PickerState.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.setValue 8 | 9 | /** 10 | * Remember a [PickerState] with the given initial item. 11 | * 12 | * @param initialItem The initial selected item. 13 | * @return A [PickerState] with the given initial item. 14 | */ 15 | @Composable 16 | fun rememberPickerState(initialItem: T) = remember { PickerState(initialItem) } 17 | 18 | /** 19 | * State holder for the picker component. 20 | * 21 | * @param initialItem The initial selected item. 22 | */ 23 | class PickerState( 24 | initialItem: T 25 | ) { 26 | /** 27 | * The currently selected item. 28 | */ 29 | var selectedItem by mutableStateOf(initialItem) 30 | } -------------------------------------------------------------------------------- /datetimepicker/src/commonMain/kotlin/com/kez/picker/date/YearMonthPicker.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.date 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.LocalContentColor 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Brush 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.Shape 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.unit.Dp 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.kez.picker.Picker 25 | import com.kez.picker.PickerState 26 | import com.kez.picker.rememberPickerState 27 | import com.kez.picker.util.MONTH_RANGE 28 | import com.kez.picker.util.YEAR_RANGE 29 | import com.kez.picker.util.currentDate 30 | import kotlinx.datetime.LocalDate 31 | 32 | /** 33 | * A year and month picker component. 34 | * 35 | * @param modifier The modifier to be applied to the component. 36 | * @param yearPickerState The state for the year picker. 37 | * @param monthPickerState The state for the month picker. 38 | * @param startLocalDate The initial date to display. 39 | * @param yearItems The list of year values to display. 40 | * @param monthItems The list of month values to display. 41 | * @param visibleItemsCount The number of items visible at once. 42 | * @param itemPadding The padding around each item. 43 | * @param textStyle The style of the text for unselected items. 44 | * @param selectedTextStyle The style of the text for the selected item. 45 | * @param dividerColor The color of the dividers. 46 | * @param fadingEdgeGradient The gradient to use for fading edges. 47 | * @param horizontalAlignment The horizontal alignment of items. 48 | * @param verticalAlignment The vertical alignment of the text within items. 49 | * @param dividerThickness The thickness of the dividers. 50 | * @param dividerShape The shape of the dividers. 51 | * @param spacingBetweenPickers The spacing between the pickers. 52 | * @param pickerWidth The width of each picker. 53 | */ 54 | @Composable 55 | fun YearMonthPicker( 56 | modifier: Modifier = Modifier, 57 | yearPickerState: PickerState = rememberPickerState(currentDate.year), 58 | monthPickerState: PickerState = rememberPickerState(currentDate.monthNumber), 59 | startLocalDate: LocalDate = currentDate, 60 | yearItems: List = YEAR_RANGE, 61 | monthItems: List = MONTH_RANGE, 62 | visibleItemsCount: Int = 3, 63 | itemPadding: PaddingValues = PaddingValues(8.dp), 64 | textStyle: TextStyle = TextStyle(fontSize = 16.sp), 65 | selectedTextStyle: TextStyle = TextStyle(fontSize = 24.sp), 66 | dividerColor: Color = LocalContentColor.current, 67 | fadingEdgeGradient: Brush = Brush.verticalGradient( 68 | 0f to Color.Transparent, 69 | 0.5f to Color.Black, 70 | 1f to Color.Transparent 71 | ), 72 | horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, 73 | verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, 74 | dividerThickness: Dp = 2.dp, 75 | dividerShape: Shape = RoundedCornerShape(10.dp), 76 | spacingBetweenPickers: Dp = 20.dp, 77 | pickerWidth: Dp = 100.dp 78 | ) { 79 | Surface(modifier = modifier) { 80 | Column( 81 | horizontalAlignment = Alignment.CenterHorizontally, 82 | verticalArrangement = Arrangement.Center, 83 | modifier = Modifier.fillMaxWidth() 84 | ) { 85 | 86 | val yearStartIndex = remember { 87 | yearItems.indexOf(startLocalDate.year) 88 | } 89 | val monthStartIndex = remember { 90 | monthItems.indexOf(startLocalDate.monthNumber) 91 | } 92 | 93 | Row( 94 | modifier = Modifier.fillMaxWidth(), 95 | horizontalArrangement = Arrangement.spacedBy( 96 | spacingBetweenPickers, 97 | Alignment.CenterHorizontally 98 | ), 99 | ) { 100 | Picker( 101 | state = yearPickerState, 102 | modifier = Modifier.width(pickerWidth), 103 | items = yearItems, 104 | startIndex = yearStartIndex, 105 | visibleItemsCount = visibleItemsCount, 106 | textModifier = Modifier.padding(itemPadding), 107 | textStyle = textStyle, 108 | selectedTextStyle = selectedTextStyle, 109 | dividerColor = dividerColor, 110 | itemPadding = itemPadding, 111 | fadingEdgeGradient = fadingEdgeGradient, 112 | horizontalAlignment = horizontalAlignment, 113 | itemTextAlignment = verticalAlignment, 114 | dividerThickness = dividerThickness, 115 | dividerShape = dividerShape 116 | ) 117 | Picker( 118 | state = monthPickerState, 119 | items = monthItems, 120 | startIndex = monthStartIndex, 121 | visibleItemsCount = visibleItemsCount, 122 | modifier = Modifier.width(pickerWidth), 123 | textStyle = textStyle, 124 | selectedTextStyle = selectedTextStyle, 125 | textModifier = Modifier.padding(itemPadding), 126 | dividerColor = dividerColor, 127 | itemPadding = itemPadding, 128 | fadingEdgeGradient = fadingEdgeGradient, 129 | horizontalAlignment = horizontalAlignment, 130 | itemTextAlignment = verticalAlignment, 131 | dividerThickness = dividerThickness, 132 | dividerShape = dividerShape 133 | ) 134 | } 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /datetimepicker/src/commonMain/kotlin/com/kez/picker/time/TimePicker.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.time 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.LocalContentColor 13 | import androidx.compose.material3.Surface 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Brush 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.Shape 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import com.kez.picker.Picker 26 | import com.kez.picker.PickerState 27 | import com.kez.picker.util.HOUR12_RANGE 28 | import com.kez.picker.util.HOUR24_RANGE 29 | import com.kez.picker.util.MINUTE_RANGE 30 | import com.kez.picker.util.TimeFormat 31 | import com.kez.picker.util.TimePeriod 32 | import com.kez.picker.util.currentDateTime 33 | import com.kez.picker.util.currentHour 34 | import com.kez.picker.util.currentMinute 35 | import kotlinx.datetime.LocalDateTime 36 | 37 | /** 38 | * A time picker component that allows the user to select hours and minutes. 39 | * 40 | * @param modifier The modifier to be applied to the component. 41 | * @param minutePickerState The state for the minute picker. 42 | * @param hourPickerState The state for the hour picker. 43 | * @param periodPickerState The state for the AM/PM period picker. 44 | * @param timeFormat The time format (12-hour or 24-hour). 45 | * @param startTime The initial time to display. 46 | * @param minuteItems The list of minute values to display. 47 | * @param hourItems The list of hour values to display. 48 | * @param periodItems The list of period values to display. 49 | * @param visibleItemsCount The number of items visible at once. 50 | * @param itemPadding The padding around each item. 51 | * @param textStyle The style of the text for unselected items. 52 | * @param selectedTextStyle The style of the text for the selected item. 53 | * @param dividerColor The color of the dividers. 54 | * @param fadingEdgeGradient The gradient to use for fading edges. 55 | * @param horizontalAlignment The horizontal alignment of items. 56 | * @param verticalAlignment The vertical alignment of the text within items. 57 | * @param dividerThickness The thickness of the dividers. 58 | * @param dividerShape The shape of the dividers. 59 | * @param spacingBetweenPickers The spacing between the pickers. 60 | * @param pickerWidth The width of each picker. 61 | */ 62 | @Composable 63 | fun TimePicker( 64 | modifier: Modifier = Modifier, 65 | minutePickerState: PickerState = remember { PickerState(currentMinute) }, 66 | hourPickerState: PickerState = remember { PickerState(currentHour) }, 67 | periodPickerState: PickerState = remember { PickerState(TimePeriod.AM) }, 68 | timeFormat: TimeFormat = TimeFormat.HOUR_24, 69 | startTime: LocalDateTime = currentDateTime, 70 | minuteItems: List = MINUTE_RANGE, 71 | hourItems: List = when (timeFormat) { 72 | TimeFormat.HOUR_12 -> HOUR12_RANGE 73 | TimeFormat.HOUR_24 -> HOUR24_RANGE 74 | }, 75 | periodItems: List = TimePeriod.entries, 76 | visibleItemsCount: Int = 3, 77 | itemPadding: PaddingValues = PaddingValues(8.dp), 78 | textStyle: TextStyle = TextStyle(fontSize = 16.sp), 79 | selectedTextStyle: TextStyle = TextStyle(fontSize = 22.sp), 80 | dividerColor: Color = LocalContentColor.current, 81 | fadingEdgeGradient: Brush = Brush.verticalGradient( 82 | 0f to Color.Transparent, 83 | 0.5f to Color.Black, 84 | 1f to Color.Transparent 85 | ), 86 | horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, 87 | verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, 88 | dividerThickness: Dp = 1.dp, 89 | dividerShape: Shape = RoundedCornerShape(10.dp), 90 | spacingBetweenPickers: Dp = 20.dp, 91 | pickerWidth: Dp = 80.dp 92 | ) { 93 | Surface(modifier = modifier) { 94 | Column( 95 | horizontalAlignment = Alignment.CenterHorizontally, 96 | verticalArrangement = Arrangement.Center, 97 | modifier = Modifier.fillMaxWidth() 98 | ) { 99 | 100 | val minuteStartIndex = remember { 101 | minuteItems.indexOf(startTime.minute) 102 | } 103 | 104 | val hourStartIndex = remember { 105 | val startHour = when (timeFormat) { 106 | TimeFormat.HOUR_12 -> { 107 | val hour = startTime.hour % 12 108 | if (hour == 0) 12 else hour 109 | } 110 | 111 | TimeFormat.HOUR_24 -> startTime.hour 112 | } 113 | hourItems.indexOf(startHour) 114 | } 115 | 116 | val periodStartIndex = remember { 117 | val period = if (startTime.hour >= 12) TimePeriod.PM else TimePeriod.AM 118 | periodItems.indexOf(period) 119 | } 120 | 121 | Row( 122 | modifier = Modifier.fillMaxWidth(), 123 | horizontalArrangement = Arrangement.Center, 124 | verticalAlignment = Alignment.CenterVertically 125 | ) { 126 | if (timeFormat == TimeFormat.HOUR_12) { 127 | Picker( 128 | state = periodPickerState, 129 | items = periodItems, 130 | visibleItemsCount = visibleItemsCount, 131 | modifier = Modifier.width(pickerWidth), 132 | textStyle = textStyle, 133 | selectedTextStyle = selectedTextStyle, 134 | textModifier = Modifier.padding(itemPadding), 135 | dividerColor = dividerColor, 136 | itemPadding = itemPadding, 137 | startIndex = periodStartIndex, 138 | fadingEdgeGradient = fadingEdgeGradient, 139 | horizontalAlignment = horizontalAlignment, 140 | itemTextAlignment = verticalAlignment, 141 | dividerThickness = dividerThickness, 142 | dividerShape = dividerShape, 143 | isInfinity = false, 144 | ) 145 | Spacer(modifier = Modifier.width(spacingBetweenPickers)) 146 | } 147 | Picker( 148 | state = hourPickerState, 149 | modifier = Modifier.width(pickerWidth), 150 | items = hourItems, 151 | startIndex = hourStartIndex, 152 | visibleItemsCount = visibleItemsCount, 153 | textModifier = Modifier.padding(itemPadding), 154 | textStyle = textStyle, 155 | selectedTextStyle = selectedTextStyle, 156 | dividerColor = dividerColor, 157 | itemPadding = itemPadding, 158 | fadingEdgeGradient = fadingEdgeGradient, 159 | horizontalAlignment = horizontalAlignment, 160 | itemTextAlignment = verticalAlignment, 161 | dividerThickness = dividerThickness, 162 | dividerShape = dividerShape 163 | ) 164 | Spacer(modifier = Modifier.width(spacingBetweenPickers)) 165 | Picker( 166 | state = minutePickerState, 167 | items = minuteItems, 168 | startIndex = minuteStartIndex, 169 | visibleItemsCount = visibleItemsCount, 170 | modifier = Modifier.width(pickerWidth), 171 | textStyle = textStyle, 172 | selectedTextStyle = selectedTextStyle, 173 | textModifier = Modifier.padding(itemPadding), 174 | dividerColor = dividerColor, 175 | itemPadding = itemPadding, 176 | fadingEdgeGradient = fadingEdgeGradient, 177 | horizontalAlignment = horizontalAlignment, 178 | itemTextAlignment = verticalAlignment, 179 | dividerThickness = dividerThickness, 180 | dividerShape = dividerShape, 181 | ) 182 | } 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /datetimepicker/src/commonMain/kotlin/com/kez/picker/util/TimeCalculation.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.util 2 | 3 | import kotlinx.datetime.LocalDateTime 4 | 5 | /** 6 | * Calculate time based on hour, minute, format and period. 7 | * 8 | * @param hour The hour value (0-23 for 24-hour format, 1-12 for 12-hour format). 9 | * @param minute The minute value (0-59). 10 | * @param timeFormat The time format (12-hour or 24-hour). 11 | * @param period The time period (AM/PM) for 12-hour format. 12 | * @return A [LocalDateTime] instance with the calculated time. 13 | */ 14 | fun calculateTime( 15 | hour: Int, 16 | minute: Int, 17 | timeFormat: TimeFormat, 18 | period: TimePeriod? = null, 19 | ): LocalDateTime { 20 | val adjustHour = when (timeFormat) { 21 | TimeFormat.HOUR_12 -> { 22 | when (period) { 23 | TimePeriod.AM -> if (hour == 12) 0 else hour 24 | TimePeriod.PM -> if (hour == 12) 12 else hour + 12 25 | null -> hour 26 | } 27 | } 28 | 29 | TimeFormat.HOUR_24 -> hour 30 | } 31 | 32 | return LocalDateTime( 33 | year = currentYear, 34 | monthNumber = currentMonth, 35 | dayOfMonth = currentDate.dayOfMonth, 36 | hour = adjustHour.coerceIn(0, 23), 37 | minute = minute.coerceIn(0, 59) 38 | ) 39 | } -------------------------------------------------------------------------------- /datetimepicker/src/commonMain/kotlin/com/kez/picker/util/TimeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.util 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.TimeZone 5 | import kotlinx.datetime.toLocalDateTime 6 | 7 | /** 8 | * Range of years for year picker (1000-9999). 9 | */ 10 | val YEAR_RANGE = (1000..9999).toList() 11 | 12 | /** 13 | * Range of months for month picker (1-12). 14 | */ 15 | val MONTH_RANGE = (1..12).toList() 16 | 17 | /** 18 | * Range of hours for 24-hour format (0-23). 19 | */ 20 | val HOUR24_RANGE = (0..23).toList() 21 | 22 | /** 23 | * Range of hours for 12-hour format (1-12). 24 | */ 25 | val HOUR12_RANGE = (1..12).toList() 26 | 27 | /** 28 | * Range of minutes (0-59). 29 | */ 30 | val MINUTE_RANGE = (0..59).toList() 31 | 32 | /** 33 | * Current date and time. 34 | */ 35 | val currentDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) 36 | 37 | /** 38 | * Current date. 39 | */ 40 | val currentDate = currentDateTime.date 41 | 42 | /** 43 | * Current year. 44 | */ 45 | val currentYear = currentDateTime.year 46 | 47 | /** 48 | * Current month number (1-12). 49 | */ 50 | val currentMonth = currentDateTime.monthNumber 51 | 52 | /** 53 | * Current minute (0-59). 54 | */ 55 | val currentMinute = currentDateTime.minute 56 | 57 | /** 58 | * Current hour (0-23). 59 | */ 60 | val currentHour = currentDateTime.hour 61 | 62 | /** 63 | * Time format for time picker. 64 | */ 65 | enum class TimeFormat { 66 | /** 67 | * 12-hour format (AM/PM). 68 | */ 69 | HOUR_12, 70 | 71 | /** 72 | * 24-hour format. 73 | */ 74 | HOUR_24 75 | } 76 | 77 | /** 78 | * Time period for 12-hour format. 79 | */ 80 | enum class TimePeriod { 81 | /** 82 | * AM period (Ante Meridiem). 83 | */ 84 | AM, 85 | 86 | /** 87 | * PM period (Post Meridiem). 88 | */ 89 | PM 90 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | org.jetbrains.compose.experimental.jscanvas.enabled=true 25 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.20" 3 | compose = "1.8.0-beta01" 4 | agp = "8.6.1" 5 | androidx-activityCompose = "1.10.1" 6 | androidx-uiTest = "1.7.8" 7 | hotReload = "1.0.0-alpha03" 8 | kotlinx-coroutines = "1.10.1" 9 | ktor = "3.1.1" 10 | androidx-lifecycle = "2.9.0-alpha05" 11 | androidx-navigation = "2.9.0-alpha15" 12 | kotlinx-serialization = "1.8.0" 13 | koin = "4.0.3" 14 | coil = "3.1.0" 15 | multiplatformSettings = "1.3.0" 16 | kotlinx-datetime = "0.6.2" 17 | room = "2.7.0-rc02" 18 | ksp = "2.1.20-1.0.31" 19 | buildConfig = "5.4.0" 20 | composeIcons = "1.1.1" 21 | vanniktech-maven = "0.28.0" 22 | 23 | [libraries] 24 | androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 25 | androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } 26 | androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } 27 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 28 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } 29 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 30 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 31 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 32 | ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } 33 | ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" } 34 | ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } 35 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } 36 | ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } 37 | ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } 38 | ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" } 39 | ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } 40 | androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" } 41 | androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 42 | androidx-navigation-composee = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } 43 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 44 | koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } 45 | koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } 46 | coil = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" } 47 | coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } 48 | multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } 49 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } 50 | room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } 51 | room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } 52 | composeIcons-featherIcons = { module = "br.com.devsrsouza.compose.icons:feather", version.ref = "composeIcons" } 53 | 54 | [plugins] 55 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 56 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 57 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 58 | android-application = { id = "com.android.application", version.ref = "agp" } 59 | android-library = { id = "com.android.library", version.ref = "agp" } 60 | hotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "hotReload" } 61 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 62 | room = { id = "androidx.room", version.ref = "room" } 63 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 64 | buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildConfig" } 65 | vanniktech-maven = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-maven" } 66 | 67 | 68 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 13 17:14:58 KST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; }; 11 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; }; 12 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | A93A953729CC810C00F8E227 /* commit-mate.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "commit-mate.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 17 | A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; 18 | A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 19 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXFrameworksBuildPhase section */ 23 | A93A953429CC810C00F8E227 /* Frameworks */ = { 24 | isa = PBXFrameworksBuildPhase; 25 | buildActionMask = 2147483647; 26 | files = ( 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXFrameworksBuildPhase section */ 31 | 32 | /* Begin PBXGroup section */ 33 | A93A952E29CC810C00F8E227 = { 34 | isa = PBXGroup; 35 | children = ( 36 | A93A953929CC810C00F8E227 /* iosApp */, 37 | A93A953829CC810C00F8E227 /* Products */, 38 | C4127409AE3703430489E7BC /* Frameworks */, 39 | ); 40 | sourceTree = ""; 41 | }; 42 | A93A953829CC810C00F8E227 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | A93A953729CC810C00F8E227 /* commit-mate.app */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | A93A953929CC810C00F8E227 /* iosApp */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | A93A953A29CC810C00F8E227 /* iosApp.swift */, 54 | A93A953E29CC810D00F8E227 /* Assets.xcassets */, 55 | A93A954029CC810D00F8E227 /* Preview Content */, 56 | ); 57 | path = iosApp; 58 | sourceTree = ""; 59 | }; 60 | A93A954029CC810D00F8E227 /* Preview Content */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */, 64 | ); 65 | path = "Preview Content"; 66 | sourceTree = ""; 67 | }; 68 | C4127409AE3703430489E7BC /* Frameworks */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | ); 72 | name = Frameworks; 73 | sourceTree = ""; 74 | }; 75 | /* End PBXGroup section */ 76 | 77 | /* Begin PBXNativeTarget section */ 78 | A93A953629CC810C00F8E227 /* iosApp */ = { 79 | isa = PBXNativeTarget; 80 | buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */; 81 | buildPhases = ( 82 | A9D80A052AAB5CDE006C8738 /* ShellScript */, 83 | A93A953329CC810C00F8E227 /* Sources */, 84 | A93A953429CC810C00F8E227 /* Frameworks */, 85 | A93A953529CC810C00F8E227 /* Resources */, 86 | ); 87 | buildRules = ( 88 | ); 89 | dependencies = ( 90 | ); 91 | name = iosApp; 92 | productName = iosApp; 93 | productReference = A93A953729CC810C00F8E227 /* commit-mate.app */; 94 | productType = "com.apple.product-type.application"; 95 | }; 96 | /* End PBXNativeTarget section */ 97 | 98 | /* Begin PBXProject section */ 99 | A93A952F29CC810C00F8E227 /* Project object */ = { 100 | isa = PBXProject; 101 | attributes = { 102 | LastSwiftUpdateCheck = 1420; 103 | LastUpgradeCheck = 1420; 104 | TargetAttributes = { 105 | A93A953629CC810C00F8E227 = { 106 | CreatedOnToolsVersion = 14.2; 107 | }; 108 | }; 109 | }; 110 | buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */; 111 | compatibilityVersion = "Xcode 14.0"; 112 | developmentRegion = en; 113 | hasScannedForEncodings = 0; 114 | knownRegions = ( 115 | en, 116 | Base, 117 | ); 118 | mainGroup = A93A952E29CC810C00F8E227; 119 | productRefGroup = A93A953829CC810C00F8E227 /* Products */; 120 | projectDirPath = ""; 121 | projectRoot = ""; 122 | targets = ( 123 | A93A953629CC810C00F8E227 /* iosApp */, 124 | ); 125 | }; 126 | /* End PBXProject section */ 127 | 128 | /* Begin PBXResourcesBuildPhase section */ 129 | A93A953529CC810C00F8E227 /* Resources */ = { 130 | isa = PBXResourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */, 134 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXResourcesBuildPhase section */ 139 | 140 | /* Begin PBXShellScriptBuildPhase section */ 141 | A9D80A052AAB5CDE006C8738 /* ShellScript */ = { 142 | isa = PBXShellScriptBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | ); 146 | inputFileListPaths = ( 147 | ); 148 | inputPaths = ( 149 | ); 150 | outputFileListPaths = ( 151 | ); 152 | outputPaths = ( 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | shellPath = /bin/sh; 156 | shellScript = "cd \"$SRCROOT/..\"\n./gradlew :sample:embedAndSignAppleFrameworkForXcode\n"; 157 | }; 158 | /* End PBXShellScriptBuildPhase section */ 159 | 160 | /* Begin PBXSourcesBuildPhase section */ 161 | A93A953329CC810C00F8E227 /* Sources */ = { 162 | isa = PBXSourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | A93A954329CC810D00F8E227 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_ANALYZER_NONNULL = YES; 177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 179 | CLANG_ENABLE_MODULES = YES; 180 | CLANG_ENABLE_OBJC_ARC = YES; 181 | CLANG_ENABLE_OBJC_WEAK = YES; 182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_COMMA = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 189 | CLANG_WARN_EMPTY_BODY = YES; 190 | CLANG_WARN_ENUM_CONVERSION = YES; 191 | CLANG_WARN_INFINITE_RECURSION = YES; 192 | CLANG_WARN_INT_CONVERSION = YES; 193 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 195 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 197 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 198 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 199 | CLANG_WARN_STRICT_PROTOTYPES = YES; 200 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 201 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | COPY_PHASE_STRIP = NO; 205 | DEBUG_INFORMATION_FORMAT = dwarf; 206 | ENABLE_STRICT_OBJC_MSGSEND = YES; 207 | ENABLE_TESTABILITY = YES; 208 | GCC_C_LANGUAGE_STANDARD = gnu11; 209 | GCC_DYNAMIC_NO_PIC = NO; 210 | GCC_NO_COMMON_BLOCKS = YES; 211 | GCC_OPTIMIZATION_LEVEL = 0; 212 | GCC_PREPROCESSOR_DEFINITIONS = ( 213 | "DEBUG=1", 214 | "$(inherited)", 215 | ); 216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 217 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 218 | GCC_WARN_UNDECLARED_SELECTOR = YES; 219 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 220 | GCC_WARN_UNUSED_FUNCTION = YES; 221 | GCC_WARN_UNUSED_VARIABLE = YES; 222 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 224 | MTL_FAST_MATH = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SDKROOT = iphoneos; 227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 229 | }; 230 | name = Debug; 231 | }; 232 | A93A954429CC810D00F8E227 /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 239 | CLANG_ENABLE_MODULES = YES; 240 | CLANG_ENABLE_OBJC_ARC = YES; 241 | CLANG_ENABLE_OBJC_WEAK = YES; 242 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 243 | CLANG_WARN_BOOL_CONVERSION = YES; 244 | CLANG_WARN_COMMA = YES; 245 | CLANG_WARN_CONSTANT_CONVERSION = YES; 246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 258 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 259 | CLANG_WARN_STRICT_PROTOTYPES = YES; 260 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 261 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | COPY_PHASE_STRIP = NO; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu11; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Release; 285 | }; 286 | A93A954629CC810D00F8E227 /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 291 | CODE_SIGN_STYLE = Automatic; 292 | CURRENT_PROJECT_VERSION = 1; 293 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 294 | ENABLE_PREVIEWS = YES; 295 | GENERATE_INFOPLIST_FILE = YES; 296 | INFOPLIST_FILE = iosApp/Info.plist; 297 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 298 | LD_RUNPATH_SEARCH_PATHS = ( 299 | "$(inherited)", 300 | "@executable_path/Frameworks", 301 | ); 302 | MARKETING_VERSION = 1.0; 303 | PRODUCT_BUNDLE_IDENTIFIER = io.github.kezlab.commitmate.iosApp; 304 | PRODUCT_NAME = "commit-mate"; 305 | SWIFT_EMIT_LOC_STRINGS = YES; 306 | SWIFT_VERSION = 5.0; 307 | TARGETED_DEVICE_FAMILY = "1,2"; 308 | }; 309 | name = Debug; 310 | }; 311 | A93A954729CC810D00F8E227 /* Release */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 315 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 316 | CODE_SIGN_STYLE = Automatic; 317 | CURRENT_PROJECT_VERSION = 1; 318 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 319 | ENABLE_PREVIEWS = YES; 320 | GENERATE_INFOPLIST_FILE = YES; 321 | INFOPLIST_FILE = iosApp/Info.plist; 322 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 323 | LD_RUNPATH_SEARCH_PATHS = ( 324 | "$(inherited)", 325 | "@executable_path/Frameworks", 326 | ); 327 | MARKETING_VERSION = 1.0; 328 | PRODUCT_BUNDLE_IDENTIFIER = io.github.kezlab.commitmate.iosApp; 329 | PRODUCT_NAME = "commit-mate"; 330 | SWIFT_EMIT_LOC_STRINGS = YES; 331 | SWIFT_VERSION = 5.0; 332 | TARGETED_DEVICE_FAMILY = "1,2"; 333 | }; 334 | name = Release; 335 | }; 336 | /* End XCBuildConfiguration section */ 337 | 338 | /* Begin XCConfigurationList section */ 339 | A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = { 340 | isa = XCConfigurationList; 341 | buildConfigurations = ( 342 | A93A954329CC810D00F8E227 /* Debug */, 343 | A93A954429CC810D00F8E227 /* Release */, 344 | ); 345 | defaultConfigurationIsVisible = 0; 346 | defaultConfigurationName = Release; 347 | }; 348 | A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 349 | isa = XCConfigurationList; 350 | buildConfigurations = ( 351 | A93A954629CC810D00F8E227 /* Debug */, 352 | A93A954729CC810D00F8E227 /* Release */, 353 | ); 354 | defaultConfigurationIsVisible = 0; 355 | defaultConfigurationName = Release; 356 | }; 357 | /* End XCConfigurationList section */ 358 | }; 359 | rootObject = A93A952F29CC810C00F8E227 /* Project object */; 360 | } 361 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kez-lab/Compose-DateTimePicker/29363635447d9c79f8c20f09a7899f78f1064701/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon@2x.png", 5 | "idiom": "iphone", 6 | "scale": "2x", 7 | "size": "60x60" 8 | }, 9 | { 10 | "filename": "AppIcon@3x.png", 11 | "idiom": "iphone", 12 | "scale": "3x", 13 | "size": "60x60" 14 | }, 15 | { 16 | "filename": "AppIcon~ipad.png", 17 | "idiom": "ipad", 18 | "scale": "1x", 19 | "size": "76x76" 20 | }, 21 | { 22 | "filename": "AppIcon@2x~ipad.png", 23 | "idiom": "ipad", 24 | "scale": "2x", 25 | "size": "76x76" 26 | }, 27 | { 28 | "filename": "AppIcon-83.5@2x~ipad.png", 29 | "idiom": "ipad", 30 | "scale": "2x", 31 | "size": "83.5x83.5" 32 | }, 33 | { 34 | "filename": "AppIcon-40@2x.png", 35 | "idiom": "iphone", 36 | "scale": "2x", 37 | "size": "40x40" 38 | }, 39 | { 40 | "filename": "AppIcon-40@3x.png", 41 | "idiom": "iphone", 42 | "scale": "3x", 43 | "size": "40x40" 44 | }, 45 | { 46 | "filename": "AppIcon-40~ipad.png", 47 | "idiom": "ipad", 48 | "scale": "1x", 49 | "size": "40x40" 50 | }, 51 | { 52 | "filename": "AppIcon-40@2x~ipad.png", 53 | "idiom": "ipad", 54 | "scale": "2x", 55 | "size": "40x40" 56 | }, 57 | { 58 | "filename": "AppIcon-20@2x.png", 59 | "idiom": "iphone", 60 | "scale": "2x", 61 | "size": "20x20" 62 | }, 63 | { 64 | "filename": "AppIcon-20@3x.png", 65 | "idiom": "iphone", 66 | "scale": "3x", 67 | "size": "20x20" 68 | }, 69 | { 70 | "filename": "AppIcon-20~ipad.png", 71 | "idiom": "ipad", 72 | "scale": "1x", 73 | "size": "20x20" 74 | }, 75 | { 76 | "filename": "AppIcon-20@2x~ipad.png", 77 | "idiom": "ipad", 78 | "scale": "2x", 79 | "size": "20x20" 80 | }, 81 | { 82 | "filename": "AppIcon-29.png", 83 | "idiom": "iphone", 84 | "scale": "1x", 85 | "size": "29x29" 86 | }, 87 | { 88 | "filename": "AppIcon-29@2x.png", 89 | "idiom": "iphone", 90 | "scale": "2x", 91 | "size": "29x29" 92 | }, 93 | { 94 | "filename": "AppIcon-29@3x.png", 95 | "idiom": "iphone", 96 | "scale": "3x", 97 | "size": "29x29" 98 | }, 99 | { 100 | "filename": "AppIcon-29~ipad.png", 101 | "idiom": "ipad", 102 | "scale": "1x", 103 | "size": "29x29" 104 | }, 105 | { 106 | "filename": "AppIcon-29@2x~ipad.png", 107 | "idiom": "ipad", 108 | "scale": "2x", 109 | "size": "29x29" 110 | }, 111 | { 112 | "filename": "AppIcon-60@2x~car.png", 113 | "idiom": "car", 114 | "scale": "2x", 115 | "size": "60x60" 116 | }, 117 | { 118 | "filename": "AppIcon-60@3x~car.png", 119 | "idiom": "car", 120 | "scale": "3x", 121 | "size": "60x60" 122 | }, 123 | { 124 | "filename": "AppIcon~ios-marketing.png", 125 | "idiom": "ios-marketing", 126 | "scale": "1x", 127 | "size": "1024x1024" 128 | } 129 | ] 130 | } 131 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/iosApp.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import sample 3 | 4 | @main 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | var window: UIWindow? 7 | 8 | func application( 9 | _ application: UIApplication, 10 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 11 | ) -> Bool { 12 | window = UIWindow(frame: UIScreen.main.bounds) 13 | if let window = window { 14 | window.rootViewController = MainKt.MainViewController() 15 | window.makeKeyAndVisible() 16 | } 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.multiplatform) 3 | alias(libs.plugins.android.application) 4 | alias(libs.plugins.compose) 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.kotlinx.serialization) 7 | } 8 | 9 | kotlin { 10 | jvmToolchain(17) 11 | 12 | androidTarget { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "17" 16 | } 17 | } 18 | } 19 | 20 | iosX64() 21 | iosArm64() 22 | iosSimulatorArm64() 23 | 24 | listOf( 25 | iosX64(), 26 | iosArm64(), 27 | iosSimulatorArm64() 28 | ).forEach { 29 | it.binaries.framework { 30 | baseName = "sample" 31 | isStatic = true 32 | } 33 | } 34 | 35 | jvm("desktop") 36 | 37 | js(IR) { 38 | browser() 39 | } 40 | 41 | sourceSets { 42 | commonMain.dependencies { 43 | implementation(project(":datetimepicker")) 44 | 45 | implementation(compose.runtime) 46 | implementation(compose.foundation) 47 | implementation(compose.material3) 48 | implementation(compose.ui) 49 | implementation(compose.components.resources) 50 | 51 | implementation(libs.kotlinx.datetime) 52 | implementation(libs.kotlinx.coroutines.core) 53 | 54 | implementation(libs.composeIcons.featherIcons) 55 | 56 | } 57 | 58 | commonTest.dependencies { 59 | implementation(kotlin("test")) 60 | } 61 | 62 | androidMain.dependencies { 63 | implementation(libs.androidx.activityCompose) 64 | implementation(libs.kotlinx.coroutines.android) 65 | implementation("androidx.core:core-splashscreen:1.0.1") 66 | } 67 | 68 | iosMain.dependencies { 69 | // iOS-specific dependencies if needed 70 | } 71 | 72 | val desktopMain by getting { 73 | dependencies { 74 | implementation(compose.desktop.currentOs) 75 | implementation(libs.kotlinx.coroutines.swing) 76 | } 77 | } 78 | 79 | val jsMain by getting { 80 | dependencies { 81 | implementation(compose.web.core) 82 | } 83 | } 84 | } 85 | } 86 | 87 | android { 88 | namespace = "com.kez.picker.sample" 89 | compileSdk = 35 90 | 91 | defaultConfig { 92 | applicationId = "com.kez.picker.sample" 93 | minSdk = 24 94 | targetSdk = 35 95 | versionCode = 1 96 | versionName = "1.0.0" 97 | } 98 | 99 | packaging { 100 | resources { 101 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 102 | } 103 | } 104 | 105 | buildTypes { 106 | release { 107 | isMinifyEnabled = true 108 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 109 | } 110 | } 111 | 112 | compileOptions { 113 | sourceCompatibility = JavaVersion.VERSION_17 114 | targetCompatibility = JavaVersion.VERSION_17 115 | } 116 | 117 | buildFeatures { 118 | compose = true 119 | } 120 | } -------------------------------------------------------------------------------- /sample/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/androidMain/kotlin/com/kez/picker/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.ui.Modifier 13 | 14 | class MainActivity : ComponentActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | enableEdgeToEdge() 18 | 19 | setContent { 20 | MaterialTheme { 21 | Scaffold { paddingValues -> 22 | Surface( 23 | modifier = Modifier 24 | .fillMaxSize() 25 | .padding(paddingValues), 26 | color = MaterialTheme.colorScheme.background 27 | ) { 28 | App() 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /sample/src/commonMain/kotlin/com/kez/picker/sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.SizeTransform 6 | import androidx.compose.animation.core.Spring 7 | import androidx.compose.animation.core.spring 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.animation.slideInHorizontally 12 | import androidx.compose.animation.slideInVertically 13 | import androidx.compose.animation.slideOutHorizontally 14 | import androidx.compose.animation.togetherWith 15 | import androidx.compose.foundation.background 16 | import androidx.compose.foundation.layout.Arrangement 17 | import androidx.compose.foundation.layout.Box 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.Row 20 | import androidx.compose.foundation.layout.Spacer 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.height 24 | import androidx.compose.foundation.layout.padding 25 | import androidx.compose.foundation.layout.size 26 | import androidx.compose.foundation.layout.width 27 | import androidx.compose.foundation.shape.CircleShape 28 | import androidx.compose.foundation.shape.RoundedCornerShape 29 | import androidx.compose.material.icons.Icons 30 | import androidx.compose.material.icons.filled.DateRange 31 | import androidx.compose.material3.Button 32 | import androidx.compose.material3.ButtonDefaults 33 | import androidx.compose.material3.Card 34 | import androidx.compose.material3.CardDefaults 35 | import androidx.compose.material3.CenterAlignedTopAppBar 36 | import androidx.compose.material3.Divider 37 | import androidx.compose.material3.ExperimentalMaterial3Api 38 | import androidx.compose.material3.Icon 39 | import androidx.compose.material3.MaterialTheme 40 | import androidx.compose.material3.NavigationBar 41 | import androidx.compose.material3.NavigationBarItem 42 | import androidx.compose.material3.Scaffold 43 | import androidx.compose.material3.Surface 44 | import androidx.compose.material3.Tab 45 | import androidx.compose.material3.TabRow 46 | import androidx.compose.material3.Text 47 | import androidx.compose.material3.darkColorScheme 48 | import androidx.compose.material3.lightColorScheme 49 | import androidx.compose.runtime.Composable 50 | import androidx.compose.runtime.LaunchedEffect 51 | import androidx.compose.runtime.getValue 52 | import androidx.compose.runtime.mutableIntStateOf 53 | import androidx.compose.runtime.mutableStateOf 54 | import androidx.compose.runtime.remember 55 | import androidx.compose.runtime.setValue 56 | import androidx.compose.ui.Alignment 57 | import androidx.compose.ui.Modifier 58 | import androidx.compose.ui.draw.clip 59 | import androidx.compose.ui.graphics.Color 60 | import androidx.compose.ui.text.TextStyle 61 | import androidx.compose.ui.text.font.FontWeight 62 | import androidx.compose.ui.unit.dp 63 | import androidx.compose.ui.unit.sp 64 | import com.kez.picker.date.YearMonthPicker 65 | import com.kez.picker.rememberPickerState 66 | import com.kez.picker.time.TimePicker 67 | import com.kez.picker.util.TimeFormat 68 | import com.kez.picker.util.TimePeriod 69 | import com.kez.picker.util.currentDate 70 | import com.kez.picker.util.currentHour 71 | import com.kez.picker.util.currentMinute 72 | import compose.icons.FeatherIcons 73 | import compose.icons.feathericons.Clock 74 | import compose.icons.feathericons.Github 75 | 76 | // 커스텀 테마 색상 77 | private val LightThemeColors = lightColorScheme( 78 | primary = Color(0xFF5C6BC0), 79 | onPrimary = Color.White, 80 | secondary = Color(0xFF26C6DA), 81 | tertiary = Color(0xFFEF5350), 82 | background = Color(0xFFF5F5F5), 83 | surface = Color.White, 84 | onSurface = Color(0xFF121212) 85 | ) 86 | 87 | private val DarkThemeColors = darkColorScheme( 88 | primary = Color(0xFF8C9EFF), 89 | onPrimary = Color.Black, 90 | secondary = Color(0xFF80DEEA), 91 | tertiary = Color(0xFFEF9A9A), 92 | background = Color(0xFF121212), 93 | surface = Color(0xFF242424), 94 | onSurface = Color(0xFFE1E1E1) 95 | ) 96 | 97 | /** 98 | * 앱의 진입점 컴포저블 99 | */ 100 | @Composable 101 | fun App() { 102 | var isDarkTheme by remember { mutableStateOf(false) } 103 | 104 | MaterialTheme( 105 | colorScheme = if (isDarkTheme) DarkThemeColors else LightThemeColors 106 | ) { 107 | AppContent() 108 | } 109 | } 110 | 111 | /** 112 | * 앱 내용 컴포저블 113 | */ 114 | @OptIn(ExperimentalMaterial3Api::class) 115 | @Composable 116 | private fun AppContent() { 117 | var selectedTabIndex by remember { mutableIntStateOf(0) } 118 | 119 | Scaffold( 120 | topBar = { 121 | CenterAlignedTopAppBar( 122 | title = { 123 | Text( 124 | text = "DateTimePicker", 125 | style = MaterialTheme.typography.titleLarge.copy( 126 | fontWeight = FontWeight.Bold 127 | ) 128 | ) 129 | } 130 | ) 131 | }, 132 | bottomBar = { 133 | NavigationBar { 134 | NavigationBarItem( 135 | selected = selectedTabIndex == 0, 136 | onClick = { selectedTabIndex = 0 }, 137 | icon = { Icon(FeatherIcons.Clock, contentDescription = "시간") }, 138 | label = { Text("시간 선택") } 139 | ) 140 | NavigationBarItem( 141 | selected = selectedTabIndex == 1, 142 | onClick = { selectedTabIndex = 1 }, 143 | icon = { Icon(Icons.Filled.DateRange, contentDescription = "날짜") }, 144 | label = { Text("날짜 선택") } 145 | ) 146 | } 147 | } 148 | ) { paddingValues -> 149 | Surface( 150 | modifier = Modifier 151 | .fillMaxSize() 152 | .padding(paddingValues), 153 | color = MaterialTheme.colorScheme.background 154 | ) { 155 | when (selectedTabIndex) { 156 | 0 -> TimePickerScreen() 157 | 1 -> DatePickerScreen() 158 | } 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * 시간 선택기 화면 165 | */ 166 | @Composable 167 | fun TimePickerScreen() { 168 | var selectedFormat by remember { mutableIntStateOf(0) } 169 | var showSelectedTime by remember { mutableStateOf(false) } 170 | 171 | // 모든 상태 미리 생성 및 공유 172 | val hour12State = rememberPickerState( 173 | if (currentHour > 12) currentHour - 12 else if (currentHour == 0) 12 else currentHour 174 | ) 175 | val hour24State = rememberPickerState(currentHour) 176 | val minuteState = rememberPickerState(currentMinute) 177 | val periodState = rememberPickerState(if (currentHour >= 12) TimePeriod.PM else TimePeriod.AM) 178 | 179 | // 선택된 시간 텍스트 180 | val selectedTimeText = remember( 181 | hour12State.selectedItem, hour24State.selectedItem, 182 | minuteState.selectedItem, periodState.selectedItem, selectedFormat 183 | ) { 184 | if (selectedFormat == 0) { 185 | val period = periodState.selectedItem 186 | val hour = hour12State.selectedItem 187 | val minute = minuteState.selectedItem 188 | formatTime12(hour, minute, period) 189 | } else { 190 | val hour = hour24State.selectedItem 191 | val minute = minuteState.selectedItem 192 | formatTime24(hour, minute) 193 | } 194 | } 195 | 196 | LaunchedEffect(selectedFormat) { 197 | showSelectedTime = false 198 | } 199 | 200 | Column( 201 | modifier = Modifier 202 | .fillMaxSize() 203 | .padding(16.dp), 204 | horizontalAlignment = Alignment.CenterHorizontally 205 | ) { 206 | // 헤더 207 | Text( 208 | text = "시간 선택", 209 | style = MaterialTheme.typography.headlineMedium, 210 | modifier = Modifier.padding(bottom = 16.dp), 211 | fontWeight = FontWeight.Bold 212 | ) 213 | 214 | // 탭 로우 215 | TabRow( 216 | selectedTabIndex = selectedFormat, 217 | modifier = Modifier 218 | .padding(horizontal = 16.dp) 219 | .clip(RoundedCornerShape(16.dp)) 220 | ) { 221 | Tab( 222 | selected = selectedFormat == 0, 223 | onClick = { selectedFormat = 0 }, 224 | text = { Text("12시간제") } 225 | ) 226 | Tab( 227 | selected = selectedFormat == 1, 228 | onClick = { selectedFormat = 1 }, 229 | text = { Text("24시간제") } 230 | ) 231 | } 232 | 233 | Spacer(modifier = Modifier.height(32.dp)) 234 | 235 | // 선택기 카드 236 | Card( 237 | modifier = Modifier.padding(16.dp), 238 | shape = RoundedCornerShape(16.dp), 239 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), 240 | colors = CardDefaults.cardColors( 241 | containerColor = MaterialTheme.colorScheme.surface 242 | ) 243 | ) { 244 | // 공통 패딩과 정렬이 있는 컨테이너 245 | Column( 246 | modifier = Modifier.padding(24.dp), 247 | horizontalAlignment = Alignment.CenterHorizontally 248 | ) { 249 | // AnimatedContent를 사용해 두 선택기 간 전환 250 | AnimatedContent( 251 | targetState = selectedFormat, 252 | transitionSpec = { 253 | val direction = if (targetState > initialState) 1 else -1 254 | slideInHorizontally { width -> direction * width } + fadeIn( 255 | animationSpec = tween(durationMillis = 300) 256 | ) togetherWith slideOutHorizontally { width -> -direction * width } + fadeOut( 257 | animationSpec = tween(durationMillis = 300) 258 | ) using SizeTransform(clip = false) 259 | }, 260 | label = "TimePicker Format Animation" 261 | ) { format -> 262 | // 고정된 크기의 컨테이너를 제공하여 애니메이션 중 레이아웃 변화 최소화 263 | Box( 264 | modifier = Modifier 265 | .fillMaxWidth() 266 | .height(220.dp), // 충분한 고정 높이 지정 267 | contentAlignment = Alignment.Center 268 | ) { 269 | if (format == 0) { 270 | // 12시간제 타임피커 271 | TimePicker( 272 | hourPickerState = hour12State, 273 | minutePickerState = minuteState, 274 | periodPickerState = periodState, 275 | timeFormat = TimeFormat.HOUR_12, 276 | textStyle = TextStyle( 277 | fontSize = 16.sp, 278 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) 279 | ), 280 | selectedTextStyle = TextStyle( 281 | fontSize = 20.sp, 282 | color = MaterialTheme.colorScheme.onSurface, 283 | fontWeight = FontWeight.Bold 284 | ), 285 | dividerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), 286 | pickerWidth = 64.dp 287 | ) 288 | } else { 289 | // 24시간제 타임피커 290 | TimePicker( 291 | hourPickerState = hour24State, 292 | minutePickerState = minuteState, 293 | timeFormat = TimeFormat.HOUR_24, 294 | textStyle = TextStyle( 295 | fontSize = 16.sp, 296 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) 297 | ), 298 | selectedTextStyle = TextStyle( 299 | fontSize = 20.sp, 300 | color = MaterialTheme.colorScheme.onSurface, 301 | fontWeight = FontWeight.Bold 302 | ), 303 | dividerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), 304 | pickerWidth = 64.dp 305 | ) 306 | } 307 | } 308 | } 309 | } 310 | } 311 | 312 | Spacer(modifier = Modifier.height(24.dp)) 313 | 314 | // 확인 버튼 315 | Button( 316 | onClick = { showSelectedTime = true }, 317 | shape = RoundedCornerShape(12.dp), 318 | colors = ButtonDefaults.buttonColors( 319 | containerColor = MaterialTheme.colorScheme.primary 320 | ), 321 | modifier = Modifier 322 | .padding(16.dp) 323 | .fillMaxWidth() 324 | .height(48.dp) 325 | ) { 326 | Text("시간 확인") 327 | } 328 | 329 | Spacer(modifier = Modifier.height(16.dp)) 330 | 331 | // 선택된 시간 표시 332 | AnimatedVisibility( 333 | visible = showSelectedTime, 334 | enter = fadeIn() + slideInVertically( 335 | initialOffsetY = { 40 }, 336 | animationSpec = spring( 337 | dampingRatio = Spring.DampingRatioMediumBouncy, 338 | stiffness = Spring.StiffnessLow 339 | ) 340 | ) 341 | ) { 342 | Card( 343 | shape = RoundedCornerShape(12.dp), 344 | colors = CardDefaults.cardColors( 345 | containerColor = MaterialTheme.colorScheme.primaryContainer 346 | ), 347 | modifier = Modifier.padding(horizontal = 16.dp) 348 | ) { 349 | Row( 350 | horizontalArrangement = Arrangement.Center, 351 | verticalAlignment = Alignment.CenterVertically, 352 | modifier = Modifier 353 | .padding(16.dp) 354 | .fillMaxWidth() 355 | ) { 356 | Icon( 357 | imageVector = FeatherIcons.Clock, 358 | contentDescription = null, 359 | tint = MaterialTheme.colorScheme.onPrimaryContainer 360 | ) 361 | Spacer(modifier = Modifier.width(8.dp)) 362 | Text( 363 | text = "선택한 시간: $selectedTimeText", 364 | style = MaterialTheme.typography.bodyLarge, 365 | color = MaterialTheme.colorScheme.onPrimaryContainer, 366 | fontWeight = FontWeight.Medium 367 | ) 368 | } 369 | } 370 | } 371 | } 372 | } 373 | 374 | /** 375 | * 날짜 선택기 화면 376 | */ 377 | @Composable 378 | fun DatePickerScreen() { 379 | var showSelectedDate by remember { mutableStateOf(false) } 380 | 381 | // 상태 관리 382 | val yearState = rememberPickerState(currentDate.year) 383 | val monthState = rememberPickerState(currentDate.monthNumber) 384 | 385 | // 선택된 날짜 텍스트 386 | val selectedDateText = remember(yearState.selectedItem, monthState.selectedItem) { 387 | val year = yearState.selectedItem ?: currentDate.year 388 | val month = monthState.selectedItem ?: currentDate.monthNumber 389 | val monthName = getMonthName(month) 390 | "${year}년 $monthName" 391 | } 392 | 393 | Column( 394 | modifier = Modifier 395 | .fillMaxSize() 396 | .padding(16.dp), 397 | horizontalAlignment = Alignment.CenterHorizontally 398 | ) { 399 | // 헤더 400 | Text( 401 | text = "날짜 선택", 402 | style = MaterialTheme.typography.headlineMedium, 403 | modifier = Modifier.padding(bottom = 24.dp), 404 | fontWeight = FontWeight.Bold 405 | ) 406 | 407 | // 현재 년월 표시 408 | Row( 409 | modifier = Modifier 410 | .fillMaxWidth() 411 | .padding(16.dp), 412 | horizontalArrangement = Arrangement.Center, 413 | verticalAlignment = Alignment.CenterVertically 414 | ) { 415 | Box( 416 | modifier = Modifier 417 | .size(8.dp) 418 | .background(MaterialTheme.colorScheme.primary, CircleShape) 419 | ) 420 | Spacer(modifier = Modifier.width(8.dp)) 421 | Text( 422 | text = "현재: ${currentDate.year}년 ${currentDate.monthNumber}월", 423 | style = MaterialTheme.typography.bodyMedium, 424 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f) 425 | ) 426 | } 427 | 428 | Spacer(modifier = Modifier.height(16.dp)) 429 | 430 | // 선택기 카드 431 | Card( 432 | modifier = Modifier.padding(16.dp), 433 | shape = RoundedCornerShape(16.dp), 434 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), 435 | colors = CardDefaults.cardColors( 436 | containerColor = MaterialTheme.colorScheme.surface 437 | ) 438 | ) { 439 | Column( 440 | modifier = Modifier.padding(24.dp), 441 | horizontalAlignment = Alignment.CenterHorizontally 442 | ) { 443 | // 커스텀 헤더 444 | Row( 445 | modifier = Modifier 446 | .fillMaxWidth() 447 | .padding(bottom = 16.dp), 448 | horizontalArrangement = Arrangement.SpaceAround 449 | ) { 450 | Text( 451 | text = "년도", 452 | style = MaterialTheme.typography.titleMedium, 453 | fontWeight = FontWeight.Medium, 454 | color = MaterialTheme.colorScheme.primary 455 | ) 456 | Text( 457 | text = "월", 458 | style = MaterialTheme.typography.titleMedium, 459 | fontWeight = FontWeight.Medium, 460 | color = MaterialTheme.colorScheme.primary 461 | ) 462 | } 463 | 464 | Divider( 465 | color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), 466 | modifier = Modifier.padding(bottom = 16.dp) 467 | ) 468 | 469 | // 년월 선택기 470 | YearMonthPicker( 471 | yearPickerState = yearState, 472 | monthPickerState = monthState, 473 | textStyle = TextStyle( 474 | fontSize = 16.sp, 475 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) 476 | ), 477 | selectedTextStyle = TextStyle( 478 | fontSize = 20.sp, 479 | color = MaterialTheme.colorScheme.onSurface, 480 | fontWeight = FontWeight.Bold 481 | ), 482 | dividerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), 483 | pickerWidth = 100.dp 484 | ) 485 | } 486 | } 487 | 488 | Spacer(modifier = Modifier.height(24.dp)) 489 | 490 | // 확인 버튼 491 | Button( 492 | onClick = { showSelectedDate = true }, 493 | shape = RoundedCornerShape(12.dp), 494 | colors = ButtonDefaults.buttonColors( 495 | containerColor = MaterialTheme.colorScheme.primary 496 | ), 497 | modifier = Modifier 498 | .padding(16.dp) 499 | .fillMaxWidth() 500 | .height(48.dp) 501 | ) { 502 | Text("날짜 확인") 503 | } 504 | 505 | Spacer(modifier = Modifier.height(16.dp)) 506 | 507 | // 선택된 날짜 표시 508 | AnimatedVisibility( 509 | visible = showSelectedDate, 510 | enter = fadeIn() + slideInVertically( 511 | initialOffsetY = { 40 }, 512 | animationSpec = spring( 513 | dampingRatio = Spring.DampingRatioMediumBouncy, 514 | stiffness = Spring.StiffnessLow 515 | ) 516 | ) 517 | ) { 518 | Card( 519 | shape = RoundedCornerShape(12.dp), 520 | colors = CardDefaults.cardColors( 521 | containerColor = MaterialTheme.colorScheme.primaryContainer 522 | ), 523 | modifier = Modifier.padding(horizontal = 16.dp) 524 | ) { 525 | Row( 526 | horizontalArrangement = Arrangement.Center, 527 | verticalAlignment = Alignment.CenterVertically, 528 | modifier = Modifier 529 | .padding(16.dp) 530 | .fillMaxWidth() 531 | ) { 532 | Icon( 533 | imageVector = Icons.Filled.DateRange, 534 | contentDescription = null, 535 | tint = MaterialTheme.colorScheme.onPrimaryContainer 536 | ) 537 | Spacer(modifier = Modifier.width(8.dp)) 538 | Text( 539 | text = "선택한 날짜: $selectedDateText", 540 | style = MaterialTheme.typography.bodyLarge, 541 | color = MaterialTheme.colorScheme.onPrimaryContainer, 542 | fontWeight = FontWeight.Medium 543 | ) 544 | } 545 | } 546 | } 547 | 548 | Spacer(modifier = Modifier.weight(1f)) 549 | 550 | // 푸터 551 | Row( 552 | modifier = Modifier 553 | .fillMaxWidth() 554 | .padding(8.dp), 555 | horizontalArrangement = Arrangement.Center, 556 | verticalAlignment = Alignment.CenterVertically 557 | ) { 558 | Text( 559 | text = "Copyright © 2024 KEZ Lab", 560 | style = MaterialTheme.typography.bodySmall, 561 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) 562 | ) 563 | Spacer(modifier = Modifier.width(8.dp)) 564 | Icon( 565 | imageVector = FeatherIcons.Github, 566 | contentDescription = "GitHub", 567 | tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), 568 | modifier = Modifier.size(16.dp) 569 | ) 570 | } 571 | } 572 | } -------------------------------------------------------------------------------- /sample/src/commonMain/kotlin/com/kez/picker/sample/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import com.kez.picker.util.TimePeriod 4 | 5 | /** 6 | * 시간 형식화를 위한 확장 함수 - 12시간제 7 | */ 8 | fun formatTime12(hour: Int?, minute: Int?, period: TimePeriod?): String { 9 | val h = hour ?: 12 10 | val m = minute ?: 0 11 | val p = period ?: TimePeriod.AM 12 | return "${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')} $p" 13 | } 14 | 15 | /** 16 | * 시간 형식화를 위한 확장 함수 - 24시간제 17 | */ 18 | fun formatTime24(hour: Int?, minute: Int?): String { 19 | val h = hour ?: 0 20 | val m = minute ?: 0 21 | return "${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}" 22 | } 23 | 24 | /** 25 | * 월 이름 가져오기 함수 26 | */ 27 | fun getMonthName(month: Int): String { 28 | return when (month) { 29 | 1 -> "1월 (Jan)" 30 | 2 -> "2월 (Feb)" 31 | 3 -> "3월 (Mar)" 32 | 4 -> "4월 (Apr)" 33 | 5 -> "5월 (May)" 34 | 6 -> "6월 (Jun)" 35 | 7 -> "7월 (Jul)" 36 | 8 -> "8월 (Aug)" 37 | 9 -> "9월 (Sep)" 38 | 10 -> "10월 (Oct)" 39 | 11 -> "11월 (Nov)" 40 | 12 -> "12월 (Dec)" 41 | else -> "알 수 없음" 42 | } 43 | } -------------------------------------------------------------------------------- /sample/src/desktopMain/kotlin/com/kez/picker/sample/Main.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | import androidx.compose.ui.window.rememberWindowState 6 | import androidx.compose.ui.unit.DpSize 7 | import androidx.compose.ui.unit.dp 8 | 9 | fun main() = application { 10 | Window( 11 | onCloseRequest = ::exitApplication, 12 | title = "Picker Demo", 13 | state = rememberWindowState(size = DpSize(400.dp, 800.dp)) 14 | ) { 15 | App() 16 | } 17 | } -------------------------------------------------------------------------------- /sample/src/iosMain/kotlin/com/kez/picker/sample/main.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | 5 | fun MainViewController() = ComposeUIViewController { 6 | App() 7 | } -------------------------------------------------------------------------------- /sample/src/jsMain/kotlin/com/kez/picker/sample/main.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import androidx.compose.ui.window.Window 4 | import org.jetbrains.skiko.wasm.onWasmReady 5 | 6 | fun main() { 7 | onWasmReady { 8 | Window("DateTimePicker Demo") { 9 | App() 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /sample/src/jvmMain/kotlin/com/kez/picker/sample/main.kt: -------------------------------------------------------------------------------- 1 | package com.kez.picker.sample 2 | 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | import androidx.compose.ui.window.rememberWindowState 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.ui.Modifier 9 | 10 | fun main() = application { 11 | Window( 12 | onCloseRequest = ::exitApplication, 13 | title = "DateTimePicker Demo", 14 | state = rememberWindowState() 15 | ) { 16 | Surface(modifier = Modifier.fillMaxSize()) { 17 | App() 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | google { 5 | content { 6 | includeGroupByRegex("com\\.android.*") 7 | includeGroupByRegex("com\\.google.*") 8 | includeGroupByRegex("androidx.*") 9 | } 10 | } 11 | mavenCentral() 12 | gradlePluginPortal() 13 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 14 | } 15 | } 16 | dependencyResolutionManagement { 17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 18 | repositories { 19 | google() 20 | mavenCentral() 21 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 22 | } 23 | } 24 | 25 | rootProject.name = "Compose-DateTimePicker" 26 | include(":datetimepicker") 27 | include(":sample") 28 | include(":iosApp") 29 | --------------------------------------------------------------------------------