craft-native 0.0.251 → 0.0.262
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/dist/android/src/index.d.ts +4 -2
- package/dist/android/src/index.js +69 -5
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftHealthServices.kt.template +132 -0
- package/dist/android/templates/CraftHealthServicesService.kt.template +217 -0
- package/dist/android/templates/CraftHealthServicesStub.kt.template +16 -0
- package/dist/android/templates/CraftNativeHealth.kt.template +115 -0
- package/dist/android/templates/CraftNativeHealthStub.kt.template +1 -0
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +0 -1
- package/dist/android/templates/CraftNativeServices.kt.template +31 -8
- package/dist/android/templates/CraftNativeShell.kt.template +73 -23
- package/dist/android/templates/MainActivityNative.kt.template +217 -28
- package/dist/android/templates/build.gradle.kts.app.template +5 -0
- package/dist/api/mobile.d.ts +9 -1
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +19 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +36 -4
- package/dist/ios/templates/CraftHealth.swift +38 -0
- package/dist/ios/templates/CraftHybrid.swift +38 -8
- package/dist/ios/templates/CraftNativeActions.swift +1 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +34 -20
- package/dist/ios/templates/CraftNativeScreen.swift +308 -28
- package/dist/ios/templates/testing/DriverTests.swift.template +21 -1
- package/dist/mobile-media-testing.d.ts +2 -0
- package/dist/mobile-testing.js +19 -10
- package/dist/mobile.js +1 -1
- package/dist/testing/media-cache/media-cache.kt +47 -6
- package/package.json +1 -1
|
@@ -226,6 +226,8 @@ private data class CraftNativeLayoutStyle(
|
|
|
226
226
|
val bottom: Int? = null,
|
|
227
227
|
val left: Int? = null,
|
|
228
228
|
val alignSelf: String? = null,
|
|
229
|
+
/** CSS/Yoga order; Kotlin's stable sort preserves source order for ties. */
|
|
230
|
+
val order: Int = 0,
|
|
229
231
|
val flexGrow: Double = 0.0,
|
|
230
232
|
val flexShrink: Double? = null,
|
|
231
233
|
val flexBasis: CraftNativeLength? = null,
|
|
@@ -252,6 +254,7 @@ private data class CraftNativeLayoutStyle(
|
|
|
252
254
|
position = craftNativeOptionalString(raw.opt("position")) ?: "relative",
|
|
253
255
|
top = number("top"), right = number("right"), bottom = number("bottom"), left = number("left"),
|
|
254
256
|
alignSelf = craftNativeOptionalString(raw.opt("alignSelf"))?.takeUnless { it == "auto" },
|
|
257
|
+
order = decimal("order")?.toInt() ?: 0,
|
|
255
258
|
flexGrow = decimal("flexGrow") ?: flex?.coerceAtLeast(0.0) ?: 0.0,
|
|
256
259
|
flexShrink = decimal("flexShrink") ?: flex?.let { if (it != 0.0) 1.0 else 0.0 },
|
|
257
260
|
flexBasis = length("flexBasis")
|
|
@@ -759,6 +762,42 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
759
762
|
private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
|
|
760
763
|
private var applyingPadding = false
|
|
761
764
|
private var safeAreaEdges = setOf("top", "right", "bottom", "left")
|
|
765
|
+
private var shieldedModalTouch = false
|
|
766
|
+
private var dismissibleModalTouch: CraftNativeModalView? = null
|
|
767
|
+
|
|
768
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
769
|
+
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
770
|
+
val x = event.x.roundToInt()
|
|
771
|
+
val y = event.y.roundToInt()
|
|
772
|
+
dismissibleModalTouch = (childCount - 1 downTo 0).asSequence()
|
|
773
|
+
.map { getChildAt(it) }
|
|
774
|
+
.filterIsInstance<CraftNativeModalView>()
|
|
775
|
+
.firstOrNull { child ->
|
|
776
|
+
child.dismissOnOutsideTap && child.visibility == View.VISIBLE && child.alpha > 0.01f
|
|
777
|
+
&& (x < child.left || x >= child.right || y < child.top || y >= child.bottom)
|
|
778
|
+
}
|
|
779
|
+
shieldedModalTouch = (childCount - 1 downTo 0).any { index ->
|
|
780
|
+
val child = getChildAt(index)
|
|
781
|
+
child is CraftNativeModalView && !child.blocksBackground && child.visibility == View.VISIBLE
|
|
782
|
+
&& (x < child.left || x >= child.right || y < child.top || y >= child.bottom)
|
|
783
|
+
&& child.hasBlockingModalBelow()
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
dismissibleModalTouch?.let { modal ->
|
|
787
|
+
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
|
788
|
+
modal.requestClose(reason = "backdrop")
|
|
789
|
+
dismissibleModalTouch = null
|
|
790
|
+
} else if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
|
791
|
+
dismissibleModalTouch = null
|
|
792
|
+
}
|
|
793
|
+
return true
|
|
794
|
+
}
|
|
795
|
+
if (shieldedModalTouch) {
|
|
796
|
+
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) shieldedModalTouch = false
|
|
797
|
+
return true
|
|
798
|
+
}
|
|
799
|
+
return super.dispatchTouchEvent(event)
|
|
800
|
+
}
|
|
762
801
|
|
|
763
802
|
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
|
764
803
|
if (!applyingPadding) authoredPadding = intArrayOf(left, top, right, bottom)
|
|
@@ -991,6 +1030,11 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
991
1030
|
invalidateMeasurementAncestors(parent)
|
|
992
1031
|
}
|
|
993
1032
|
|
|
1033
|
+
/** Flex/grid order with Kotlin's stable sort retaining source order for ties. */
|
|
1034
|
+
private fun orderedChildIndices(): List<Int> = (0 until childCount).sortedWith(
|
|
1035
|
+
compareBy<Int> { styles[getChildAt(it)]?.order ?: 0 }.thenBy { it },
|
|
1036
|
+
)
|
|
1037
|
+
|
|
994
1038
|
private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
|
|
995
1039
|
private data class Line(val entries: MutableList<Entry> = mutableListOf(), var main: Float = 0f, var cross: Float = 0f)
|
|
996
1040
|
private data class GridLayout(val rows: List<List<Entry>>, val absolute: List<Entry>, val cellWidth: Float, val rowHeights: List<Float>)
|
|
@@ -1002,7 +1046,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1002
1046
|
val lines = mutableListOf<Line>()
|
|
1003
1047
|
val absolute = mutableListOf<Entry>()
|
|
1004
1048
|
val pending = mutableListOf<Entry>()
|
|
1005
|
-
for (index in
|
|
1049
|
+
for (index in orderedChildIndices()) {
|
|
1006
1050
|
val child = getChildAt(index)
|
|
1007
1051
|
if (child.visibility == GONE) continue
|
|
1008
1052
|
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
@@ -1010,13 +1054,25 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1010
1054
|
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
1011
1055
|
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
1012
1056
|
if (child is CraftNativeModalView) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1057
|
+
if (child.blocksBackground) {
|
|
1058
|
+
val modalWidth = max(0, width - paddingLeft - paddingRight)
|
|
1059
|
+
val modalHeight = max(0, height - paddingTop - paddingBottom)
|
|
1060
|
+
child.measure(
|
|
1061
|
+
View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
|
|
1062
|
+
View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
|
|
1063
|
+
)
|
|
1064
|
+
absolute += Entry(child, style, modalWidth, modalHeight)
|
|
1065
|
+
} else {
|
|
1066
|
+
// A non-modal popup is still promoted to the screen root,
|
|
1067
|
+
// but its authored absolute style controls its viewport
|
|
1068
|
+
// frame instead of turning it into a full-screen child.
|
|
1069
|
+
// The root content box is the same screenViewport origin
|
|
1070
|
+
// returned by View.measure; safe-area padding is not
|
|
1071
|
+
// injected a second time here.
|
|
1072
|
+
val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
|
|
1073
|
+
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
1074
|
+
absolute += Entry(child, style, childWidth, childHeight)
|
|
1075
|
+
}
|
|
1020
1076
|
continue
|
|
1021
1077
|
}
|
|
1022
1078
|
val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
|
|
@@ -1101,7 +1157,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1101
1157
|
val cellWidth = max(0f, (availableWidth.toFloat() - max(0, gridColumns - 1) * gap) / max(1, gridColumns).toFloat())
|
|
1102
1158
|
val rows = mutableListOf<MutableList<Entry>>()
|
|
1103
1159
|
val absolute = mutableListOf<Entry>()
|
|
1104
|
-
for (index in
|
|
1160
|
+
for (index in orderedChildIndices()) {
|
|
1105
1161
|
val child = getChildAt(index)
|
|
1106
1162
|
if (child.visibility == GONE) continue
|
|
1107
1163
|
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
@@ -1109,13 +1165,19 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1109
1165
|
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
1110
1166
|
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
1111
1167
|
if (child is CraftNativeModalView) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1168
|
+
if (child.blocksBackground) {
|
|
1169
|
+
val modalWidth = max(0, width - paddingLeft - paddingRight)
|
|
1170
|
+
val modalHeight = max(0, height - paddingTop - paddingBottom)
|
|
1171
|
+
child.measure(
|
|
1172
|
+
View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
|
|
1173
|
+
View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
|
|
1174
|
+
)
|
|
1175
|
+
absolute += Entry(child, style, modalWidth, modalHeight)
|
|
1176
|
+
} else {
|
|
1177
|
+
val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
|
|
1178
|
+
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
1179
|
+
absolute += Entry(child, style, childWidth, childHeight)
|
|
1180
|
+
}
|
|
1119
1181
|
continue
|
|
1120
1182
|
}
|
|
1121
1183
|
val childWidth = measureChildWidth(child, style, cellWidth.roundToInt(), horizontalMargins)
|
|
@@ -1136,9 +1198,9 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1136
1198
|
val rowHeights = if (gridRows != null) {
|
|
1137
1199
|
val count = max(gridRows ?: 0, naturalRowHeights.size)
|
|
1138
1200
|
while (rows.size < count) rows.add(mutableListOf())
|
|
1139
|
-
val available = max(0f, height - paddingTop - paddingBottom - max(0, count - 1) * rowGap)
|
|
1201
|
+
val available = max(0f, (height - paddingTop - paddingBottom).toFloat() - max(0, count - 1) * rowGap)
|
|
1140
1202
|
val equal = available / max(1, count).toFloat()
|
|
1141
|
-
(naturalRowHeights + List(count - naturalRowHeights.size) {
|
|
1203
|
+
(naturalRowHeights + List(count - naturalRowHeights.size) { 0f })
|
|
1142
1204
|
.map { max(it, equal) }
|
|
1143
1205
|
} else naturalRowHeights
|
|
1144
1206
|
return GridLayout(rows, absolute, cellWidth, rowHeights)
|
|
@@ -1149,7 +1211,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1149
1211
|
val columnWidths = IntArray(columns)
|
|
1150
1212
|
val rowHeights = mutableListOf<Int>()
|
|
1151
1213
|
var flowIndex = 0
|
|
1152
|
-
for (index in
|
|
1214
|
+
for (index in orderedChildIndices()) {
|
|
1153
1215
|
val child = getChildAt(index)
|
|
1154
1216
|
if (child.visibility == GONE) continue
|
|
1155
1217
|
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
@@ -1438,12 +1500,12 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1438
1500
|
put(base, 26, node.padding[3].toFloat())
|
|
1439
1501
|
put(base, 27, node.padding[0].toFloat())
|
|
1440
1502
|
putEnum(base, 28, if (node.grid) 1 else 0)
|
|
1441
|
-
|
|
1503
|
+
putEnum(base, 29, node.gridColumns)
|
|
1442
1504
|
put(base, 30, node.rowGap)
|
|
1443
1505
|
put(base, 31, node.columnGap)
|
|
1444
1506
|
if (fields >= 33) putEnum(base, 32, (if (node.reverse) 1 else 0) or (if (node.wrapReverse) 2 else 0))
|
|
1445
1507
|
if (autoRowsAware) put(base, 33, sharedLength(node.gridAutoRows, contentHeight))
|
|
1446
|
-
if (explicitRowsAware)
|
|
1508
|
+
if (explicitRowsAware) putEnum(base, 34, node.gridRows ?: 0)
|
|
1447
1509
|
val firstChild = cursor
|
|
1448
1510
|
node.children.forEach { child -> write(child, contentWidth, contentHeight, false, node.direction) }
|
|
1449
1511
|
treeMeta[index * 2] = firstChild.toFloat()
|
|
@@ -1527,7 +1589,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1527
1589
|
put(base, 26, padding[3].toFloat())
|
|
1528
1590
|
put(base, 27, padding[0].toFloat())
|
|
1529
1591
|
putEnum(base, 28, 0)
|
|
1530
|
-
|
|
1592
|
+
putEnum(base, 29, 1)
|
|
1531
1593
|
put(base, 30, if (root) rowGapValue else 0f)
|
|
1532
1594
|
put(base, 31, if (root) columnGapValue else 0f)
|
|
1533
1595
|
}
|
|
@@ -1796,7 +1858,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1796
1858
|
if (absolute.isEmpty() && lines.isNotEmpty() && (lines.size == 1 || wrap)) {
|
|
1797
1859
|
val sharedEntries = if (wrap) lines.flatMap { it.entries } else lines[0].entries
|
|
1798
1860
|
val sharedTree = sharedTreeFrames(sharedEntries, width, height)
|
|
1799
|
-
if (sharedTree != null && (sharedTree.nodes.size > sharedEntries.size + 1 || grid)) {
|
|
1861
|
+
if (sharedTree != null && (sharedTree.nodes.size > sharedEntries.size + 1 || grid || reverse || wrapReverse || gridAutoRows != null || gridRows != null)) {
|
|
1800
1862
|
for (index in 1 until sharedTree.nodes.size) {
|
|
1801
1863
|
val view = sharedTree.nodes[index].view ?: continue
|
|
1802
1864
|
val offset = index * 4
|
|
@@ -2560,6 +2622,8 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
2560
2622
|
private var animationGeneration = 0
|
|
2561
2623
|
var focusToRestore: View? = null
|
|
2562
2624
|
var onRequestClose: ((String) -> Unit)? = null
|
|
2625
|
+
var viewportStyle = JSONObject()
|
|
2626
|
+
var declarationPath = ""
|
|
2563
2627
|
|
|
2564
2628
|
var transparent: Boolean = false
|
|
2565
2629
|
set(value) {
|
|
@@ -2567,6 +2631,18 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
2567
2631
|
blocker.setBackgroundColor(if (value) Color.TRANSPARENT else Color.argb(82, 0, 0, 0))
|
|
2568
2632
|
}
|
|
2569
2633
|
|
|
2634
|
+
/** Existing Modal nodes block by default; non-modal popups let outside taps through. */
|
|
2635
|
+
var blocksBackground: Boolean = true
|
|
2636
|
+
set(value) {
|
|
2637
|
+
field = value
|
|
2638
|
+
isClickable = value
|
|
2639
|
+
blocker.isClickable = value
|
|
2640
|
+
blocker.isEnabled = value
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/** A non-modal popup may opt into consuming an outside tap for dismissal. */
|
|
2644
|
+
var dismissOnOutsideTap: Boolean = false
|
|
2645
|
+
|
|
2570
2646
|
init {
|
|
2571
2647
|
isClickable = true
|
|
2572
2648
|
blocker.isClickable = true
|
|
@@ -2580,6 +2656,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
2580
2656
|
}
|
|
2581
2657
|
|
|
2582
2658
|
fun requestClose(reason: String = "backdrop") {
|
|
2659
|
+
if (!blocksBackground && !dismissOnOutsideTap) return
|
|
2583
2660
|
val handler = onRequestClose ?: return
|
|
2584
2661
|
isClickable = false
|
|
2585
2662
|
blocker.isEnabled = false
|
|
@@ -2588,8 +2665,8 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
2588
2665
|
|
|
2589
2666
|
fun setVisible(visible: Boolean, opacity: Float, animationType: String?) {
|
|
2590
2667
|
if (visible) {
|
|
2591
|
-
isClickable =
|
|
2592
|
-
blocker.isEnabled =
|
|
2668
|
+
isClickable = blocksBackground
|
|
2669
|
+
blocker.isEnabled = blocksBackground
|
|
2593
2670
|
}
|
|
2594
2671
|
if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE) {
|
|
2595
2672
|
if (visible && alpha != opacity) alpha = opacity
|
|
@@ -2643,9 +2720,33 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
2643
2720
|
translationY = 0f
|
|
2644
2721
|
alpha = 0f
|
|
2645
2722
|
isClickable = true
|
|
2723
|
+
blocksBackground = true
|
|
2724
|
+
dismissOnOutsideTap = false
|
|
2646
2725
|
blocker.isEnabled = true
|
|
2647
2726
|
onRequestClose = null
|
|
2648
2727
|
}
|
|
2728
|
+
|
|
2729
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
2730
|
+
if (!blocksBackground) {
|
|
2731
|
+
if (dismissOnOutsideTap && event.action == MotionEvent.ACTION_UP) {
|
|
2732
|
+
requestClose(reason = "backdrop")
|
|
2733
|
+
return true
|
|
2734
|
+
}
|
|
2735
|
+
// Do not let an outside popup tap fall through to a blocking modal
|
|
2736
|
+
// below it; a normal non-modal popup remains pass-through.
|
|
2737
|
+
return hasBlockingModalBelow()
|
|
2738
|
+
}
|
|
2739
|
+
return super.onTouchEvent(event)
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
fun hasBlockingModalBelow(): Boolean {
|
|
2743
|
+
val group = parent as? ViewGroup ?: return false
|
|
2744
|
+
val index = group.indexOfChild(this)
|
|
2745
|
+
return index > 0 && (0 until index).any { siblingIndex ->
|
|
2746
|
+
val sibling = group.getChildAt(siblingIndex)
|
|
2747
|
+
sibling is CraftNativeModalView && sibling.blocksBackground && sibling.visibility == View.VISIBLE && sibling.alpha > 0.01f
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2649
2750
|
}
|
|
2650
2751
|
|
|
2651
2752
|
private data class LayoutBounds(
|
|
@@ -2902,7 +3003,8 @@ internal class CraftNativeInputEcho {
|
|
|
2902
3003
|
* A native screen's `<Video>`: the platform's VideoView.
|
|
2903
3004
|
*
|
|
2904
3005
|
* It plays without taking audio focus, so the person's music carries on
|
|
2905
|
-
* under it, as on iOS. `
|
|
3006
|
+
* under it, as on iOS. `onLoadStart`, `onLoad`, `onLoadEnd`, `onPlay`,
|
|
3007
|
+
* `onPause`, `onEnd` and `onError`
|
|
2906
3008
|
* reach the screen in the web's names, from the controls as well as from
|
|
2907
3009
|
* props. With `pictureInPicture` the activity takes it into a floating
|
|
2908
3010
|
* window when the person leaves the app while it plays (MainActivity
|
|
@@ -2918,6 +3020,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2918
3020
|
private var ended = false
|
|
2919
3021
|
private var muted = false
|
|
2920
3022
|
private var resizeMode = "contain"
|
|
3023
|
+
private var requestedTimeSeconds: Double? = null
|
|
3024
|
+
private var pendingSeekSeconds: Double? = null
|
|
2921
3025
|
private var preparedPlayer: MediaPlayer? = null
|
|
2922
3026
|
/** The screen's handlers by event name, and how an event reaches them. */
|
|
2923
3027
|
var handlers: JSONObject = JSONObject()
|
|
@@ -2946,12 +3050,25 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2946
3050
|
setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE)
|
|
2947
3051
|
}
|
|
2948
3052
|
|
|
2949
|
-
fun
|
|
3053
|
+
private fun applyPendingSeek() {
|
|
3054
|
+
val target = pendingSeekSeconds ?: return
|
|
3055
|
+
if (preparedPlayer == null) return
|
|
3056
|
+
val current = currentPosition.coerceAtLeast(0) / 1000.0
|
|
3057
|
+
pendingSeekSeconds = null
|
|
3058
|
+
if (kotlin.math.abs(current - target) > 0.25) seekTo((target * 1000.0).roundToInt().coerceAtLeast(0))
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, currentTime: Any?, controls: Boolean, resizeMode: String?, pictureInPicture: Boolean = false) {
|
|
2950
3062
|
val uri = when (source) {
|
|
2951
3063
|
is String -> source
|
|
2952
3064
|
is JSONObject -> craftNativeOptionalString(source.opt("uri")).orEmpty()
|
|
2953
3065
|
else -> ""
|
|
2954
3066
|
}
|
|
3067
|
+
val nextTime = craftNativeNumber(currentTime)?.takeIf { it.isFinite() && it >= 0.0 }
|
|
3068
|
+
if (nextTime != requestedTimeSeconds) {
|
|
3069
|
+
requestedTimeSeconds = nextTime
|
|
3070
|
+
pendingSeekSeconds = nextTime
|
|
3071
|
+
}
|
|
2955
3072
|
shouldLoop = loop
|
|
2956
3073
|
val nextShouldPlay = autoplay && !paused
|
|
2957
3074
|
if (playIntent != nextShouldPlay) {
|
|
@@ -2971,6 +3088,7 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2971
3088
|
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
2972
3089
|
if (uri != sourceUri) {
|
|
2973
3090
|
sourceUri = uri
|
|
3091
|
+
pendingSeekSeconds = requestedTimeSeconds
|
|
2974
3092
|
userPaused = false
|
|
2975
3093
|
ended = false
|
|
2976
3094
|
stopPlayback()
|
|
@@ -2980,6 +3098,7 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2980
3098
|
// A copy kept on the phone (craft.media.keep) plays in place of
|
|
2981
3099
|
// the address: offline in a gym with no signal.
|
|
2982
3100
|
val kept = CraftNativeMedia.local(context, uri)
|
|
3101
|
+
fire("onLoadStart")
|
|
2983
3102
|
setVideoURI(if (kept != null) Uri.fromFile(kept) else Uri.parse(uri))
|
|
2984
3103
|
setOnPreparedListener { mediaPlayer ->
|
|
2985
3104
|
preparedPlayer = mediaPlayer
|
|
@@ -2987,7 +3106,9 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2987
3106
|
mediaPlayer.isLooping = shouldLoop
|
|
2988
3107
|
val volume = if (this.muted) 0f else 1f
|
|
2989
3108
|
mediaPlayer.setVolume(volume, volume)
|
|
3109
|
+
applyPendingSeek()
|
|
2990
3110
|
fire("onLoad", JSONObject().put("duration", duration / 1000.0))
|
|
3111
|
+
fire("onLoadEnd")
|
|
2991
3112
|
if (shouldPlay) start()
|
|
2992
3113
|
}
|
|
2993
3114
|
setOnCompletionListener {
|
|
@@ -3005,6 +3126,7 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
3005
3126
|
}
|
|
3006
3127
|
}
|
|
3007
3128
|
if (uri.isBlank()) return
|
|
3129
|
+
applyPendingSeek()
|
|
3008
3130
|
runCatching {
|
|
3009
3131
|
if (shouldPlay && !isPlaying && !userPaused && !ended) start()
|
|
3010
3132
|
else if (!shouldPlay && isPlaying) pause()
|
|
@@ -3074,6 +3196,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
3074
3196
|
stopPlayback()
|
|
3075
3197
|
setPlaying(false)
|
|
3076
3198
|
sourceUri = ""
|
|
3199
|
+
requestedTimeSeconds = null
|
|
3200
|
+
pendingSeekSeconds = null
|
|
3077
3201
|
playIntent = null
|
|
3078
3202
|
userPaused = false
|
|
3079
3203
|
ended = false
|
|
@@ -4619,6 +4743,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4619
4743
|
put("viewMeasure")
|
|
4620
4744
|
put("viewScroll")
|
|
4621
4745
|
put("viewReparent")
|
|
4746
|
+
put("viewNonModalPresentation")
|
|
4622
4747
|
}
|
|
4623
4748
|
|
|
4624
4749
|
private fun appVersion(): Pair<String, String> = runCatching {
|
|
@@ -4906,6 +5031,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4906
5031
|
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
4907
5032
|
}
|
|
4908
5033
|
if (applied.isNotEmpty()) {
|
|
5034
|
+
// Updating a modal's content may reattach its nested popups to
|
|
5035
|
+
// their declaration parent. Restore the screen overlay contract.
|
|
5036
|
+
promoteModalOverlays(screen)
|
|
4909
5037
|
scheduleLayoutEvents(screen)
|
|
4910
5038
|
}
|
|
4911
5039
|
return true
|
|
@@ -4995,6 +5123,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4995
5123
|
screen.controls.putAll(next)
|
|
4996
5124
|
screen.handlers.keys.retainAll(retainedViews(screen))
|
|
4997
5125
|
screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
|
|
5126
|
+
promoteModalOverlays(screen)
|
|
4998
5127
|
updateSafeAreaViews()
|
|
4999
5128
|
// A full render can change a view's custom layout style without
|
|
5000
5129
|
// changing the root bounds. Ensure native @layout observers still see
|
|
@@ -5003,6 +5132,45 @@ class MainActivity : AppCompatActivity() {
|
|
|
5003
5132
|
if (visibleScreen === screen) applyStatusBar(screen)
|
|
5004
5133
|
}
|
|
5005
5134
|
|
|
5135
|
+
/** Keep Modal nodes in the screen viewport even when declared in a clipped subtree. */
|
|
5136
|
+
private fun promoteModalOverlays(screen: Screen) {
|
|
5137
|
+
if (screen.modalVisibility.isEmpty()) return
|
|
5138
|
+
// Snapshot parent-before-child order before moving anything. The
|
|
5139
|
+
// controls map registers descendants first, which would place a
|
|
5140
|
+
// parent modal above its own popup when brought forward in that order.
|
|
5141
|
+
val overlays = mutableListOf<CraftNativeModalView>()
|
|
5142
|
+
fun collect(view: View) {
|
|
5143
|
+
if (view is CraftNativeModalView) overlays.add(view)
|
|
5144
|
+
if (view is ViewGroup) for (index in 0 until view.childCount) collect(view.getChildAt(index))
|
|
5145
|
+
}
|
|
5146
|
+
collect(screen.view)
|
|
5147
|
+
// A previous targeted update may already have promoted siblings in a
|
|
5148
|
+
// different order. Use declaration paths, with numeric child indices,
|
|
5149
|
+
// so a nested popup stays above its parent after every update.
|
|
5150
|
+
overlays.sortWith(Comparator { first, second ->
|
|
5151
|
+
val firstPath = first.declarationPath.split('.')
|
|
5152
|
+
val secondPath = second.declarationPath.split('.')
|
|
5153
|
+
for (index in 0 until min(firstPath.size, secondPath.size)) {
|
|
5154
|
+
val left = firstPath[index]
|
|
5155
|
+
val right = secondPath[index]
|
|
5156
|
+
val leftIndex = left.toIntOrNull()
|
|
5157
|
+
val rightIndex = right.toIntOrNull()
|
|
5158
|
+
val order = if (leftIndex != null && rightIndex != null) leftIndex.compareTo(rightIndex) else left.compareTo(right)
|
|
5159
|
+
if (order != 0) return@Comparator order
|
|
5160
|
+
}
|
|
5161
|
+
firstPath.size.compareTo(secondPath.size)
|
|
5162
|
+
})
|
|
5163
|
+
overlays.forEach { modal ->
|
|
5164
|
+
if (modal.parent !== screen.view) {
|
|
5165
|
+
(modal.parent as? ViewGroup)?.removeView(modal)
|
|
5166
|
+
screen.view.addView(modal)
|
|
5167
|
+
// Removing a view clears its old parent's flex style.
|
|
5168
|
+
screen.view.setChildStyle(modal, modal.viewportStyle, resources.displayMetrics.density)
|
|
5169
|
+
}
|
|
5170
|
+
modal.bringToFront()
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5006
5174
|
private fun applyStatusBar(screen: Screen) {
|
|
5007
5175
|
val controller = WindowCompat.getInsetsController(window, window.decorView)
|
|
5008
5176
|
val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
@@ -5365,6 +5533,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
5365
5533
|
"Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
|
|
5366
5534
|
val focusBeforeContent = screen.view.findFocus()
|
|
5367
5535
|
transparent = craftNativeBoolean(props.opt("transparent"))
|
|
5536
|
+
blocksBackground = !craftNativeBoolean(props.opt("nonModal"))
|
|
5537
|
+
dismissOnOutsideTap = craftNativeBoolean(props.opt("dismissOnOutsideTap"))
|
|
5538
|
+
// Modal nodes are promoted from their declaration parent to
|
|
5539
|
+
// the screen root. Keep the authored layout style with that
|
|
5540
|
+
// promoted child so non-modal absolute coordinates remain in
|
|
5541
|
+
// the screen viewport rather than the old clipped parent.
|
|
5542
|
+
viewportStyle = style
|
|
5543
|
+
declarationPath = path
|
|
5544
|
+
screen.view.setChildStyle(this, style, resources.displayMetrics.density)
|
|
5368
5545
|
content.configure(style, resources.displayMetrics.density)
|
|
5369
5546
|
reconcileChildren(screen, content, children, identity, next, style, previousControls)
|
|
5370
5547
|
// Dynamic :visible bindings arrive as a display style mutation;
|
|
@@ -5598,6 +5775,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5598
5775
|
paused = craftNativeBoolean(props.opt("paused")),
|
|
5599
5776
|
loop = craftNativeBoolean(props.opt("loop")),
|
|
5600
5777
|
muted = craftNativeBoolean(props.opt("muted")),
|
|
5778
|
+
currentTime = props.opt("currentTime"),
|
|
5601
5779
|
controls = craftNativeBoolean(props.opt("controls")),
|
|
5602
5780
|
resizeMode = craftNativeResizeMode(style, props),
|
|
5603
5781
|
pictureInPicture = craftNativeBoolean(props.opt("pictureInPicture")),
|
|
@@ -6001,6 +6179,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6001
6179
|
if (layout.getChildAt(index) is CraftNativeFlexSpacer) layout.removeViewAt(index)
|
|
6002
6180
|
}
|
|
6003
6181
|
val desired = mutableListOf<View>()
|
|
6182
|
+
val desiredOrder = mutableMapOf<View, Int>()
|
|
6004
6183
|
for (index in 0 until children.length()) {
|
|
6005
6184
|
val child = children.opt(index)
|
|
6006
6185
|
val childView = when (child) {
|
|
@@ -6013,9 +6192,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
6013
6192
|
}
|
|
6014
6193
|
if (childView != null) desired.add(childView)
|
|
6015
6194
|
if (childView != null && child is JSONObject) {
|
|
6016
|
-
|
|
6195
|
+
val childStyle = child.optJSONObject("style") ?: JSONObject()
|
|
6196
|
+
layout.setChildStyle(childView, childStyle, resources.displayMetrics.density)
|
|
6197
|
+
desiredOrder[childView] = craftNativeFiniteNumber(childStyle, "order", 0.0).toInt()
|
|
6017
6198
|
}
|
|
6018
6199
|
}
|
|
6200
|
+
desired.sortWith(compareBy { desiredOrder[it] ?: 0 })
|
|
6019
6201
|
if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse()
|
|
6020
6202
|
val desiredSet = desired.toSet()
|
|
6021
6203
|
for (index in layout.childCount - 1 downTo 0) {
|
|
@@ -6754,6 +6936,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6754
6936
|
linkedControl -> "link"
|
|
6755
6937
|
type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") -> "button"
|
|
6756
6938
|
type == "Icon" -> "image"
|
|
6939
|
+
type in setOf("FlatList", "SectionList") -> "list"
|
|
6757
6940
|
else -> type.lowercase()
|
|
6758
6941
|
}
|
|
6759
6942
|
}
|
|
@@ -7467,6 +7650,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
7467
7650
|
// released/recycled rows so a later identity cannot inherit a closure
|
|
7468
7651
|
// that still captures the previous row's screen state.
|
|
7469
7652
|
released.forEach {
|
|
7653
|
+
// Modals are promoted out of their logical layout into the
|
|
7654
|
+
// screen overlay. Removing the logical node therefore does not
|
|
7655
|
+
// remove the native view through ordinary child reconciliation;
|
|
7656
|
+
// detach every released view so an open popup cannot survive as
|
|
7657
|
+
// an orphan after its owner unmounts.
|
|
7658
|
+
(it.parent as? ViewGroup)?.removeView(it)
|
|
7470
7659
|
// AdapterView (including Spinner, our native Picker) rejects
|
|
7471
7660
|
// setOnClickListener altogether. Its selection callback is
|
|
7472
7661
|
// detached below; ordinary Views still need their click target
|
|
@@ -111,6 +111,11 @@ dependencies {
|
|
|
111
111
|
// Android Health Connect (included only when enabled)
|
|
112
112
|
{{HEALTH_CONNECT_DEPENDENCIES}}
|
|
113
113
|
|
|
114
|
+
// Android Health Services live exercise tracking (included only when enabled)
|
|
115
|
+
{{HEALTH_SERVICES_DEPENDENCIES}}
|
|
116
|
+
|
|
117
|
+
{{WALLET_DEPENDENCY}}
|
|
118
|
+
|
|
114
119
|
// On-device computer vision APIs exposed by CraftBridge
|
|
115
120
|
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
|
116
121
|
implementation("com.google.mlkit:image-labeling:17.0.9")
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -1227,6 +1227,11 @@ export interface NativeTabBarOptions {
|
|
|
1227
1227
|
tintDark?: string;
|
|
1228
1228
|
/** Whether Craft's horizontal tab-switch gesture may begin over the page. Defaults to true. */
|
|
1229
1229
|
swipeEnabled?: boolean;
|
|
1230
|
+
/** The selected tab highlight. Omit to keep the default capsule. */
|
|
1231
|
+
selection?: {
|
|
1232
|
+
/** Continuous corner radius in points. The highlight height is the upper bound. */
|
|
1233
|
+
cornerRadius?: number;
|
|
1234
|
+
};
|
|
1230
1235
|
}
|
|
1231
1236
|
/**
|
|
1232
1237
|
* The phone's own tab bar, floating over the page: Liquid Glass on iOS 26 and
|
|
@@ -1698,10 +1703,13 @@ export interface HealthLiveWorkoutResult {
|
|
|
1698
1703
|
}
|
|
1699
1704
|
export interface HealthLiveWorkoutEvent {
|
|
1700
1705
|
id: string;
|
|
1701
|
-
event: 'state' | 'error';
|
|
1706
|
+
event: 'state' | 'metrics' | 'error';
|
|
1702
1707
|
state?: 'notStarted' | 'prepared' | 'running' | 'paused' | 'stopped' | 'ended' | 'unknown';
|
|
1703
1708
|
/** Epoch milliseconds for a state transition. */
|
|
1704
1709
|
date?: number;
|
|
1710
|
+
heartRateBpm?: number;
|
|
1711
|
+
activeEnergyCalories?: number;
|
|
1712
|
+
distanceMeters?: number;
|
|
1705
1713
|
message?: string;
|
|
1706
1714
|
}
|
|
1707
1715
|
/** The health reads and writes a host may or may not have, depending on the app build it runs in. */
|
package/dist/cli.js
CHANGED
|
@@ -2842,7 +2842,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
2842
2842
|
`);
|
|
2843
2843
|
}
|
|
2844
2844
|
// package.json
|
|
2845
|
-
var version = "0.0.
|
|
2845
|
+
var version = "0.0.262";
|
|
2846
2846
|
|
|
2847
2847
|
// bin/cli.ts
|
|
2848
2848
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -38,6 +38,8 @@ export interface FlexStyle {
|
|
|
38
38
|
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
39
39
|
/** Flex wrap */
|
|
40
40
|
flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
|
|
41
|
+
/** Flex/grid sibling order; equal values retain source order */
|
|
42
|
+
order?: number;
|
|
41
43
|
/** Justify content (main axis) */
|
|
42
44
|
justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
|
|
43
45
|
/** Align items (cross axis); baseline aligns text-bearing children by their first baseline */
|
|
@@ -309,6 +311,8 @@ export interface BaseProps {
|
|
|
309
311
|
accessibilityState?: AccessibilityState;
|
|
310
312
|
/** Accessibility role */
|
|
311
313
|
accessibilityRole?: 'none' | 'button' | 'link' | 'image' | 'text' | 'header' | 'search' | 'adjustable' | 'menu' | 'summary' | 'alert' | 'checkbox' | 'combobox' | 'menuitem' | 'progressbar' | 'radio' | 'tablist' | 'radiogroup' | 'list' | 'menubar' | 'spinbutton' | 'switch' | 'tab' | 'timer' | 'toolbar' | 'scrollbar';
|
|
314
|
+
/** Native list chrome slot for keyed FlatList/SectionList children. */
|
|
315
|
+
listRole?: 'header' | 'item' | 'separator' | 'empty' | 'footer';
|
|
312
316
|
/** Matches a view across an animated state change. */
|
|
313
317
|
transitionID?: string;
|
|
314
318
|
/** Children elements */
|
|
@@ -454,8 +458,14 @@ export interface VideoProps extends BaseProps {
|
|
|
454
458
|
autoPlay?: boolean;
|
|
455
459
|
loop?: boolean;
|
|
456
460
|
muted?: boolean;
|
|
461
|
+
/** Controlled playback position in seconds. */
|
|
462
|
+
currentTime?: number;
|
|
457
463
|
paused?: boolean;
|
|
458
464
|
resizeMode?: 'contain' | 'cover' | 'stretch';
|
|
465
|
+
/** The source began loading. */
|
|
466
|
+
onLoadStart?: () => void;
|
|
467
|
+
/** The source became ready for playback. */
|
|
468
|
+
onLoadEnd?: () => void;
|
|
459
469
|
/**
|
|
460
470
|
* Carries on in a floating window (Picture in Picture) when the app goes to
|
|
461
471
|
* the background or another app opens while it plays, and comes back into
|
|
@@ -848,16 +858,7 @@ export interface ActivityIndicatorProps extends BaseProps {
|
|
|
848
858
|
* FlatList component props.
|
|
849
859
|
*/
|
|
850
860
|
export interface FlatListProps<T> extends ScrollViewBaseProps {
|
|
851
|
-
/**
|
|
852
|
-
data: T[];
|
|
853
|
-
/** Render item function */
|
|
854
|
-
renderItem: (info: {
|
|
855
|
-
item: T;
|
|
856
|
-
index: number;
|
|
857
|
-
}) => any;
|
|
858
|
-
/** Key extractor function */
|
|
859
|
-
keyExtractor?: (item: T, index: number) => string;
|
|
860
|
-
/** Number of columns */
|
|
861
|
+
/** Number of columns in the keyed child tree. */
|
|
861
862
|
numColumns?: number;
|
|
862
863
|
/** Render rows in reverse order along the scroll axis. */
|
|
863
864
|
inverted?: boolean;
|
|
@@ -870,6 +871,10 @@ export interface FlatListProps<T> extends ScrollViewBaseProps {
|
|
|
870
871
|
/** Refreshing state */
|
|
871
872
|
refreshing?: boolean;
|
|
872
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* @deprecated Grouped SectionList data is deferred until the versioned native
|
|
876
|
+
* list-data protocol exists. Use keyed child rows with `listRole` instead.
|
|
877
|
+
*/
|
|
873
878
|
export interface SectionListSection<T> {
|
|
874
879
|
data: T[];
|
|
875
880
|
key?: string;
|
|
@@ -881,6 +886,10 @@ export interface ModalProps extends BaseProps {
|
|
|
881
886
|
style?: StyleValue<ViewStyle>;
|
|
882
887
|
visible?: boolean;
|
|
883
888
|
transparent?: boolean;
|
|
889
|
+
/** Keep the popup interactive without blocking the screen behind it. */
|
|
890
|
+
nonModal?: boolean;
|
|
891
|
+
/** When non-modal, consume an outside tap and call onRequestClose. */
|
|
892
|
+
dismissOnOutsideTap?: boolean;
|
|
884
893
|
animationType?: 'none' | 'fade' | 'slide';
|
|
885
894
|
onShow?: () => void;
|
|
886
895
|
onDismiss?: () => void;
|
package/dist/index.cjs
CHANGED