react-native-platform-components 0.0.2
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/LICENSE +20 -0
- package/PlatformComponents.podspec +20 -0
- package/README.md +233 -0
- package/android/build.gradle +78 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/platformcomponents/DateConstraints.kt +83 -0
- package/android/src/main/java/com/platformcomponents/Helper.kt +27 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +684 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +149 -0
- package/android/src/main/java/com/platformcomponents/PCMaterialMode.kt +16 -0
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +410 -0
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +114 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +22 -0
- package/ios/PCDatePicker.h +11 -0
- package/ios/PCDatePicker.mm +248 -0
- package/ios/PCDatePickerView.swift +405 -0
- package/ios/PCSelectionMenu.h +10 -0
- package/ios/PCSelectionMenu.mm +182 -0
- package/ios/PCSelectionMenu.swift +434 -0
- package/lib/module/DatePicker.js +74 -0
- package/lib/module/DatePicker.js.map +1 -0
- package/lib/module/DatePickerNativeComponent.ts +68 -0
- package/lib/module/SelectionMenu.js +79 -0
- package/lib/module/SelectionMenu.js.map +1 -0
- package/lib/module/SelectionMenu.web.js +57 -0
- package/lib/module/SelectionMenu.web.js.map +1 -0
- package/lib/module/SelectionMenuNativeComponent.ts +106 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/sharedTypes.js +4 -0
- package/lib/module/sharedTypes.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DatePicker.d.ts +38 -0
- package/lib/typescript/src/DatePicker.d.ts.map +1 -0
- package/lib/typescript/src/DatePickerNativeComponent.d.ts +53 -0
- package/lib/typescript/src/DatePickerNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.d.ts +50 -0
- package/lib/typescript/src/SelectionMenu.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.web.d.ts +19 -0
- package/lib/typescript/src/SelectionMenu.web.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts +85 -0
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/sharedTypes.d.ts +10 -0
- package/lib/typescript/src/sharedTypes.d.ts.map +1 -0
- package/package.json +178 -0
- package/shared/PCDatePickerComponentDescriptors-custom.h +52 -0
- package/shared/PCDatePickerShadowNode-custom.cpp +1 -0
- package/shared/PCDatePickerShadowNode-custom.h +27 -0
- package/shared/PCDatePickerState-custom.h +13 -0
- package/shared/PCSelectionMenuComponentDescriptors-custom.h +25 -0
- package/shared/PCSelectionMenuShadowNode-custom.cpp +36 -0
- package/shared/PCSelectionMenuShadowNode-custom.h +46 -0
- package/src/DatePicker.tsx +146 -0
- package/src/DatePickerNativeComponent.ts +68 -0
- package/src/SelectionMenu.tsx +170 -0
- package/src/SelectionMenu.web.tsx +93 -0
- package/src/SelectionMenuNativeComponent.ts +106 -0
- package/src/index.tsx +3 -0
- package/src/sharedTypes.ts +14 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.ContextWrapper
|
|
6
|
+
import android.content.DialogInterface
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.widget.DatePicker
|
|
11
|
+
import android.widget.FrameLayout
|
|
12
|
+
import android.widget.LinearLayout
|
|
13
|
+
import android.widget.TimePicker
|
|
14
|
+
import androidx.appcompat.app.AlertDialog
|
|
15
|
+
import androidx.fragment.app.FragmentActivity
|
|
16
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
17
|
+
import com.google.android.material.datepicker.CalendarConstraints
|
|
18
|
+
import com.google.android.material.datepicker.MaterialDatePicker
|
|
19
|
+
import com.google.android.material.timepicker.MaterialTimePicker
|
|
20
|
+
import com.google.android.material.timepicker.TimeFormat
|
|
21
|
+
import java.util.Calendar
|
|
22
|
+
import java.util.Locale
|
|
23
|
+
import java.util.TimeZone
|
|
24
|
+
import kotlin.math.max
|
|
25
|
+
import kotlin.math.min
|
|
26
|
+
|
|
27
|
+
class PCDatePickerView(context: Context) : FrameLayout(context) {
|
|
28
|
+
|
|
29
|
+
// --- Public props (set by manager) ---
|
|
30
|
+
private var mode: String = "date" // "date" | "time" | "dateAndTime"
|
|
31
|
+
private var presentation: String = "modal" // "inline" | "modal" | "popover" | "sheet" | "auto" (we treat non-inline as modal-ish)
|
|
32
|
+
private var visible: String = "closed" // "open" | "closed" (only for non-inline)
|
|
33
|
+
private var locale: Locale? = null
|
|
34
|
+
private var timeZone: TimeZone = TimeZone.getDefault()
|
|
35
|
+
|
|
36
|
+
private var dateMs: Long? = null
|
|
37
|
+
private var minDateMs: Long? = null
|
|
38
|
+
private var maxDateMs: Long? = null
|
|
39
|
+
|
|
40
|
+
// --- Android config from nested `android` prop ---
|
|
41
|
+
private var androidFirstDayOfWeek: Int? = null
|
|
42
|
+
private var androidMaterialMode: PCMaterialMode = PCMaterialMode.SYSTEM // SYSTEM | M3
|
|
43
|
+
private var androidDialogTitle: String? = null
|
|
44
|
+
private var androidPositiveTitle: String? = null
|
|
45
|
+
private var androidNegativeTitle: String? = null
|
|
46
|
+
|
|
47
|
+
// --- Events (wired by manager) ---
|
|
48
|
+
var onConfirm: ((Long) -> Unit)? = null
|
|
49
|
+
var onCancel: (() -> Unit)? = null
|
|
50
|
+
|
|
51
|
+
// --- Inline UI ---
|
|
52
|
+
private var inlineContainer: LinearLayout? = null
|
|
53
|
+
private var inlineDatePicker: DatePicker? = null
|
|
54
|
+
private var inlineTimePicker: TimePicker? = null
|
|
55
|
+
private var suppressInlineCallbacks = false
|
|
56
|
+
|
|
57
|
+
// --- Modal state ---
|
|
58
|
+
private var showingModal = false
|
|
59
|
+
|
|
60
|
+
init {
|
|
61
|
+
minimumHeight = 0
|
|
62
|
+
minimumWidth = 0
|
|
63
|
+
rebuildUI()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Headless layout when not inline
|
|
67
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
68
|
+
if (!isInline()) {
|
|
69
|
+
setMeasuredDimension(0, 0)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -----------------------------
|
|
76
|
+
// Manager-facing apply* methods
|
|
77
|
+
// -----------------------------
|
|
78
|
+
|
|
79
|
+
fun applyMode(value: String?) {
|
|
80
|
+
mode = when (value) {
|
|
81
|
+
"date", "time", "dateAndTime" -> value
|
|
82
|
+
else -> "date"
|
|
83
|
+
}
|
|
84
|
+
rebuildUI()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fun applyPresentation(value: String?) {
|
|
88
|
+
presentation = value ?: "modal"
|
|
89
|
+
rebuildUI()
|
|
90
|
+
// If we were showing and presentation changed, we’ll let JS drive visible again.
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun applyVisible(value: String?) {
|
|
94
|
+
visible = when (value) {
|
|
95
|
+
"open", "closed" -> value
|
|
96
|
+
else -> "closed"
|
|
97
|
+
}
|
|
98
|
+
if (isInline()) return
|
|
99
|
+
|
|
100
|
+
if (visible == "open") presentIfNeeded() else dismissIfNeeded()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fun applyLocale(value: String?) {
|
|
104
|
+
locale =
|
|
105
|
+
try {
|
|
106
|
+
if (value.isNullOrBlank()) null else Locale.forLanguageTag(value)
|
|
107
|
+
} catch (_: Throwable) {
|
|
108
|
+
null
|
|
109
|
+
}
|
|
110
|
+
// Inline pickers don’t render strings; no-op other than storing.
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fun applyTimeZoneName(value: String?) {
|
|
114
|
+
timeZone =
|
|
115
|
+
try {
|
|
116
|
+
if (value.isNullOrBlank()) TimeZone.getDefault() else TimeZone.getTimeZone(value)
|
|
117
|
+
} catch (_: Throwable) {
|
|
118
|
+
TimeZone.getDefault()
|
|
119
|
+
}
|
|
120
|
+
// Update inline display
|
|
121
|
+
syncInlineFromState()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun applyDateMs(value: Long?) {
|
|
125
|
+
dateMs = value
|
|
126
|
+
syncInlineFromState()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fun applyMinDateMs(value: Long?) {
|
|
130
|
+
minDateMs = value
|
|
131
|
+
// clamp if needed
|
|
132
|
+
dateMs = clamp(dateMs ?: System.currentTimeMillis())
|
|
133
|
+
syncInlineFromState()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fun applyMaxDateMs(value: Long?) {
|
|
137
|
+
maxDateMs = value
|
|
138
|
+
// clamp if needed
|
|
139
|
+
dateMs = clamp(dateMs ?: System.currentTimeMillis())
|
|
140
|
+
syncInlineFromState()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* REQUIRED by your manager (nested `android` object).
|
|
145
|
+
* Only supports material: "system" | "m3"
|
|
146
|
+
*/
|
|
147
|
+
fun applyAndroidConfig(
|
|
148
|
+
firstDayOfWeek: Int?,
|
|
149
|
+
material: String?,
|
|
150
|
+
dialogTitle: String?,
|
|
151
|
+
positiveButtonTitle: String?,
|
|
152
|
+
negativeButtonTitle: String?
|
|
153
|
+
) {
|
|
154
|
+
androidFirstDayOfWeek = firstDayOfWeek
|
|
155
|
+
|
|
156
|
+
androidMaterialMode = when (material) {
|
|
157
|
+
"m3" -> PCMaterialMode.M3
|
|
158
|
+
"system", null -> PCMaterialMode.SYSTEM
|
|
159
|
+
else -> PCMaterialMode.SYSTEM
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
androidDialogTitle = dialogTitle
|
|
163
|
+
androidPositiveTitle = positiveButtonTitle
|
|
164
|
+
androidNegativeTitle = negativeButtonTitle
|
|
165
|
+
|
|
166
|
+
// Inline date picker can use firstDayOfWeek in its internal Calendar calculations
|
|
167
|
+
syncInlineFromState()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// -----------------------------
|
|
171
|
+
// UI construction
|
|
172
|
+
// -----------------------------
|
|
173
|
+
|
|
174
|
+
private fun isInline(): Boolean = presentation == "inline"
|
|
175
|
+
|
|
176
|
+
private fun rebuildUI() {
|
|
177
|
+
removeAllViews()
|
|
178
|
+
inlineContainer = null
|
|
179
|
+
inlineDatePicker = null
|
|
180
|
+
inlineTimePicker = null
|
|
181
|
+
|
|
182
|
+
if (!isInline()) {
|
|
183
|
+
requestLayout()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
val container = LinearLayout(context).apply {
|
|
188
|
+
layoutParams = LayoutParams(
|
|
189
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
190
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
191
|
+
)
|
|
192
|
+
orientation = LinearLayout.VERTICAL
|
|
193
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// date and/or time
|
|
197
|
+
if (mode == "date" || mode == "dateAndTime") {
|
|
198
|
+
val dp = DatePicker(context).apply {
|
|
199
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
200
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
201
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
202
|
+
)
|
|
203
|
+
calendarViewShown = true
|
|
204
|
+
spinnersShown = false
|
|
205
|
+
}
|
|
206
|
+
container.addView(dp)
|
|
207
|
+
inlineDatePicker = dp
|
|
208
|
+
|
|
209
|
+
dp.setOnDateChangedListener { _, year, month, day ->
|
|
210
|
+
if (suppressInlineCallbacks) return@setOnDateChangedListener
|
|
211
|
+
onInlineDateChanged(year, month, day)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (mode == "time" || mode == "dateAndTime") {
|
|
216
|
+
val tp = TimePicker(context).apply {
|
|
217
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
218
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
219
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
220
|
+
)
|
|
221
|
+
val is24 = android.text.format.DateFormat.is24HourFormat(context)
|
|
222
|
+
setIs24HourView(is24)
|
|
223
|
+
}
|
|
224
|
+
container.addView(tp)
|
|
225
|
+
inlineTimePicker = tp
|
|
226
|
+
|
|
227
|
+
tp.setOnTimeChangedListener { _, hour, minute ->
|
|
228
|
+
if (suppressInlineCallbacks) return@setOnTimeChangedListener
|
|
229
|
+
onInlineTimeChanged(hour, minute)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
addView(container)
|
|
234
|
+
inlineContainer = container
|
|
235
|
+
|
|
236
|
+
syncInlineFromState()
|
|
237
|
+
requestLayout()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun syncInlineFromState() {
|
|
241
|
+
if (!isInline()) return
|
|
242
|
+
|
|
243
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
244
|
+
dateMs = ts
|
|
245
|
+
|
|
246
|
+
val cal = calendarFor(ts)
|
|
247
|
+
|
|
248
|
+
suppressInlineCallbacks = true
|
|
249
|
+
try {
|
|
250
|
+
inlineDatePicker?.let { dp ->
|
|
251
|
+
// Apply min/max bounds on the widget itself where possible
|
|
252
|
+
minDateMs?.let { dp.minDate = it }
|
|
253
|
+
maxDateMs?.let { dp.maxDate = it }
|
|
254
|
+
|
|
255
|
+
val y = cal.get(Calendar.YEAR)
|
|
256
|
+
val m = cal.get(Calendar.MONTH)
|
|
257
|
+
val d = cal.get(Calendar.DAY_OF_MONTH)
|
|
258
|
+
dp.updateDate(y, m, d)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
inlineTimePicker?.let { tp ->
|
|
262
|
+
val hour = cal.get(Calendar.HOUR_OF_DAY)
|
|
263
|
+
val minute = cal.get(Calendar.MINUTE)
|
|
264
|
+
if (Build.VERSION.SDK_INT >= 23) {
|
|
265
|
+
if (tp.hour != hour) tp.hour = hour
|
|
266
|
+
if (tp.minute != minute) tp.minute = minute
|
|
267
|
+
} else {
|
|
268
|
+
@Suppress("DEPRECATION")
|
|
269
|
+
if (tp.currentHour != hour) tp.currentHour = hour
|
|
270
|
+
@Suppress("DEPRECATION")
|
|
271
|
+
if (tp.currentMinute != minute) tp.currentMinute = minute
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
suppressInlineCallbacks = false
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// -----------------------------
|
|
280
|
+
// Inline change handlers
|
|
281
|
+
// -----------------------------
|
|
282
|
+
|
|
283
|
+
private fun onInlineDateChanged(year: Int, month: Int, day: Int) {
|
|
284
|
+
val base = clamp(dateMs ?: System.currentTimeMillis())
|
|
285
|
+
val cal = calendarFor(base)
|
|
286
|
+
|
|
287
|
+
cal.set(Calendar.YEAR, year)
|
|
288
|
+
cal.set(Calendar.MONTH, month)
|
|
289
|
+
cal.set(Calendar.DAY_OF_MONTH, day)
|
|
290
|
+
cal.set(Calendar.SECOND, 0)
|
|
291
|
+
cal.set(Calendar.MILLISECOND, 0)
|
|
292
|
+
|
|
293
|
+
dateMs = clamp(cal.timeInMillis)
|
|
294
|
+
// Inline = no confirm/cancel; treat as immediate confirm (same as your old behavior)
|
|
295
|
+
onConfirm?.invoke(dateMs!!)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private fun onInlineTimeChanged(hour: Int, minute: Int) {
|
|
299
|
+
val base = clamp(dateMs ?: System.currentTimeMillis())
|
|
300
|
+
val cal = calendarFor(base)
|
|
301
|
+
|
|
302
|
+
cal.set(Calendar.HOUR_OF_DAY, hour)
|
|
303
|
+
cal.set(Calendar.MINUTE, minute)
|
|
304
|
+
cal.set(Calendar.SECOND, 0)
|
|
305
|
+
cal.set(Calendar.MILLISECOND, 0)
|
|
306
|
+
|
|
307
|
+
dateMs = clamp(cal.timeInMillis)
|
|
308
|
+
onConfirm?.invoke(dateMs!!)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// -----------------------------
|
|
312
|
+
// Headless modal presentation
|
|
313
|
+
// -----------------------------
|
|
314
|
+
|
|
315
|
+
private fun presentIfNeeded() {
|
|
316
|
+
if (showingModal) return
|
|
317
|
+
val act = findFragmentActivity() ?: run {
|
|
318
|
+
// If we cannot present, treat as cancel
|
|
319
|
+
onCancel?.invoke()
|
|
320
|
+
showingModal = false
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
showingModal = true
|
|
325
|
+
|
|
326
|
+
when (mode) {
|
|
327
|
+
"time" -> presentTime(act)
|
|
328
|
+
"dateAndTime" -> presentDateThenTime(act)
|
|
329
|
+
else -> presentDate(act)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private fun dismissIfNeeded() {
|
|
334
|
+
// We don’t retain dialog instances here; JS will close by dismissing itself or user action.
|
|
335
|
+
// This keeps parity with Fabric headless patterns.
|
|
336
|
+
showingModal = false
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private fun presentDate(act: FragmentActivity) {
|
|
340
|
+
if (androidMaterialMode == PCMaterialMode.M3) presentM3Date(act) else presentSystemDate(act)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private fun presentTime(act: FragmentActivity) {
|
|
344
|
+
if (androidMaterialMode == PCMaterialMode.M3) presentM3Time(act) else presentSystemTime(act)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private fun presentDateThenTime(act: FragmentActivity) {
|
|
348
|
+
if (androidMaterialMode == PCMaterialMode.M3) {
|
|
349
|
+
presentM3DateThenTime(act)
|
|
350
|
+
} else {
|
|
351
|
+
presentSystemDateThenTime(act)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// -----------------------------
|
|
356
|
+
// SYSTEM dialogs (AlertDialog host for full control)
|
|
357
|
+
// -----------------------------
|
|
358
|
+
|
|
359
|
+
private fun presentSystemDate(act: FragmentActivity) {
|
|
360
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
361
|
+
val cal = calendarFor(ts)
|
|
362
|
+
|
|
363
|
+
val picker = DatePicker(act).apply {
|
|
364
|
+
calendarViewShown = true
|
|
365
|
+
spinnersShown = false
|
|
366
|
+
minDateMs?.let { minDate = it }
|
|
367
|
+
maxDateMs?.let { maxDate = it }
|
|
368
|
+
updateDate(
|
|
369
|
+
cal.get(Calendar.YEAR),
|
|
370
|
+
cal.get(Calendar.MONTH),
|
|
371
|
+
cal.get(Calendar.DAY_OF_MONTH)
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
val dlg = AlertDialog.Builder(act)
|
|
376
|
+
.setTitle(androidDialogTitle ?: "")
|
|
377
|
+
.setView(picker)
|
|
378
|
+
.setPositiveButton(androidPositiveTitle ?: "OK") { _, _ ->
|
|
379
|
+
val c = calendarFor(ts)
|
|
380
|
+
c.set(Calendar.YEAR, picker.year)
|
|
381
|
+
c.set(Calendar.MONTH, picker.month)
|
|
382
|
+
c.set(Calendar.DAY_OF_MONTH, picker.dayOfMonth)
|
|
383
|
+
c.set(Calendar.SECOND, 0)
|
|
384
|
+
c.set(Calendar.MILLISECOND, 0)
|
|
385
|
+
|
|
386
|
+
dateMs = clamp(c.timeInMillis)
|
|
387
|
+
onConfirm?.invoke(dateMs!!)
|
|
388
|
+
onCancelOrClose()
|
|
389
|
+
}
|
|
390
|
+
.setNegativeButton(androidNegativeTitle ?: "Cancel") { _, _ ->
|
|
391
|
+
onCancel?.invoke()
|
|
392
|
+
onCancelOrClose()
|
|
393
|
+
}
|
|
394
|
+
.setOnCancelListener {
|
|
395
|
+
onCancel?.invoke()
|
|
396
|
+
onCancelOrClose()
|
|
397
|
+
}
|
|
398
|
+
.create()
|
|
399
|
+
|
|
400
|
+
dlg.show()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private fun presentSystemTime(act: FragmentActivity) {
|
|
404
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
405
|
+
val cal = calendarFor(ts)
|
|
406
|
+
|
|
407
|
+
val picker = TimePicker(act).apply {
|
|
408
|
+
val is24 = android.text.format.DateFormat.is24HourFormat(act)
|
|
409
|
+
setIs24HourView(is24)
|
|
410
|
+
|
|
411
|
+
val hour = cal.get(Calendar.HOUR_OF_DAY)
|
|
412
|
+
val minute = cal.get(Calendar.MINUTE)
|
|
413
|
+
|
|
414
|
+
if (Build.VERSION.SDK_INT >= 23) {
|
|
415
|
+
this.hour = hour
|
|
416
|
+
this.minute = minute
|
|
417
|
+
} else {
|
|
418
|
+
@Suppress("DEPRECATION") this.currentHour = hour
|
|
419
|
+
@Suppress("DEPRECATION") this.currentMinute = minute
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
val dlg = AlertDialog.Builder(act)
|
|
424
|
+
.setTitle(androidDialogTitle ?: "")
|
|
425
|
+
.setView(picker)
|
|
426
|
+
.setPositiveButton(androidPositiveTitle ?: "OK") { _, _ ->
|
|
427
|
+
val h: Int
|
|
428
|
+
val m: Int
|
|
429
|
+
if (Build.VERSION.SDK_INT >= 23) {
|
|
430
|
+
h = picker.hour
|
|
431
|
+
m = picker.minute
|
|
432
|
+
} else {
|
|
433
|
+
@Suppress("DEPRECATION") h = picker.currentHour
|
|
434
|
+
@Suppress("DEPRECATION") m = picker.currentMinute
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
val c = calendarFor(ts)
|
|
438
|
+
c.set(Calendar.HOUR_OF_DAY, h)
|
|
439
|
+
c.set(Calendar.MINUTE, m)
|
|
440
|
+
c.set(Calendar.SECOND, 0)
|
|
441
|
+
c.set(Calendar.MILLISECOND, 0)
|
|
442
|
+
|
|
443
|
+
dateMs = clamp(c.timeInMillis)
|
|
444
|
+
onConfirm?.invoke(dateMs!!)
|
|
445
|
+
onCancelOrClose()
|
|
446
|
+
}
|
|
447
|
+
.setNegativeButton(androidNegativeTitle ?: "Cancel") { _, _ ->
|
|
448
|
+
onCancel?.invoke()
|
|
449
|
+
onCancelOrClose()
|
|
450
|
+
}
|
|
451
|
+
.setOnCancelListener {
|
|
452
|
+
onCancel?.invoke()
|
|
453
|
+
onCancelOrClose()
|
|
454
|
+
}
|
|
455
|
+
.create()
|
|
456
|
+
|
|
457
|
+
dlg.show()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private fun presentSystemDateThenTime(act: FragmentActivity) {
|
|
461
|
+
// date first
|
|
462
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
463
|
+
val cal = calendarFor(ts)
|
|
464
|
+
|
|
465
|
+
val picker = DatePicker(act).apply {
|
|
466
|
+
calendarViewShown = true
|
|
467
|
+
spinnersShown = false
|
|
468
|
+
minDateMs?.let { minDate = it }
|
|
469
|
+
maxDateMs?.let { maxDate = it }
|
|
470
|
+
updateDate(
|
|
471
|
+
cal.get(Calendar.YEAR),
|
|
472
|
+
cal.get(Calendar.MONTH),
|
|
473
|
+
cal.get(Calendar.DAY_OF_MONTH)
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
val dlg = AlertDialog.Builder(act)
|
|
478
|
+
.setTitle(androidDialogTitle ?: "")
|
|
479
|
+
.setView(picker)
|
|
480
|
+
.setPositiveButton(androidPositiveTitle ?: "Next") { _, _ ->
|
|
481
|
+
val c = calendarFor(ts)
|
|
482
|
+
c.set(Calendar.YEAR, picker.year)
|
|
483
|
+
c.set(Calendar.MONTH, picker.month)
|
|
484
|
+
c.set(Calendar.DAY_OF_MONTH, picker.dayOfMonth)
|
|
485
|
+
c.set(Calendar.SECOND, 0)
|
|
486
|
+
c.set(Calendar.MILLISECOND, 0)
|
|
487
|
+
|
|
488
|
+
dateMs = clamp(c.timeInMillis)
|
|
489
|
+
// then time dialog (same mode)
|
|
490
|
+
presentSystemTime(act)
|
|
491
|
+
}
|
|
492
|
+
.setNegativeButton(androidNegativeTitle ?: "Cancel") { _, _ ->
|
|
493
|
+
onCancel?.invoke()
|
|
494
|
+
onCancelOrClose()
|
|
495
|
+
}
|
|
496
|
+
.setOnCancelListener {
|
|
497
|
+
onCancel?.invoke()
|
|
498
|
+
onCancelOrClose()
|
|
499
|
+
}
|
|
500
|
+
.create()
|
|
501
|
+
|
|
502
|
+
dlg.show()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// -----------------------------
|
|
506
|
+
// M3 dialogs
|
|
507
|
+
// -----------------------------
|
|
508
|
+
|
|
509
|
+
private fun presentM3Date(act: FragmentActivity) {
|
|
510
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
511
|
+
|
|
512
|
+
val builder = MaterialDatePicker.Builder.datePicker()
|
|
513
|
+
.setSelection(ts)
|
|
514
|
+
|
|
515
|
+
androidDialogTitle?.let { builder.setTitleText(it) }
|
|
516
|
+
androidPositiveTitle?.let { builder.setPositiveButtonText(it) }
|
|
517
|
+
androidNegativeTitle?.let { builder.setNegativeButtonText(it) }
|
|
518
|
+
|
|
519
|
+
val constraints = buildM3CalendarConstraints()
|
|
520
|
+
if (constraints != null) builder.setCalendarConstraints(constraints)
|
|
521
|
+
|
|
522
|
+
val picker = builder.build()
|
|
523
|
+
|
|
524
|
+
picker.addOnPositiveButtonClickListener { selection ->
|
|
525
|
+
val sel = (selection ?: ts)
|
|
526
|
+
// Selection is date-based; merge with existing time-of-day
|
|
527
|
+
val base = calendarFor(ts)
|
|
528
|
+
val selUtc = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { timeInMillis = sel }
|
|
529
|
+
base.set(Calendar.YEAR, selUtc.get(Calendar.YEAR))
|
|
530
|
+
base.set(Calendar.MONTH, selUtc.get(Calendar.MONTH))
|
|
531
|
+
base.set(Calendar.DAY_OF_MONTH, selUtc.get(Calendar.DAY_OF_MONTH))
|
|
532
|
+
base.set(Calendar.SECOND, 0)
|
|
533
|
+
base.set(Calendar.MILLISECOND, 0)
|
|
534
|
+
|
|
535
|
+
dateMs = clamp(base.timeInMillis)
|
|
536
|
+
onConfirm?.invoke(dateMs!!)
|
|
537
|
+
onCancelOrClose()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
picker.addOnDismissListener {
|
|
541
|
+
// If dismissed without confirm, treat as cancel
|
|
542
|
+
if (showingModal) {
|
|
543
|
+
onCancel?.invoke()
|
|
544
|
+
onCancelOrClose()
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
picker.show(act.supportFragmentManager, "PCDatePicker_M3_DATE")
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private fun presentM3Time(act: FragmentActivity) {
|
|
552
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
553
|
+
val cal = calendarFor(ts)
|
|
554
|
+
|
|
555
|
+
val is24 = android.text.format.DateFormat.is24HourFormat(act)
|
|
556
|
+
val builder = MaterialTimePicker.Builder()
|
|
557
|
+
.setTimeFormat(if (is24) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H)
|
|
558
|
+
.setHour(cal.get(Calendar.HOUR_OF_DAY))
|
|
559
|
+
.setMinute(cal.get(Calendar.MINUTE))
|
|
560
|
+
|
|
561
|
+
androidDialogTitle?.let { builder.setTitleText(it) }
|
|
562
|
+
// These exist in recent Material; if you’re on an older one, you’ll get compile errors.
|
|
563
|
+
androidPositiveTitle?.let { builder.setPositiveButtonText(it) }
|
|
564
|
+
androidNegativeTitle?.let { builder.setNegativeButtonText(it) }
|
|
565
|
+
|
|
566
|
+
val picker = builder.build()
|
|
567
|
+
|
|
568
|
+
picker.addOnPositiveButtonClickListener {
|
|
569
|
+
val c = calendarFor(ts)
|
|
570
|
+
c.set(Calendar.HOUR_OF_DAY, picker.hour)
|
|
571
|
+
c.set(Calendar.MINUTE, picker.minute)
|
|
572
|
+
c.set(Calendar.SECOND, 0)
|
|
573
|
+
c.set(Calendar.MILLISECOND, 0)
|
|
574
|
+
|
|
575
|
+
dateMs = clamp(c.timeInMillis)
|
|
576
|
+
onConfirm?.invoke(dateMs!!)
|
|
577
|
+
onCancelOrClose()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
picker.addOnDismissListener {
|
|
581
|
+
if (showingModal) {
|
|
582
|
+
onCancel?.invoke()
|
|
583
|
+
onCancelOrClose()
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
picker.show(act.supportFragmentManager, "PCDatePicker_M3_TIME")
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private fun presentM3DateThenTime(act: FragmentActivity) {
|
|
591
|
+
val ts = clamp(dateMs ?: System.currentTimeMillis())
|
|
592
|
+
|
|
593
|
+
val builder = MaterialDatePicker.Builder.datePicker()
|
|
594
|
+
.setSelection(ts)
|
|
595
|
+
|
|
596
|
+
androidDialogTitle?.let { builder.setTitleText(it) }
|
|
597
|
+
androidPositiveTitle?.let { builder.setPositiveButtonText(it) }
|
|
598
|
+
androidNegativeTitle?.let { builder.setNegativeButtonText(it) }
|
|
599
|
+
|
|
600
|
+
val constraints = buildM3CalendarConstraints()
|
|
601
|
+
if (constraints != null) builder.setCalendarConstraints(constraints)
|
|
602
|
+
|
|
603
|
+
val picker = builder.build()
|
|
604
|
+
|
|
605
|
+
picker.addOnPositiveButtonClickListener { selection ->
|
|
606
|
+
val sel = (selection ?: ts)
|
|
607
|
+
val base = calendarFor(ts)
|
|
608
|
+
val selUtc = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { timeInMillis = sel }
|
|
609
|
+
base.set(Calendar.YEAR, selUtc.get(Calendar.YEAR))
|
|
610
|
+
base.set(Calendar.MONTH, selUtc.get(Calendar.MONTH))
|
|
611
|
+
base.set(Calendar.DAY_OF_MONTH, selUtc.get(Calendar.DAY_OF_MONTH))
|
|
612
|
+
base.set(Calendar.SECOND, 0)
|
|
613
|
+
base.set(Calendar.MILLISECOND, 0)
|
|
614
|
+
|
|
615
|
+
dateMs = clamp(base.timeInMillis)
|
|
616
|
+
// then time
|
|
617
|
+
presentM3Time(act)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
picker.addOnDismissListener {
|
|
621
|
+
if (showingModal) {
|
|
622
|
+
onCancel?.invoke()
|
|
623
|
+
onCancelOrClose()
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
picker.show(act.supportFragmentManager, "PCDatePicker_M3_DATE_THEN_TIME")
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private fun buildM3CalendarConstraints(): CalendarConstraints? {
|
|
631
|
+
val min = minDateMs
|
|
632
|
+
val max = maxDateMs
|
|
633
|
+
if (min == null && max == null) return null
|
|
634
|
+
|
|
635
|
+
val b = CalendarConstraints.Builder()
|
|
636
|
+
min?.let { b.setStart(it) }
|
|
637
|
+
max?.let { b.setEnd(it) }
|
|
638
|
+
return b.build()
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// -----------------------------
|
|
642
|
+
// Utility
|
|
643
|
+
// -----------------------------
|
|
644
|
+
|
|
645
|
+
private fun onCancelOrClose() {
|
|
646
|
+
showingModal = false
|
|
647
|
+
// JS typically sets visible="closed" in response to onCancel/onConfirm,
|
|
648
|
+
// but we defensively mark ourselves closed.
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private fun clamp(valueMs: Long): Long {
|
|
652
|
+
var v = valueMs
|
|
653
|
+
minDateMs?.let { v = max(v, it) }
|
|
654
|
+
maxDateMs?.let { v = min(v, it) }
|
|
655
|
+
return v
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private fun calendarFor(ts: Long): Calendar {
|
|
659
|
+
val cal = Calendar.getInstance(timeZone, locale ?: Locale.getDefault())
|
|
660
|
+
androidFirstDayOfWeek?.let { cal.firstDayOfWeek = it }
|
|
661
|
+
cal.timeInMillis = ts
|
|
662
|
+
return cal
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private fun findFragmentActivity(): FragmentActivity? {
|
|
666
|
+
val trc = context as? ThemedReactContext
|
|
667
|
+
val a1 = trc?.currentActivity
|
|
668
|
+
if (a1 is FragmentActivity) return a1
|
|
669
|
+
|
|
670
|
+
var c: Context? = context
|
|
671
|
+
while (c is ContextWrapper) {
|
|
672
|
+
if (c is FragmentActivity) return c
|
|
673
|
+
val base = (c as ContextWrapper).baseContext
|
|
674
|
+
if (base == c) break
|
|
675
|
+
c = base
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
val a2 = (context as? Activity)
|
|
679
|
+
return a2 as? FragmentActivity
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Minimal enum for material mode
|
|
683
|
+
private enum class PCMaterialMode { SYSTEM, M3 }
|
|
684
|
+
}
|