@momo-kits/native-kits 0.152.4-beta.6 → 0.152.4-maxapi
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +133 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +20 -0
- package/README.md +7 -0
- package/build.gradle.kts +32 -0
- package/compose/MoMoComposeKits.podspec +54 -0
- package/compose/build.gradle.kts +149 -0
- package/compose/src/androidMain/AndroidManifest.xml +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +105 -0
- package/compose/src/commonMain/composeResources/files/lottie_circle_loader.json +1 -0
- package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
- package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +715 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +214 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +77 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +27 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +334 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +345 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +131 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +143 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +179 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +111 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +384 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +160 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +234 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +236 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +228 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +34 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +85 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +95 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +64 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +208 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +172 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +199 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +237 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +13 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +191 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +258 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +111 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +94 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +159 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScaleSizeScope.kt +17 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +459 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +216 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +180 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +251 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +31 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +385 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +38 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +88 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +144 -0
- package/gradle.properties +19 -0
- package/gradlew +240 -0
- package/gradlew.bat +91 -0
- package/ios/Application/ApplicationEnvironment.swift +50 -0
- package/ios/Application/Components.swift +263 -0
- package/ios/Application/ComposeApi.swift +22 -0
- package/ios/Application/FloatingButton.swift +172 -0
- package/ios/Application/HeaderRight.swift +271 -0
- package/ios/Application/Screen.swift +249 -0
- package/ios/Badge/BadgeDot.swift +31 -0
- package/ios/Button/Button.swift +211 -0
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
- package/ios/Checkbox/Checkbox.swift +81 -0
- package/ios/Chip/Chip.swift +96 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +172 -0
- package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
- package/ios/Colors+Radius+Spacing/Spacing.swift +12 -0
- package/ios/Extensions/Color++.swift +25 -0
- package/ios/Icon/Icon.swift +51 -0
- package/ios/Image/Image.swift +70 -0
- package/ios/Input/Input.swift +207 -0
- package/ios/Input/InputPhoneNumber.swift +176 -0
- package/ios/Input/InputSearch.swift +238 -0
- package/ios/Input/InputTextArea.swift +242 -0
- package/ios/Lottie/LottieView.swift +86 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +41 -0
- package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
- package/ios/Popup/PopupDisplay.swift +284 -0
- package/ios/Popup/PopupInput.swift +96 -0
- package/ios/Popup/PopupPromotion.swift +73 -0
- package/ios/PopupView/FullscreenPopup.swift +251 -0
- package/ios/PopupView/Modifiers.swift +158 -0
- package/ios/PopupView/PopupView.swift +289 -0
- package/ios/PopupView/Utils++.swift +281 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
- package/ios/Swipeable/SwipeCell.swift +278 -0
- package/ios/Swipeable/SwipeCellModel.swift +86 -0
- package/ios/Switch/Switch.swift +44 -0
- package/ios/Template/Logo/Logo.swift +75 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
- package/ios/Theme.md +18 -0
- package/ios/Typography/Text.swift +140 -0
- package/ios/Typography/Typography.swift +95 -0
- package/ios/native-kits.podspec +18 -0
- package/package.json +6 -7
- package/settings.gradle.kts +25 -0
- package/shared/build.gradle.kts +0 -74
package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package vn.momo.kits.components.datetimepicker
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.layout.Column
|
|
5
|
+
import androidx.compose.foundation.layout.Row
|
|
6
|
+
import androidx.compose.foundation.layout.Spacer
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
8
|
+
import androidx.compose.foundation.layout.height
|
|
9
|
+
import androidx.compose.foundation.layout.padding
|
|
10
|
+
import androidx.compose.foundation.layout.width
|
|
11
|
+
import androidx.compose.runtime.Composable
|
|
12
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
13
|
+
import androidx.compose.runtime.getValue
|
|
14
|
+
import androidx.compose.runtime.mutableStateOf
|
|
15
|
+
import androidx.compose.runtime.remember
|
|
16
|
+
import androidx.compose.runtime.setValue
|
|
17
|
+
import androidx.compose.runtime.snapshotFlow
|
|
18
|
+
import androidx.compose.ui.Alignment
|
|
19
|
+
import androidx.compose.ui.Modifier
|
|
20
|
+
import androidx.compose.ui.unit.dp
|
|
21
|
+
import androidx.compose.ui.unit.max
|
|
22
|
+
import kotlinx.coroutines.flow.collect
|
|
23
|
+
import kotlinx.coroutines.flow.mapNotNull
|
|
24
|
+
import kotlinx.coroutines.flow.onEach
|
|
25
|
+
import kotlinx.datetime.LocalDateTime
|
|
26
|
+
import kotlinx.datetime.number
|
|
27
|
+
import vn.momo.kits.components.Text
|
|
28
|
+
import vn.momo.kits.const.AppTheme
|
|
29
|
+
import vn.momo.kits.const.Spacing
|
|
30
|
+
import vn.momo.kits.const.Typography
|
|
31
|
+
|
|
32
|
+
private val datePickerHeight = 210.dp
|
|
33
|
+
private val datePickerWithLabelsHeight = 238.dp
|
|
34
|
+
|
|
35
|
+
@Composable
|
|
36
|
+
fun DateTimePicker(
|
|
37
|
+
modifier: Modifier = Modifier,
|
|
38
|
+
format: String = "DD-MM-YYYY",
|
|
39
|
+
minuteInterval: Int = 1,
|
|
40
|
+
onChange: (LocalDateTime) -> Unit,
|
|
41
|
+
selectedValue: LocalDateTime? = null,
|
|
42
|
+
minDate: LocalDateTime? = null,
|
|
43
|
+
maxDate: LocalDateTime? = null,
|
|
44
|
+
arrayLabelTime: List<String> = emptyList()
|
|
45
|
+
) {
|
|
46
|
+
val effectiveSelectedValue = remember(selectedValue) {
|
|
47
|
+
selectedValue ?: getCurrentDateTime()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
val effectiveMinDate = remember(minDate) {
|
|
51
|
+
minDate ?: createRelativeDate(-10)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val effectiveMaxDate = remember(maxDate) {
|
|
55
|
+
maxDate ?: createRelativeDate(10)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
val needCheckRange = remember(minDate, maxDate) {
|
|
59
|
+
minDate == null && maxDate == null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
val initialValue = remember(effectiveMinDate, effectiveMaxDate, effectiveSelectedValue) {
|
|
63
|
+
when {
|
|
64
|
+
!needCheckRange -> effectiveSelectedValue
|
|
65
|
+
effectiveSelectedValue < effectiveMinDate -> effectiveMinDate
|
|
66
|
+
effectiveSelectedValue > effectiveMaxDate -> effectiveMaxDate
|
|
67
|
+
else -> effectiveSelectedValue
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var currentDate by remember(initialValue) {
|
|
72
|
+
mutableStateOf(
|
|
73
|
+
PickerData(
|
|
74
|
+
day = initialValue.dayOfMonth,
|
|
75
|
+
month = initialValue.month.number,
|
|
76
|
+
year = initialValue.year,
|
|
77
|
+
hour = initialValue.hour,
|
|
78
|
+
minute = initialValue.minute
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
val onWheelChangeValue = remember(currentDate, effectiveMinDate, effectiveMaxDate) {
|
|
84
|
+
{ name: String, value: String ->
|
|
85
|
+
var day = if (name == "day") value.toInt() else currentDate.day
|
|
86
|
+
var month = if (name == "month") value.toInt() else currentDate.month
|
|
87
|
+
val year = if (name == "year") value.toInt() else currentDate.year
|
|
88
|
+
val hour = if (name == "hour") value.toInt() else currentDate.hour
|
|
89
|
+
val minute = if (name == "minute") value.toInt() else currentDate.minute
|
|
90
|
+
val timeMode = if (name == "timeMode") value else currentDate.timeMode
|
|
91
|
+
if (name == "year") {
|
|
92
|
+
month = when (year) {
|
|
93
|
+
effectiveMaxDate.year -> month.coerceIn(
|
|
94
|
+
1,
|
|
95
|
+
effectiveMaxDate.monthNumber
|
|
96
|
+
)
|
|
97
|
+
effectiveMinDate.year -> month.coerceIn(
|
|
98
|
+
effectiveMinDate.monthNumber,
|
|
99
|
+
12,
|
|
100
|
+
)
|
|
101
|
+
else -> month
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (name == "month" || name == "year") {
|
|
105
|
+
val maxDayOfMonth = when (month) {
|
|
106
|
+
4, 6, 9, 11 -> 30
|
|
107
|
+
2 -> if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) 29 else 28
|
|
108
|
+
else -> 31
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
day = when {
|
|
112
|
+
year == effectiveMaxDate.year && month == effectiveMaxDate.monthNumber ->
|
|
113
|
+
day.coerceIn(1, effectiveMaxDate.dayOfMonth)
|
|
114
|
+
|
|
115
|
+
year == effectiveMinDate.year && month == effectiveMinDate.monthNumber ->
|
|
116
|
+
day.coerceIn(effectiveMinDate.dayOfMonth, maxDayOfMonth)
|
|
117
|
+
|
|
118
|
+
day > maxDayOfMonth -> maxDayOfMonth
|
|
119
|
+
else -> day
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
currentDate = PickerData(
|
|
123
|
+
day = day,
|
|
124
|
+
month = month,
|
|
125
|
+
year = year,
|
|
126
|
+
hour = hour,
|
|
127
|
+
minute = minute,
|
|
128
|
+
timeMode = timeMode,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
LaunchedEffect(effectiveMinDate, effectiveMaxDate) {
|
|
134
|
+
snapshotFlow { currentDate }
|
|
135
|
+
.mapNotNull {
|
|
136
|
+
val dateTime = it.toLocalDateTime()
|
|
137
|
+
if (!needCheckRange || dateTime in effectiveMinDate..effectiveMaxDate) dateTime
|
|
138
|
+
else null
|
|
139
|
+
}
|
|
140
|
+
.onEach(onChange)
|
|
141
|
+
.collect()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
val pickerHeight = if (arrayLabelTime.isNotEmpty())
|
|
145
|
+
datePickerWithLabelsHeight else
|
|
146
|
+
datePickerHeight
|
|
147
|
+
|
|
148
|
+
Row(
|
|
149
|
+
modifier = modifier
|
|
150
|
+
.fillMaxWidth()
|
|
151
|
+
.height(pickerHeight)
|
|
152
|
+
.background(AppTheme.current.colors.background.surface)
|
|
153
|
+
.padding(horizontal = Spacing.M),
|
|
154
|
+
verticalAlignment = Alignment.CenterVertically
|
|
155
|
+
) {
|
|
156
|
+
val dateComponents = getDateComponents(
|
|
157
|
+
format,
|
|
158
|
+
currentDate,
|
|
159
|
+
effectiveMinDate,
|
|
160
|
+
effectiveMaxDate,
|
|
161
|
+
minuteInterval,
|
|
162
|
+
)
|
|
163
|
+
dateComponents.forEachIndexed { index, component ->
|
|
164
|
+
val hasLabel = index < arrayLabelTime.size && arrayLabelTime[index].isNotEmpty()
|
|
165
|
+
|
|
166
|
+
Column(
|
|
167
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
168
|
+
modifier = Modifier.weight(1f)
|
|
169
|
+
) {
|
|
170
|
+
if (hasLabel) {
|
|
171
|
+
Text(
|
|
172
|
+
text = arrayLabelTime[index],
|
|
173
|
+
style = Typography.actionSBold,
|
|
174
|
+
modifier = Modifier.padding(bottom = Spacing.S)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
WheelPicker(
|
|
179
|
+
name = component.name,
|
|
180
|
+
data = component.data,
|
|
181
|
+
selectedData = when (component.name) {
|
|
182
|
+
"day" -> paddingNum(currentDate.day)
|
|
183
|
+
"month" -> paddingNum(currentDate.month)
|
|
184
|
+
"year" -> currentDate.year.toString()
|
|
185
|
+
"hour" -> paddingNum(currentDate.hour)
|
|
186
|
+
"minute" -> paddingNum(currentDate.minute)
|
|
187
|
+
"timeMode" -> currentDate.timeMode
|
|
188
|
+
else -> ""
|
|
189
|
+
},
|
|
190
|
+
onChange = onWheelChangeValue,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (index < dateComponents.size - 1) {
|
|
195
|
+
Spacer(modifier = Modifier.width(Spacing.M))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package vn.momo.kits.components.datetimepicker
|
|
2
|
+
|
|
3
|
+
import kotlinx.datetime.LocalDateTime
|
|
4
|
+
|
|
5
|
+
data class PickerData(
|
|
6
|
+
var day: Int,
|
|
7
|
+
var month: Int,
|
|
8
|
+
var year: Int,
|
|
9
|
+
var hour: Int = 0,
|
|
10
|
+
var minute: Int = 0,
|
|
11
|
+
var timeMode: String = ""
|
|
12
|
+
) {
|
|
13
|
+
fun toLocalDateTime(): LocalDateTime {
|
|
14
|
+
return LocalDateTime(
|
|
15
|
+
year = year,
|
|
16
|
+
monthNumber = month,
|
|
17
|
+
dayOfMonth = day,
|
|
18
|
+
hour = hour,
|
|
19
|
+
minute = minute,
|
|
20
|
+
second = 0,
|
|
21
|
+
nanosecond = 0
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
data class DateComponent(
|
|
27
|
+
val name: String,
|
|
28
|
+
val data: List<String>
|
|
29
|
+
)
|
package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
package vn.momo.kits.components.datetimepicker
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import androidx.compose.runtime.remember
|
|
5
|
+
import kotlinx.datetime.Clock
|
|
6
|
+
import kotlinx.datetime.DatePeriod
|
|
7
|
+
import kotlinx.datetime.LocalDateTime
|
|
8
|
+
import kotlinx.datetime.TimeZone
|
|
9
|
+
import kotlinx.datetime.minus
|
|
10
|
+
import kotlinx.datetime.number
|
|
11
|
+
import kotlinx.datetime.plus
|
|
12
|
+
import kotlinx.datetime.toLocalDateTime
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format a LocalDateTime object into a string
|
|
16
|
+
*/
|
|
17
|
+
fun formatLocalDateTime(dateTime: LocalDateTime, format: String = "yyyy-MM-dd HH:mm"): String {
|
|
18
|
+
return when (format) {
|
|
19
|
+
"yyyy-MM-dd HH:mm" -> "${dateTime.year}-${paddingNum(dateTime.month.number)}-${
|
|
20
|
+
paddingNum(
|
|
21
|
+
dateTime.dayOfMonth
|
|
22
|
+
)
|
|
23
|
+
} ${paddingNum(dateTime.hour)}:${paddingNum(dateTime.minute)}"
|
|
24
|
+
|
|
25
|
+
"dd-MM-yyyy" -> "${paddingNum(dateTime.dayOfMonth)}-${paddingNum(dateTime.month.number)}-${dateTime.year}"
|
|
26
|
+
"HH:mm" -> "${paddingNum(dateTime.hour)}:${paddingNum(dateTime.minute)}"
|
|
27
|
+
else -> "${dateTime.year}-${paddingNum(dateTime.month.number)}-${paddingNum(dateTime.dayOfMonth)} ${
|
|
28
|
+
paddingNum(
|
|
29
|
+
dateTime.hour
|
|
30
|
+
)
|
|
31
|
+
}:${paddingNum(dateTime.minute)}"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pad a number with a leading zero if it's less than 10
|
|
37
|
+
*/
|
|
38
|
+
fun paddingNum(num: Int): String {
|
|
39
|
+
return if (num > 9) num.toString() else "0$num"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a list of days in a month
|
|
44
|
+
*/
|
|
45
|
+
fun getDaysInMonth(
|
|
46
|
+
year: Int,
|
|
47
|
+
month: Int,
|
|
48
|
+
minDate: LocalDateTime,
|
|
49
|
+
maxDate: LocalDateTime
|
|
50
|
+
): List<String> {
|
|
51
|
+
val daysInMonth = when (month) {
|
|
52
|
+
1, 3, 5, 7, 8, 10, 12 -> 31
|
|
53
|
+
4, 6, 9, 11 -> 30
|
|
54
|
+
2 -> if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) 29 else 28
|
|
55
|
+
else -> throw IllegalArgumentException("Invalid month: $month")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var startDay = 1
|
|
59
|
+
var endDay = daysInMonth
|
|
60
|
+
|
|
61
|
+
if (year == minDate.year && month == minDate.month.number) {
|
|
62
|
+
startDay = minDate.dayOfMonth
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (year == maxDate.year && month == maxDate.month.number) {
|
|
66
|
+
endDay = maxDate.dayOfMonth
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
val days = mutableListOf<String>()
|
|
70
|
+
for (day in startDay..endDay) {
|
|
71
|
+
days.add(paddingNum(day))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return days
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get a list of months
|
|
79
|
+
*/
|
|
80
|
+
fun getMonths(
|
|
81
|
+
minDate: LocalDateTime,
|
|
82
|
+
maxDate: LocalDateTime,
|
|
83
|
+
currentYear: Int
|
|
84
|
+
): List<String> {
|
|
85
|
+
var startMonth = 1
|
|
86
|
+
var endMonth = 12
|
|
87
|
+
|
|
88
|
+
if (currentYear == minDate.year) {
|
|
89
|
+
startMonth = minDate.month.number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (currentYear == maxDate.year) {
|
|
93
|
+
endMonth = maxDate.month.number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
val months = mutableListOf<String>()
|
|
97
|
+
for (month in startMonth..endMonth) {
|
|
98
|
+
months.add(paddingNum(month))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return months
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get a list of years
|
|
106
|
+
*/
|
|
107
|
+
fun getYears(minDate: LocalDateTime, maxDate: LocalDateTime): List<String> {
|
|
108
|
+
val startYear = minDate.year
|
|
109
|
+
val endYear = maxDate.year
|
|
110
|
+
val years = mutableListOf<String>()
|
|
111
|
+
|
|
112
|
+
for (year in startYear..endYear) {
|
|
113
|
+
years.add(year.toString())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return years
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get a list of hours
|
|
121
|
+
*/
|
|
122
|
+
fun getHours(hourMode: Int): List<String> {
|
|
123
|
+
val hours = mutableListOf<String>()
|
|
124
|
+
for (i in 0 until hourMode) {
|
|
125
|
+
val hour = if (hourMode == 12) i + 1 else i
|
|
126
|
+
hours.add(paddingNum(hour))
|
|
127
|
+
}
|
|
128
|
+
return hours
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get a list of minutes
|
|
133
|
+
*/
|
|
134
|
+
fun getMinutes(interval: Int): List<String> {
|
|
135
|
+
val minutes = mutableListOf<String>()
|
|
136
|
+
var min = 0
|
|
137
|
+
while (min < 60) {
|
|
138
|
+
minutes.add(paddingNum(min))
|
|
139
|
+
min += interval
|
|
140
|
+
}
|
|
141
|
+
return minutes
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Time mode for 12-hour clock
|
|
146
|
+
*/
|
|
147
|
+
val timeMode = listOf("AM", "PM")
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get today's date
|
|
151
|
+
*/
|
|
152
|
+
fun getCurrentDateTime(): LocalDateTime {
|
|
153
|
+
return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a date that's a certain number of years before or after the current date
|
|
158
|
+
*/
|
|
159
|
+
fun createRelativeDate(years: Int): LocalDateTime {
|
|
160
|
+
val currentDateTime = getCurrentDateTime()
|
|
161
|
+
val period = DatePeriod(years = if (years < 0) -years else years)
|
|
162
|
+
|
|
163
|
+
val adjustedDate = if (years < 0) {
|
|
164
|
+
currentDateTime.date.minus(period)
|
|
165
|
+
} else {
|
|
166
|
+
currentDateTime.date.plus(period)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return LocalDateTime(
|
|
170
|
+
adjustedDate.year,
|
|
171
|
+
adjustedDate.monthNumber,
|
|
172
|
+
adjustedDate.dayOfMonth,
|
|
173
|
+
currentDateTime.hour,
|
|
174
|
+
currentDateTime.minute
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Composable
|
|
179
|
+
internal fun getDateComponents(
|
|
180
|
+
format: String,
|
|
181
|
+
currentDate: PickerData,
|
|
182
|
+
effectiveMinDate: LocalDateTime,
|
|
183
|
+
effectiveMaxDate: LocalDateTime,
|
|
184
|
+
minuteInterval: Int,
|
|
185
|
+
): List<DateComponent> {
|
|
186
|
+
val formatParts = remember(format) {
|
|
187
|
+
format.split(Regex("[^A-Za-z]+"))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
val isOnlyHour = remember(formatParts) {
|
|
191
|
+
formatParts.size == 1 && formatParts[0] == "HH"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
val dayData =
|
|
195
|
+
remember(currentDate.year, currentDate.month, effectiveMinDate, effectiveMaxDate) {
|
|
196
|
+
getDaysInMonth(currentDate.year, currentDate.month, effectiveMinDate, effectiveMaxDate)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
val monthData = remember(currentDate.year, effectiveMinDate, effectiveMaxDate) {
|
|
200
|
+
getMonths(effectiveMinDate, effectiveMaxDate, currentDate.year)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
val yearData = remember(effectiveMinDate, effectiveMaxDate) {
|
|
204
|
+
getYears(effectiveMinDate, effectiveMaxDate)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
val hourData = remember(isOnlyHour) {
|
|
208
|
+
getHours(if (isOnlyHour) 12 else 24)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
val minData = remember(minuteInterval) {
|
|
212
|
+
getMinutes(minuteInterval)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
val dateComponents =
|
|
216
|
+
remember(formatParts, dayData, monthData, yearData, hourData, minData, isOnlyHour) {
|
|
217
|
+
val components = mutableListOf<DateComponent>()
|
|
218
|
+
|
|
219
|
+
formatParts.forEach { part ->
|
|
220
|
+
when (part) {
|
|
221
|
+
"DD" -> components.add(DateComponent("day", dayData))
|
|
222
|
+
"MM" -> components.add(DateComponent("month", monthData))
|
|
223
|
+
"YYYY" -> components.add(DateComponent("year", yearData))
|
|
224
|
+
"HH" -> components.add(DateComponent("hour", hourData))
|
|
225
|
+
"mm" -> components.add(DateComponent("minute", minData))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (isOnlyHour) {
|
|
230
|
+
components.add(DateComponent("timeMode", timeMode))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
components
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return dateComponents
|
|
237
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
package vn.momo.kits.components.datetimepicker
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.border
|
|
5
|
+
import androidx.compose.foundation.gestures.animateScrollBy
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
8
|
+
import androidx.compose.foundation.layout.height
|
|
9
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
10
|
+
import androidx.compose.foundation.lazy.LazyListState
|
|
11
|
+
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
12
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
13
|
+
import androidx.compose.runtime.Composable
|
|
14
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
15
|
+
import androidx.compose.runtime.getValue
|
|
16
|
+
import androidx.compose.runtime.mutableStateOf
|
|
17
|
+
import androidx.compose.runtime.remember
|
|
18
|
+
import androidx.compose.runtime.setValue
|
|
19
|
+
import androidx.compose.runtime.snapshotFlow
|
|
20
|
+
import androidx.compose.ui.Alignment
|
|
21
|
+
import androidx.compose.ui.Modifier
|
|
22
|
+
import androidx.compose.ui.draw.alpha
|
|
23
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
24
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
25
|
+
import androidx.compose.ui.unit.Dp
|
|
26
|
+
import androidx.compose.ui.unit.dp
|
|
27
|
+
import androidx.compose.ui.zIndex
|
|
28
|
+
import kotlinx.coroutines.delay
|
|
29
|
+
import kotlinx.coroutines.flow.collect
|
|
30
|
+
import kotlinx.coroutines.flow.debounce
|
|
31
|
+
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
32
|
+
import kotlinx.coroutines.flow.onEach
|
|
33
|
+
import vn.momo.kits.components.Text
|
|
34
|
+
import vn.momo.kits.const.AppTheme
|
|
35
|
+
import vn.momo.kits.const.Colors
|
|
36
|
+
import vn.momo.kits.const.Radius
|
|
37
|
+
import vn.momo.kits.const.Typography
|
|
38
|
+
import kotlin.math.abs
|
|
39
|
+
|
|
40
|
+
private val wheelItemHeight = 42.dp
|
|
41
|
+
private val wheelPickerBorderWidth = 1.dp
|
|
42
|
+
|
|
43
|
+
@Composable
|
|
44
|
+
fun WheelPicker(
|
|
45
|
+
name: String,
|
|
46
|
+
data: List<String>,
|
|
47
|
+
selectedData: String,
|
|
48
|
+
onChange: (String, String) -> Unit,
|
|
49
|
+
modifier: Modifier = Modifier
|
|
50
|
+
) {
|
|
51
|
+
val listState = rememberLazyListState()
|
|
52
|
+
val itemHeightPx = with(LocalDensity.current) { wheelItemHeight.toPx() }
|
|
53
|
+
|
|
54
|
+
val paddedData = remember(data) {
|
|
55
|
+
listOf("", "") + data + listOf("", "")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
val initialIndex = remember(selectedData, paddedData) {
|
|
59
|
+
paddedData.indexOf(selectedData).coerceAtLeast(2)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
LaunchedEffect(initialIndex) {
|
|
63
|
+
listState.scrollToItem(initialIndex - 2)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var selectedIndex by remember { mutableStateOf(0) }
|
|
67
|
+
|
|
68
|
+
LaunchedEffect(paddedData) {
|
|
69
|
+
snapshotFlow { selectedIndex }
|
|
70
|
+
.distinctUntilChanged()
|
|
71
|
+
.debounce(50)
|
|
72
|
+
.onEach {
|
|
73
|
+
val value = paddedData.getOrNull(it)
|
|
74
|
+
if (!value.isNullOrEmpty()) {
|
|
75
|
+
onChange(name, value)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
.collect()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
LaunchedEffect(paddedData) {
|
|
82
|
+
snapshotFlow { listState.isScrollInProgress }
|
|
83
|
+
.onEach {
|
|
84
|
+
if (it) return@onEach
|
|
85
|
+
val firstItem = listState.layoutInfo.visibleItemsInfo.firstOrNull() ?: return@onEach
|
|
86
|
+
val firstItemIdx = firstItem.index
|
|
87
|
+
val firstItemOffset = firstItem.offset
|
|
88
|
+
if (firstItemOffset == 0) {
|
|
89
|
+
selectedIndex = firstItemIdx + 2
|
|
90
|
+
return@onEach
|
|
91
|
+
}
|
|
92
|
+
val isScrollUp = abs(firstItemOffset) > itemHeightPx / 2
|
|
93
|
+
val offset = if (isScrollUp) {
|
|
94
|
+
itemHeightPx - abs(firstItemOffset)
|
|
95
|
+
} else {
|
|
96
|
+
-abs(firstItemOffset)
|
|
97
|
+
}
|
|
98
|
+
delay(50)
|
|
99
|
+
listState.animateScrollBy(offset.toFloat())
|
|
100
|
+
val centeredIndex = firstItemIdx + (if (isScrollUp) 1 else 0) + 2
|
|
101
|
+
if (centeredIndex in 2 until paddedData.size - 2) {
|
|
102
|
+
selectedIndex = centeredIndex
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
.collect()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Box(
|
|
109
|
+
modifier = modifier
|
|
110
|
+
.border(
|
|
111
|
+
width = wheelPickerBorderWidth,
|
|
112
|
+
color = AppTheme.current.colors.border.default,
|
|
113
|
+
shape = RoundedCornerShape(Radius.S)
|
|
114
|
+
)
|
|
115
|
+
.background(
|
|
116
|
+
color = AppTheme.current.colors.background.surface,
|
|
117
|
+
)
|
|
118
|
+
) {
|
|
119
|
+
LazyColumn(
|
|
120
|
+
state = listState,
|
|
121
|
+
modifier = Modifier
|
|
122
|
+
.fillMaxWidth()
|
|
123
|
+
) {
|
|
124
|
+
items(paddedData.size) { index ->
|
|
125
|
+
WheelPickerItem(
|
|
126
|
+
text = paddedData[index],
|
|
127
|
+
isSelected = index == selectedIndex,
|
|
128
|
+
index = index,
|
|
129
|
+
listState = listState,
|
|
130
|
+
itemHeight = wheelItemHeight
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Box(
|
|
136
|
+
modifier = Modifier
|
|
137
|
+
.fillMaxWidth()
|
|
138
|
+
.height(wheelItemHeight)
|
|
139
|
+
.background(Colors.blue_10)
|
|
140
|
+
.align(Alignment.Center)
|
|
141
|
+
.alpha(0.2f)
|
|
142
|
+
.zIndex(-2f)
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@Composable
|
|
148
|
+
fun WheelPickerItem(
|
|
149
|
+
text: String,
|
|
150
|
+
isSelected: Boolean,
|
|
151
|
+
index: Int,
|
|
152
|
+
listState: LazyListState,
|
|
153
|
+
itemHeight: Dp
|
|
154
|
+
) {
|
|
155
|
+
Box(
|
|
156
|
+
modifier = Modifier
|
|
157
|
+
.fillMaxWidth()
|
|
158
|
+
.height(itemHeight)
|
|
159
|
+
.graphicsLayer {
|
|
160
|
+
val firstVisibleIndex = listState.firstVisibleItemIndex
|
|
161
|
+
val visibleItemOffset = listState.firstVisibleItemScrollOffset
|
|
162
|
+
|
|
163
|
+
val viewportHeight = 5 * itemHeight.value
|
|
164
|
+
val itemHeightValue = itemHeight.value
|
|
165
|
+
|
|
166
|
+
val absoluteOffset =
|
|
167
|
+
(index - firstVisibleIndex) * itemHeightValue - (visibleItemOffset / itemHeightValue)
|
|
168
|
+
val relativePosition = absoluteOffset / viewportHeight
|
|
169
|
+
|
|
170
|
+
val distance = abs(2 - relativePosition)
|
|
171
|
+
|
|
172
|
+
alpha = when {
|
|
173
|
+
distance <= 1 -> 1f
|
|
174
|
+
distance <= 2 -> 0.8f
|
|
175
|
+
distance <= 3 -> 0.4f
|
|
176
|
+
else -> 0.4f
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
contentAlignment = Alignment.Center
|
|
180
|
+
) {
|
|
181
|
+
Text(
|
|
182
|
+
text = text,
|
|
183
|
+
style = Typography.actionSBold,
|
|
184
|
+
modifier = Modifier.graphicsLayer {
|
|
185
|
+
val scale = if (isSelected) 1f else 0.87f
|
|
186
|
+
scaleX = scale
|
|
187
|
+
scaleY = scale
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|