@craft-native/android 0.0.202 → 0.0.203
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/README.md +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +142 -86
- package/dist/index.js.map +6 -5
- package/dist/testing.d.ts +2 -0
- package/package.json +1 -1
- package/templates/MainActivityNative.kt.template +151 -55
- package/templates/build.gradle.kts.app.template +2 -0
|
@@ -2020,6 +2020,9 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
2020
2020
|
|
|
2021
2021
|
private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(context) {
|
|
2022
2022
|
val content = CraftNativeFlexLayout(context)
|
|
2023
|
+
// Keep one refresh target across axis changes. Replacing all children of
|
|
2024
|
+
// SwipeRefreshLayout would also detach its own animated indicator.
|
|
2025
|
+
private val scrollContainer = FrameLayout(context)
|
|
2023
2026
|
private val vertical = ScrollView(context)
|
|
2024
2027
|
private val horizontal = HorizontalScrollView(context)
|
|
2025
2028
|
private var horizontalMode: Boolean? = null
|
|
@@ -2029,12 +2032,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2029
2032
|
private var alwaysBounceHorizontal = true
|
|
2030
2033
|
private var keyboardShouldPersistTaps = "never"
|
|
2031
2034
|
private var refreshHandler: (() -> Unit)? = null
|
|
2035
|
+
private var authoredRefreshing: Boolean? = null
|
|
2036
|
+
private val defaultProgressStart = progressViewStartOffset
|
|
2037
|
+
private val defaultProgressEnd = progressViewEndOffset
|
|
2038
|
+
private var configuredProgressOffset: Int? = null
|
|
2032
2039
|
|
|
2033
2040
|
init {
|
|
2034
2041
|
vertical.isNestedScrollingEnabled = true
|
|
2035
2042
|
horizontal.isNestedScrollingEnabled = true
|
|
2036
2043
|
vertical.isFillViewport = true
|
|
2037
2044
|
horizontal.isFillViewport = true
|
|
2045
|
+
addView(scrollContainer, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
2046
|
+
setOnChildScrollUpCallback { _, _ -> horizontalMode != true && vertical.canScrollVertically(-1) }
|
|
2038
2047
|
setOnRefreshListener { refreshHandler?.invoke() }
|
|
2039
2048
|
// The outer refresh host must stay touch-enabled so its real scroll
|
|
2040
2049
|
// child can receive gestures even when no pull-to-refresh handler is
|
|
@@ -2103,8 +2112,13 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2103
2112
|
fun setRefreshHandler(handler: (() -> Unit)?, refreshing: Boolean) {
|
|
2104
2113
|
refreshHandler = handler
|
|
2105
2114
|
isEnabled = true
|
|
2106
|
-
|
|
2107
|
-
|
|
2115
|
+
val requested = handler != null && refreshing
|
|
2116
|
+
// An unchanged false prop must not cancel a native pull before its
|
|
2117
|
+
// onRefresh callback has reached the script.
|
|
2118
|
+
if (handler == null || authoredRefreshing != requested) {
|
|
2119
|
+
authoredRefreshing = requested
|
|
2120
|
+
isRefreshing = requested
|
|
2121
|
+
}
|
|
2108
2122
|
}
|
|
2109
2123
|
|
|
2110
2124
|
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
@@ -2115,21 +2129,28 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2115
2129
|
if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
|
|
2116
2130
|
else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
|
|
2117
2131
|
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
2118
|
-
if (offset.isFinite())
|
|
2119
|
-
|
|
2132
|
+
val requested = if (offset.isFinite()) (offset * density).roundToInt() else null
|
|
2133
|
+
if (configuredProgressOffset != requested) {
|
|
2134
|
+
val wasRefreshing = isRefreshing
|
|
2135
|
+
val shift = requested ?: 0
|
|
2136
|
+
setProgressViewOffset(false, defaultProgressStart + shift, defaultProgressEnd + shift)
|
|
2137
|
+
configuredProgressOffset = requested
|
|
2138
|
+
if (wasRefreshing) isRefreshing = true
|
|
2139
|
+
}
|
|
2120
2140
|
}
|
|
2121
2141
|
|
|
2122
|
-
override fun
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2142
|
+
override fun onAttachedToWindow() {
|
|
2143
|
+
super.onAttachedToWindow()
|
|
2144
|
+
// Navigation may detach and reattach a retained screen. Its handler
|
|
2145
|
+
// belongs to the screen; releaseViewState clears it on disposal.
|
|
2146
|
+
if (authoredRefreshing == true) isRefreshing = true
|
|
2126
2147
|
}
|
|
2127
2148
|
|
|
2128
2149
|
fun setHorizontal(value: Boolean) {
|
|
2129
2150
|
if (horizontalMode == value) return
|
|
2130
2151
|
horizontalMode = value
|
|
2131
2152
|
(content.parent as? ViewGroup)?.removeView(content)
|
|
2132
|
-
removeAllViews()
|
|
2153
|
+
scrollContainer.removeAllViews()
|
|
2133
2154
|
val scroller: ViewGroup = if (value) horizontal else vertical
|
|
2134
2155
|
scroller.removeAllViews()
|
|
2135
2156
|
content.orientation = if (value) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
@@ -2137,7 +2158,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2137
2158
|
scroller.addView(content, if (value)
|
|
2138
2159
|
ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
2139
2160
|
else ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
|
2140
|
-
addView(scroller, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
2161
|
+
scrollContainer.addView(scroller, FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
2141
2162
|
}
|
|
2142
2163
|
|
|
2143
2164
|
/**
|
|
@@ -3337,28 +3358,59 @@ class MainActivity : AppCompatActivity() {
|
|
|
3337
3358
|
val capabilityTimeouts = mutableMapOf<String, Runnable>()
|
|
3338
3359
|
val audio = CraftNativeAudio()
|
|
3339
3360
|
var active = true
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3361
|
+
var closing = false
|
|
3362
|
+
private set
|
|
3363
|
+
private var closed = false
|
|
3364
|
+
private val closeWaiters = mutableListOf<() -> Unit>()
|
|
3365
|
+
private val closeHandler = Handler(Looper.getMainLooper())
|
|
3366
|
+
private val closeDeadline = Runnable { finishClose() }
|
|
3367
|
+
|
|
3368
|
+
fun close(done: () -> Unit = {}) {
|
|
3369
|
+
if (closed) { done(); return }
|
|
3370
|
+
closeWaiters.add(done)
|
|
3371
|
+
if (closing) return
|
|
3343
3372
|
active = false
|
|
3373
|
+
closing = true
|
|
3344
3374
|
audio.close()
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3375
|
+
// Evaluation completion does not mean its outbound messages have
|
|
3376
|
+
// reached the host. Close only after the ordered channel barrier,
|
|
3377
|
+
// so onDestroy's final writes run before navigation continues.
|
|
3378
|
+
closeHandler.postDelayed(closeDeadline, CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS)
|
|
3349
3379
|
val teardown = runCatching {
|
|
3350
3380
|
isolate.evaluateJavaScriptAsync(
|
|
3351
|
-
"
|
|
3381
|
+
"""
|
|
3382
|
+
(function() {
|
|
3383
|
+
var bridge = globalThis.__stxNativeBridge;
|
|
3384
|
+
if (!bridge || typeof bridge.postMessage !== 'function') return 'unavailable';
|
|
3385
|
+
try { globalThis.__stxNativeUnmount && globalThis.__stxNativeUnmount(); }
|
|
3386
|
+
catch (error) { console.error('Screen teardown failed: ' + error); }
|
|
3387
|
+
bridge.postMessage(JSON.stringify({ type: 'SCREEN_DISPOSED', payload: {} }));
|
|
3388
|
+
return 'sent';
|
|
3389
|
+
})()
|
|
3390
|
+
""".trimIndent()
|
|
3352
3391
|
)
|
|
3353
3392
|
}.getOrNull()
|
|
3354
3393
|
if (teardown == null) {
|
|
3355
|
-
|
|
3394
|
+
finishClose()
|
|
3356
3395
|
return
|
|
3357
3396
|
}
|
|
3358
3397
|
teardown.addListener({
|
|
3359
3398
|
runCatching { teardown.get() }
|
|
3360
|
-
|
|
3361
|
-
|
|
3399
|
+
.onSuccess { if (it == "unavailable") finishClose() }
|
|
3400
|
+
.onFailure { finishClose() }
|
|
3401
|
+
}, ContextCompat.getMainExecutor(view.context))
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
fun finishClose() {
|
|
3405
|
+
if (!closing) return
|
|
3406
|
+
closing = false
|
|
3407
|
+
closed = true
|
|
3408
|
+
closeHandler.removeCallbacks(closeDeadline)
|
|
3409
|
+
port?.close()
|
|
3410
|
+
isolate.close()
|
|
3411
|
+
val waiters = closeWaiters.toList()
|
|
3412
|
+
closeWaiters.clear()
|
|
3413
|
+
waiters.forEach { it() }
|
|
3362
3414
|
}
|
|
3363
3415
|
}
|
|
3364
3416
|
|
|
@@ -3745,9 +3797,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
3745
3797
|
// Already in this stack (perhaps under others): back to it, with the params it was opened with now.
|
|
3746
3798
|
val existing = screens.lastOrNull { it.path == match.path }
|
|
3747
3799
|
if (existing != null) {
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3800
|
+
val stack = screens
|
|
3801
|
+
val removed = mutableListOf<Screen>()
|
|
3802
|
+
while (stack.lastOrNull() !== existing) removed.add(stack.removeAt(stack.lastIndex))
|
|
3803
|
+
closeScreens(removed) {
|
|
3804
|
+
if (screens === stack && stack.lastOrNull() === existing) {
|
|
3805
|
+
show(existing)
|
|
3806
|
+
send(existing, "ROUTE_PARAMS", JSONObject().put("params", match.params))
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3751
3809
|
return
|
|
3752
3810
|
}
|
|
3753
3811
|
openScreen(match.screen, match.params)?.path = match.path
|
|
@@ -3772,14 +3830,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3772
3830
|
val port = if (useMessagePorts) {
|
|
3773
3831
|
isolate.createMessageChannel("craft", executor, MessagePortClient { message ->
|
|
3774
3832
|
val current = screen ?: return@MessagePortClient
|
|
3775
|
-
if (current.active && message.type == Message.TYPE_STRING) receiveSafely(current, message.string)
|
|
3833
|
+
if ((current.active || current.closing) && message.type == Message.TYPE_STRING) receiveSafely(current, message.string)
|
|
3776
3834
|
})
|
|
3777
3835
|
} else {
|
|
3778
3836
|
isolate.setConsoleCallback(executor, JavaScriptConsoleCallback { entry ->
|
|
3779
3837
|
val current = screen ?: return@JavaScriptConsoleCallback
|
|
3780
|
-
if (current.active && entry.message.startsWith(prefix)) {
|
|
3838
|
+
if ((current.active || current.closing) && entry.message.startsWith(prefix)) {
|
|
3781
3839
|
current.chunks.accept(entry.message.removePrefix(prefix))?.let { receiveSafely(current, it) }
|
|
3782
|
-
} else if (current.active) {
|
|
3840
|
+
} else if (current.active || current.closing) {
|
|
3783
3841
|
Log.i(TAG, "[${current.route ?: "home"}] ${entry.message}")
|
|
3784
3842
|
}
|
|
3785
3843
|
})
|
|
@@ -4228,11 +4286,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
4228
4286
|
}
|
|
4229
4287
|
val payload = message.optJSONObject("payload") ?: JSONObject()
|
|
4230
4288
|
val type = message.optString("type")
|
|
4289
|
+
if (!screen.active && (!screen.closing || type !in setOf("API_REQUEST", "API_CANCEL", "SYNC_WRITE", "SCREEN_DISPOSED"))) return
|
|
4231
4290
|
// Timers and frames come many times a second: not worth a log line each.
|
|
4232
4291
|
if (type != "TIMER" && type != "TIMER_CLEAR" && type != "FRAME" && Log.isLoggable(TAG, Log.DEBUG)) {
|
|
4233
4292
|
Log.d(TAG, "Native STX $type (${raw.length} chars) for ${screen.route ?: "home"}")
|
|
4234
4293
|
}
|
|
4235
4294
|
when (message.optString("type")) {
|
|
4295
|
+
"SCREEN_DISPOSED" -> if (screen.closing) screen.finishClose()
|
|
4236
4296
|
"RENDER" -> payload.optJSONObject("document")?.let { render(screen, it) }
|
|
4237
4297
|
"MUTATE" -> try {
|
|
4238
4298
|
val result = applyMutation(screen, payload)
|
|
@@ -4290,9 +4350,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
4290
4350
|
if (visibleScreen !== screen) return
|
|
4291
4351
|
val route = payload.optString("screen").takeIf { it.isNotBlank() } ?: return
|
|
4292
4352
|
if (message.optString("type") == "NAVIGATE_REPLACE") {
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4353
|
+
val stack = screens
|
|
4354
|
+
val removed = stack.removeAt(stack.lastIndex)
|
|
4355
|
+
val previous = stack.lastOrNull()
|
|
4356
|
+
closeScreen(removed) {
|
|
4357
|
+
if (screens === stack && stack.lastOrNull() === previous) openScreen(route, payload.optJSONObject("params") ?: JSONObject())
|
|
4358
|
+
}
|
|
4359
|
+
} else openScreen(route, payload.optJSONObject("params") ?: JSONObject())
|
|
4296
4360
|
}
|
|
4297
4361
|
"NAVIGATE_BACK" -> if (visibleScreen === screen) back()
|
|
4298
4362
|
"NAVIGATE_POP_TO_ROOT" -> if (visibleScreen === screen) popToRoot()
|
|
@@ -4301,9 +4365,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4301
4365
|
val id = payload.optString("requestId")
|
|
4302
4366
|
if (id.isNotBlank()) {
|
|
4303
4367
|
val token = "${screen.capabilityScope}/$id"
|
|
4304
|
-
screen.pendingCapabilities.remove(token)
|
|
4368
|
+
val pending = screen.pendingCapabilities.remove(token)
|
|
4305
4369
|
screen.capabilityTimeouts.remove(token)?.let(capabilityHandler::removeCallbacks)
|
|
4306
|
-
capabilities.cancel(token)
|
|
4370
|
+
if (pending) capabilities.cancel(token)
|
|
4307
4371
|
}
|
|
4308
4372
|
}
|
|
4309
4373
|
"AUDIO" -> screen.audio.handle(payload)
|
|
@@ -4483,6 +4547,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4483
4547
|
val children = node.optJSONArray("children") ?: JSONArray()
|
|
4484
4548
|
val protocolId = node.optString("id").takeIf { it.isNotBlank() }
|
|
4485
4549
|
val explicitKey = authoredKey(node, props)
|
|
4550
|
+
// Descendants are scoped to this retained identity, not its sibling
|
|
4551
|
+
// position. Moving a keyed parent must retain its nested inputs too.
|
|
4486
4552
|
val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
4487
4553
|
?: protocolId?.let { "id:$it" }
|
|
4488
4554
|
?: path
|
|
@@ -4581,8 +4647,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4581
4647
|
}
|
|
4582
4648
|
}
|
|
4583
4649
|
}
|
|
4584
|
-
"TextInput" -> (reusableInput
|
|
4585
|
-
?: EditText(this)).apply {
|
|
4650
|
+
"TextInput" -> (reusableInput ?: EditText(this)).apply {
|
|
4586
4651
|
// Setting filters, the input type or single-line mode makes
|
|
4587
4652
|
// TextView set its text again, which puts the caret back at
|
|
4588
4653
|
// the start: typing then came out backwards. Each is set only
|
|
@@ -4762,7 +4827,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4762
4827
|
"Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
|
|
4763
4828
|
transparent = props.optBoolean("transparent", false)
|
|
4764
4829
|
content.configure(style, resources.displayMetrics.density)
|
|
4765
|
-
reconcileChildren(screen, content, children,
|
|
4830
|
+
reconcileChildren(screen, content, children, identity, next, style, previousControls)
|
|
4766
4831
|
// Dynamic :visible bindings arrive as a display style mutation;
|
|
4767
4832
|
// static visible props remain authoritative when present.
|
|
4768
4833
|
val visible = if (props.has("visible") && !props.isNull("visible")) {
|
|
@@ -4807,7 +4872,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4807
4872
|
configureContainer(this, style)
|
|
4808
4873
|
screen.pressActiveOpacities[this] = craftNativeFiniteFloat(props, "activeOpacity", 0.5f)
|
|
4809
4874
|
screen.pressBaseOpacities[this] = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
4810
|
-
reconcileChildren(screen, this, children,
|
|
4875
|
+
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
4811
4876
|
}
|
|
4812
4877
|
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
4813
4878
|
setOnCheckedChangeListener(null)
|
|
@@ -4956,7 +5021,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4956
5021
|
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
4957
5022
|
updateImage(screen, image, props.opt("source"), props.opt("defaultSource"))
|
|
4958
5023
|
configureContainer(content, style)
|
|
4959
|
-
reconcileChildren(screen, content, children,
|
|
5024
|
+
reconcileChildren(screen, content, children, identity, next, style, previousControls)
|
|
4960
5025
|
}
|
|
4961
5026
|
"Video" -> (previous as? CraftNativeVideoView ?: CraftNativeVideoView(this)).apply {
|
|
4962
5027
|
handlers = events
|
|
@@ -5077,7 +5142,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5077
5142
|
val child = children.opt(index)
|
|
5078
5143
|
if (child !is JSONObject || canonicalType(child.optString("type")) != "RefreshControl") contentChildren.put(child)
|
|
5079
5144
|
}
|
|
5080
|
-
reconcileChildren(screen, content, contentChildren,
|
|
5145
|
+
reconcileChildren(screen, content, contentChildren, identity, next, contentStyle, previousControls)
|
|
5081
5146
|
}
|
|
5082
5147
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
5083
5148
|
val nativeList = list
|
|
@@ -5091,21 +5156,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
5091
5156
|
?.hideSoftInputFromWindow(windowToken, 0)
|
|
5092
5157
|
}
|
|
5093
5158
|
nativeList.overScrollMode = if (props.optBoolean("bounces", true)) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
5094
|
-
reconcileFlatList(screen, nativeList, children,
|
|
5159
|
+
reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events)
|
|
5095
5160
|
}
|
|
5096
5161
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
5097
5162
|
configureContainer(this, style)
|
|
5098
5163
|
if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
|
|
5099
|
-
reconcileChildren(screen, this, children,
|
|
5164
|
+
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
5100
5165
|
}
|
|
5101
5166
|
"KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
|
|
5102
5167
|
configure(props, style, resources.displayMetrics.density)
|
|
5103
5168
|
setKeyboardInset(keyboardInset)
|
|
5104
|
-
reconcileChildren(screen, content, children,
|
|
5169
|
+
reconcileChildren(screen, content, children, identity, next, style, previousControls)
|
|
5105
5170
|
}
|
|
5106
5171
|
else -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
5107
5172
|
configureContainer(this, style)
|
|
5108
|
-
reconcileChildren(screen, this, children,
|
|
5173
|
+
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
5109
5174
|
}
|
|
5110
5175
|
}
|
|
5111
5176
|
if (control is CraftNativeFlexLayout) {
|
|
@@ -5286,6 +5351,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5286
5351
|
"sectionlist" -> "SectionList"
|
|
5287
5352
|
"flatlist" -> "FlatList"
|
|
5288
5353
|
"svg" -> "Svg"
|
|
5354
|
+
"g" -> "G"
|
|
5355
|
+
"svgtext", "svg-text" -> "SvgText"
|
|
5289
5356
|
"touchableopacity" -> "TouchableOpacity"
|
|
5290
5357
|
"touchablehighlight" -> "TouchableHighlight"
|
|
5291
5358
|
"pressable" -> "Pressable"
|
|
@@ -5974,15 +6041,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
5974
6041
|
private fun inputTypeFor(props: JSONObject): Int {
|
|
5975
6042
|
val base = if (props.optBoolean("multiline", false)) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
5976
6043
|
else InputType.TYPE_CLASS_TEXT
|
|
6044
|
+
// Text variations do not carry their input class or multiline flag.
|
|
6045
|
+
// Preserve both when selecting an email, URL, or search keyboard.
|
|
5977
6046
|
val keyboard = when (props.optString("keyboardType")) {
|
|
5978
|
-
"email-address" -> InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
5979
|
-
"url" -> InputType.TYPE_TEXT_VARIATION_URI
|
|
6047
|
+
"email-address" -> base or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
6048
|
+
"url" -> base or InputType.TYPE_TEXT_VARIATION_URI
|
|
5980
6049
|
"numeric", "number-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
5981
6050
|
"decimal-pad" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
5982
6051
|
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
5983
|
-
"web-search" ->
|
|
5984
|
-
"visible-password" ->
|
|
5985
|
-
else ->
|
|
6052
|
+
"web-search" -> base or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
|
|
6053
|
+
"visible-password" -> base or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
6054
|
+
else -> base
|
|
5986
6055
|
}
|
|
5987
6056
|
val capitalization = when (props.optString("autoCapitalize")) {
|
|
5988
6057
|
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
@@ -5992,7 +6061,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5992
6061
|
}
|
|
5993
6062
|
val correction = if (props.optBoolean("autoCorrect", true)) InputType.TYPE_TEXT_FLAG_AUTO_CORRECT else 0
|
|
5994
6063
|
val secure = if (props.optBoolean("secureTextEntry", false)) InputType.TYPE_TEXT_VARIATION_PASSWORD else 0
|
|
5995
|
-
return
|
|
6064
|
+
return keyboard or capitalization or correction or secure
|
|
5996
6065
|
}
|
|
5997
6066
|
|
|
5998
6067
|
/// The autofill hint for `textContentType` (iOS's names) or `autoComplete`
|
|
@@ -6081,7 +6150,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6081
6150
|
}
|
|
6082
6151
|
}
|
|
6083
6152
|
perform { result ->
|
|
6084
|
-
if (!screen.active || !screen.pendingCapabilities.remove(token)) return@perform
|
|
6153
|
+
if ((!screen.active && !screen.closing) || !screen.pendingCapabilities.remove(token)) return@perform
|
|
6085
6154
|
screen.capabilityTimeouts.remove(token)?.let(capabilityHandler::removeCallbacks)
|
|
6086
6155
|
val response = JSONObject()
|
|
6087
6156
|
.put("version", CRAFT_NATIVE_CAPABILITY_PROTOCOL_VERSION)
|
|
@@ -6316,15 +6385,26 @@ class MainActivity : AppCompatActivity() {
|
|
|
6316
6385
|
|
|
6317
6386
|
/** Back to the first screen in one move. */
|
|
6318
6387
|
private fun popToRoot() {
|
|
6319
|
-
|
|
6320
|
-
|
|
6388
|
+
val stack = screens
|
|
6389
|
+
val removed = mutableListOf<Screen>()
|
|
6390
|
+
while (stack.size > 1) removed.add(stack.removeAt(stack.lastIndex))
|
|
6391
|
+
val target = stack.lastOrNull()
|
|
6392
|
+
closeScreens(removed) {
|
|
6393
|
+
if (target != null && screens === stack && stack.lastOrNull() === target) show(target)
|
|
6394
|
+
}
|
|
6321
6395
|
}
|
|
6322
6396
|
|
|
6323
6397
|
/** Back: the screen below in this tab, then the first tab, then out of the app. */
|
|
6324
6398
|
private fun back() {
|
|
6399
|
+
// A previous back is still draining its screen's teardown messages.
|
|
6400
|
+
if (visibleScreen == null) return
|
|
6325
6401
|
if (screens.size > 1) {
|
|
6326
|
-
|
|
6327
|
-
|
|
6402
|
+
val stack = screens
|
|
6403
|
+
val removed = stack.removeAt(stack.lastIndex)
|
|
6404
|
+
val target = stack.last()
|
|
6405
|
+
closeScreen(removed) {
|
|
6406
|
+
if (screens === stack && stack.lastOrNull() === target) show(target)
|
|
6407
|
+
}
|
|
6328
6408
|
} else if (chrome != null && currentTab != 0) {
|
|
6329
6409
|
selectTab(0)
|
|
6330
6410
|
} else {
|
|
@@ -6542,7 +6622,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
6542
6622
|
screen.modalCloseHandlers.keys.removeAll(released)
|
|
6543
6623
|
}
|
|
6544
6624
|
|
|
6545
|
-
private fun
|
|
6625
|
+
private fun closeScreens(screens: List<Screen>, done: () -> Unit) {
|
|
6626
|
+
val iterator = screens.iterator()
|
|
6627
|
+
fun next() {
|
|
6628
|
+
if (iterator.hasNext()) closeScreen(iterator.next()) { next() }
|
|
6629
|
+
else done()
|
|
6630
|
+
}
|
|
6631
|
+
next()
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6634
|
+
private fun closeScreen(screen: Screen, done: () -> Unit = {}) {
|
|
6635
|
+
if (!screen.active) { screen.close(done); return }
|
|
6546
6636
|
CraftNativeHub.unregister(screen.capabilityScope)
|
|
6547
6637
|
screen.timers.values.forEach(capabilityHandler::removeCallbacks)
|
|
6548
6638
|
screen.timers.clear()
|
|
@@ -6578,7 +6668,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
6578
6668
|
screen.flatListRows.clear()
|
|
6579
6669
|
screen.flatListOwners.clear()
|
|
6580
6670
|
screen.flatListOwnerByView.clear()
|
|
6581
|
-
screen.close
|
|
6671
|
+
screen.close {
|
|
6672
|
+
screen.capabilityTimeouts.values.forEach(capabilityHandler::removeCallbacks)
|
|
6673
|
+
screen.capabilityTimeouts.clear()
|
|
6674
|
+
screen.pendingCapabilities.forEach(capabilities::cancel)
|
|
6675
|
+
screen.pendingCapabilities.clear()
|
|
6676
|
+
done()
|
|
6677
|
+
}
|
|
6582
6678
|
}
|
|
6583
6679
|
|
|
6584
6680
|
@Deprecated("Uses the activity back callback for native route navigation")
|
|
@@ -80,6 +80,8 @@ dependencies {
|
|
|
80
80
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
|
81
81
|
{{REFRESH_LAYOUT_DEPENDENCY}}
|
|
82
82
|
implementation("androidx.webkit:webkit:1.12.1")
|
|
83
|
+
// App Link exclusions hand off to a browser in both renderers.
|
|
84
|
+
implementation("androidx.browser:browser:1.8.0")
|
|
83
85
|
{{JAVASCRIPT_ENGINE_DEPENDENCY}}
|
|
84
86
|
implementation("com.google.android.material:material:1.11.0")
|
|
85
87
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|