├── .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 |
--------------------------------------------------------------------------------