craft-native 0.0.247 → 0.0.249
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 +2 -0
- package/dist/android/src/index.js +22 -18
- package/dist/android/templates/CraftNative.kt.template +17 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
- package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
- package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
- package/dist/android/templates/CraftNativeServices.kt.template +25 -0
- package/dist/android/templates/CraftNativeShell.kt.template +41 -9
- package/dist/android/templates/MainActivityNative.kt.template +369 -69
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/api/mobile.d.ts +37 -1
- package/dist/browser.js +9 -9
- package/dist/cli.js +90 -673
- package/dist/components/index.d.ts +50 -9
- package/dist/index.cjs +394 -438
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +446 -493
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +54 -53
- package/dist/ios/templates/CraftApp.swift +44 -3
- package/dist/ios/templates/CraftHealth.swift +217 -0
- package/dist/ios/templates/CraftHeartRate.swift +6 -3
- package/dist/ios/templates/CraftHybrid.swift +30 -22
- package/dist/ios/templates/CraftLiveActivities.swift +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +87 -8
- package/dist/ios/templates/CraftNativeFiles.swift +15 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
- package/dist/ios/templates/CraftNativeMutation.swift +36 -0
- package/dist/ios/templates/CraftNativeScreen.swift +826 -69
- package/dist/ios/templates/CraftNativeServices.swift +4 -2
- package/dist/ios/templates/CraftNativeUI.swift +10 -2
- package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
- package/dist/ios/templates/testing/HostApp.swift +9 -2
- package/dist/macos/templates/CraftNativeMutation.swift +36 -0
- package/dist/macos/templates/native_layout.zig +226 -2
- package/dist/mobile-testing.js +2 -2
- package/dist/mobile.js +101 -63
- package/package.json +1 -1
|
@@ -57,11 +57,24 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
57
57
|
rightMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
58
58
|
bottomMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
59
59
|
})
|
|
60
|
+
refreshThemeDefaults()
|
|
60
61
|
mapView.onCreate(null)
|
|
61
62
|
mapView.addOnDidFinishLoadingMapListener { if (!destroyed && initialCameraKey == null) applyPending() }
|
|
62
63
|
mapView.getMapAsync(this)
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
fun refreshThemeDefaults() {
|
|
67
|
+
val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
68
|
+
val foreground = if (dark) Color.rgb(245, 245, 245) else Color.rgb(37, 51, 33)
|
|
69
|
+
val surface = if (dark) Color.argb(230, 38, 38, 38) else Color.argb(230, 255, 255, 255)
|
|
70
|
+
attribution.setTextColor(foreground)
|
|
71
|
+
attribution.setBackgroundColor(surface)
|
|
72
|
+
scale.setBackgroundColor(surface)
|
|
73
|
+
scaleLabel.setTextColor(foreground)
|
|
74
|
+
scaleLine.setBackgroundColor(foreground)
|
|
75
|
+
readyStyle?.let { updateOverlays(it) }
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
override fun onMapReady(nativeMap: MapLibreMap) {
|
|
66
79
|
if (destroyed) return
|
|
67
80
|
map = nativeMap
|
|
@@ -109,6 +122,14 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
109
122
|
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
110
123
|
|
|
111
124
|
private fun applyPending() {
|
|
125
|
+
// MapLibre may deliver style and render callbacks from its worker
|
|
126
|
+
// thread. All map, style, and GeoJSON source mutations must happen on
|
|
127
|
+
// Android's UI thread, so marshal the whole reconciliation before
|
|
128
|
+
// touching the native map object.
|
|
129
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
130
|
+
post { applyPending() }
|
|
131
|
+
return
|
|
132
|
+
}
|
|
112
133
|
val nativeMap = map ?: return
|
|
113
134
|
if (destroyed) return
|
|
114
135
|
nativeMap.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
|
|
@@ -200,6 +221,12 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
200
221
|
style.addLayer(LineLayer("craft-map-routes", "craft-map-overlays")
|
|
201
222
|
.withFilter(Expression.eq(Expression.get("kind"), "line"))
|
|
202
223
|
.withProperties(lineColor(Expression.toColor(Expression.get("color"))), lineWidth(Expression.get("width")), lineCap("round"), lineJoin("round")))
|
|
224
|
+
style.addLayer(FillLayer("craft-map-polygons", "craft-map-overlays")
|
|
225
|
+
.withFilter(Expression.eq(Expression.get("kind"), "polygon"))
|
|
226
|
+
.withProperties(fillColor(Expression.toColor(Expression.get("fillColor")))))
|
|
227
|
+
style.addLayer(LineLayer("craft-map-polygon-strokes", "craft-map-overlays")
|
|
228
|
+
.withFilter(Expression.eq(Expression.get("kind"), "polygon"))
|
|
229
|
+
.withProperties(lineColor(Expression.toColor(Expression.get("strokeColor"))), lineWidth(Expression.get("strokeWidth")), lineCap("round"), lineJoin("round")))
|
|
203
230
|
style.addLayer(CircleLayer("craft-map-markers", "craft-map-overlays")
|
|
204
231
|
.withFilter(Expression.eq(Expression.get("kind"), "marker"))
|
|
205
232
|
.withProperties(circleColor(Expression.toColor(Expression.get("color"))), circleRadius(7f), circleStrokeColor(Color.WHITE), circleStrokeWidth(2f)))
|
|
@@ -212,6 +239,42 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
212
239
|
val features = JSONArray()
|
|
213
240
|
val pointsForFit = mutableListOf<LatLng>()
|
|
214
241
|
markerHandlers.clear()
|
|
242
|
+
val dataMarkers = pendingProps.optJSONArray("markers")
|
|
243
|
+
for (index in 0 until (dataMarkers?.length() ?: 0)) {
|
|
244
|
+
val overlay = dataMarkers?.optJSONObject(index) ?: continue
|
|
245
|
+
val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
|
|
246
|
+
val point = coordinates(overlay.optJSONArray("geometry")).firstOrNull() ?: continue
|
|
247
|
+
val bearing = finiteNumber(overlay.opt("bearing"))
|
|
248
|
+
val properties = JSONObject()
|
|
249
|
+
.put("craftOverlayId", id)
|
|
250
|
+
.put("kind", if (bearing == null) "marker" else "direction")
|
|
251
|
+
.put("craftMarkerId", id)
|
|
252
|
+
.put("bearing", (bearing ?: 0.0) % 360)
|
|
253
|
+
.put("color", cssColor(overlay.opt("pinColor"), Color.rgb(36, 114, 75)))
|
|
254
|
+
val geometry = JSONObject().put("type", "Point").put("coordinates", JSONArray().put(point.longitude).put(point.latitude))
|
|
255
|
+
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
256
|
+
pointsForFit.add(point)
|
|
257
|
+
}
|
|
258
|
+
val dataPolylines = pendingProps.optJSONArray("polylines")
|
|
259
|
+
for (index in 0 until (dataPolylines?.length() ?: 0)) {
|
|
260
|
+
val overlay = dataPolylines?.optJSONObject(index) ?: continue
|
|
261
|
+
val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
|
|
262
|
+
val points = coordinates(overlay.optJSONArray("geometry"))
|
|
263
|
+
if (points.size <= 1) continue
|
|
264
|
+
val rawWidth = overlay.opt("strokeWidth")
|
|
265
|
+
val logicalWidth = if (rawWidth == null || rawWidth == JSONObject.NULL) 3.0 else finiteNumber(rawWidth) ?: continue
|
|
266
|
+
val width = (logicalWidth * resources.displayMetrics.density).takeIf { it.isFinite() && it > 0 } ?: continue
|
|
267
|
+
val properties = JSONObject()
|
|
268
|
+
.put("craftOverlayId", id)
|
|
269
|
+
.put("kind", "line")
|
|
270
|
+
.put("color", cssColor(overlay.opt("strokeColor"), Color.BLUE))
|
|
271
|
+
.put("width", width)
|
|
272
|
+
val geometry = JSONObject().put("type", "LineString").put("coordinates", JSONArray().apply {
|
|
273
|
+
points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) }
|
|
274
|
+
})
|
|
275
|
+
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
276
|
+
pointsForFit.addAll(points)
|
|
277
|
+
}
|
|
215
278
|
for (index in 0 until pendingChildren.length()) {
|
|
216
279
|
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
217
280
|
val type = child.optString("type").lowercase()
|
|
@@ -231,11 +294,25 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
231
294
|
pointsForFit.add(point)
|
|
232
295
|
} else if ((type == "mappolyline" || type == "polyline") && points.size > 1) {
|
|
233
296
|
val authoredWidth = props.opt("strokeWidth")
|
|
234
|
-
val
|
|
235
|
-
|
|
297
|
+
val logicalWidth = if (authoredWidth == null || authoredWidth == JSONObject.NULL) 3.0 else finiteNumber(authoredWidth) ?: continue
|
|
298
|
+
// The shared contract is expressed in logical points. MapLibre's
|
|
299
|
+
// style line-width is in physical pixels, just like the Google
|
|
300
|
+
// Maps polyline width, so convert only at this host boundary.
|
|
301
|
+
val width = (logicalWidth * resources.displayMetrics.density)
|
|
302
|
+
.takeIf { it.isFinite() && it > 0 } ?: continue
|
|
236
303
|
properties.put("kind", "line").put("color", cssColor(props.opt("strokeColor"), Color.BLUE)).put("width", width)
|
|
237
304
|
geometry.put("type", "LineString").put("coordinates", JSONArray().apply { points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } })
|
|
238
305
|
pointsForFit.addAll(points)
|
|
306
|
+
} else if ((type == "mappolygon" || type == "polygon") && points.size > 2) {
|
|
307
|
+
val rawWidth = props.opt("strokeWidth")
|
|
308
|
+
val logicalWidth = if (rawWidth == null || rawWidth == JSONObject.NULL) 2.0 else finiteNumber(rawWidth) ?: continue
|
|
309
|
+
val width = (logicalWidth * resources.displayMetrics.density).takeIf { it.isFinite() && it > 0 } ?: continue
|
|
310
|
+
val fill = cssColor(props.opt("fillColor"), Color.argb(72, 33, 150, 83))
|
|
311
|
+
val stroke = cssColor(props.opt("strokeColor"), Color.rgb(33, 150, 83))
|
|
312
|
+
properties.put("kind", "polygon").put("fillColor", fill).put("strokeColor", stroke).put("strokeWidth", width)
|
|
313
|
+
val ring = points + points.first()
|
|
314
|
+
geometry.put("type", "Polygon").put("coordinates", JSONArray().put(JSONArray().apply { ring.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } }))
|
|
315
|
+
pointsForFit.addAll(points)
|
|
239
316
|
} else continue
|
|
240
317
|
features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
|
|
241
318
|
}
|
|
@@ -244,10 +321,20 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
244
321
|
}
|
|
245
322
|
|
|
246
323
|
private fun cssColor(value: Any?, fallback: Int): String {
|
|
247
|
-
val color = (value as? String)?.let {
|
|
324
|
+
val color = (value as? String)?.let { mapColor(it, fallback) } ?: fallback
|
|
248
325
|
return "rgba(${Color.red(color)},${Color.green(color)},${Color.blue(color)},${Color.alpha(color) / 255.0})"
|
|
249
326
|
}
|
|
250
327
|
|
|
328
|
+
private fun mapColor(raw: String, fallback: Int): Int {
|
|
329
|
+
val value = raw.trim().lowercase()
|
|
330
|
+
val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
331
|
+
return when (value) {
|
|
332
|
+
"label" -> if (dark) Color.WHITE else Color.BLACK
|
|
333
|
+
"secondarylabel", "secondary-label" -> if (dark) Color.LTGRAY else Color.DKGRAY
|
|
334
|
+
else -> runCatching { craftNativeColor(raw) }.getOrDefault(fallback)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
private fun directionBitmap(): Bitmap {
|
|
252
339
|
val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
|
|
253
340
|
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
@@ -111,6 +111,14 @@ class CraftNativeMutationDocument {
|
|
|
111
111
|
moveChild(operation, candidateNodes)
|
|
112
112
|
operation.optString("parentId").takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
|
|
113
113
|
}
|
|
114
|
+
"reparentChild" -> {
|
|
115
|
+
val previousParent = reparentChild(operation, candidateNodes)
|
|
116
|
+
operation.optString("parentId").takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
|
|
117
|
+
previousParent.takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
|
|
118
|
+
// A child crossing parents needs the committed tree to
|
|
119
|
+
// be reconciled so the host can move its existing view.
|
|
120
|
+
requiresFullRender = true
|
|
121
|
+
}
|
|
114
122
|
"removeNode" -> {
|
|
115
123
|
val parent = candidateNodes[operation.optString("id")]?.parent
|
|
116
124
|
if (parent == null) requiresFullRender = true
|
|
@@ -152,6 +160,9 @@ class CraftNativeMutationDocument {
|
|
|
152
160
|
|
|
153
161
|
fun node(id: String): JSONObject? = materialize(id, nodes)
|
|
154
162
|
|
|
163
|
+
/** Reserve surviving protocol nodes before reusing an authored keyed slot. */
|
|
164
|
+
fun containsNode(id: String): Boolean = nodes.containsKey(id)
|
|
165
|
+
|
|
155
166
|
/** The committed parent used to redraw a child that has no native view. */
|
|
156
167
|
fun parentId(id: String): String? = nodes[id]?.parent
|
|
157
168
|
|
|
@@ -225,6 +236,27 @@ class CraftNativeMutationDocument {
|
|
|
225
236
|
parent.children.add(insertionIndex(operation, parent.children.size), childId)
|
|
226
237
|
}
|
|
227
238
|
|
|
239
|
+
private fun reparentChild(operation: JSONObject, nodes: MutableMap<String, Node>): String {
|
|
240
|
+
val parentId = requiredId(operation, "parentId", "reparentChild")
|
|
241
|
+
val childId = requiredId(operation, "childId", "reparentChild")
|
|
242
|
+
val parent = nodes[parentId] ?: fail("UNKNOWN_PARENT", "node $parentId does not exist")
|
|
243
|
+
val child = nodes[childId] ?: fail("UNKNOWN_NODE", "node $childId does not exist")
|
|
244
|
+
val previousParentId = child.parent
|
|
245
|
+
?: fail("NOT_ATTACHED", "node $childId is not attached to a parent")
|
|
246
|
+
val previousParent = nodes[previousParentId]
|
|
247
|
+
?: fail("INVALID_TREE", "node $childId has a missing parent")
|
|
248
|
+
if (!acceptsChildren(parent.type)) fail("INVALID_PARENT", "node $parentId cannot contain child nodes")
|
|
249
|
+
if (parentId == childId || parentId in descendants(childId, nodes)) {
|
|
250
|
+
fail("CYCLE", "reparenting node $childId would create a cycle")
|
|
251
|
+
}
|
|
252
|
+
if (parentId == previousParentId) fail("SAME_PARENT", "node $childId is already a child of $parentId")
|
|
253
|
+
val index = insertionIndex(operation, parent.children.size)
|
|
254
|
+
previousParent.children.remove(childId)
|
|
255
|
+
parent.children.add(index, childId)
|
|
256
|
+
child.parent = parentId
|
|
257
|
+
return previousParentId
|
|
258
|
+
}
|
|
259
|
+
|
|
228
260
|
private fun removeNode(
|
|
229
261
|
operation: JSONObject,
|
|
230
262
|
nodes: MutableMap<String, Node>,
|
|
@@ -22,6 +22,12 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
|
|
|
22
22
|
if (handler == null) isRefreshing = false
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
fun resetForTeardown() {
|
|
26
|
+
refreshHandler = null
|
|
27
|
+
isRefreshing = false
|
|
28
|
+
isEnabled = false
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
fun setRefreshAppearance(props: JSONObject, fallback: Int) {
|
|
26
32
|
val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
|
|
27
33
|
val resolved = tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
|
|
@@ -29,8 +35,7 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
override fun onDetachedFromWindow() {
|
|
32
|
-
|
|
33
|
-
isRefreshing = false
|
|
38
|
+
resetForTeardown()
|
|
34
39
|
super.onDetachedFromWindow()
|
|
35
40
|
}
|
|
36
41
|
}
|
|
@@ -113,6 +113,7 @@ internal class CraftNativeServices(
|
|
|
113
113
|
// `health.grantedTypes()` and `health.openSettings()`.
|
|
114
114
|
add("healthAvailability")
|
|
115
115
|
}
|
|
116
|
+
if (config.optBoolean("enableWallet")) add("wallet")
|
|
116
117
|
if (config.optBoolean("enableKeepAwake")) add("keepAwake")
|
|
117
118
|
if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
|
|
118
119
|
add("geolocation")
|
|
@@ -130,6 +131,7 @@ internal class CraftNativeServices(
|
|
|
130
131
|
fun handles(module: String): Boolean = module in setOf(
|
|
131
132
|
"Dialog", "Browser", "Linking", "Share", "Push", "Health", "KeepAwake", "Brightness", "StatusBar", "Speech", "Background",
|
|
132
133
|
"Location", "Media", "CrashReporter", "HeartRate",
|
|
134
|
+
"Wallet",
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
fun perform(module: String, method: String, args: JSONArray, completion: (CraftNativeCapabilityResult) -> Unit) {
|
|
@@ -144,6 +146,7 @@ internal class CraftNativeServices(
|
|
|
144
146
|
"Share" -> share(args.optJSONObject(0) ?: JSONObject(), done)
|
|
145
147
|
"Push" -> push(method, done)
|
|
146
148
|
"Health" -> health?.perform(method, args, done) ?: done(failure("CAPABILITY_DISABLED", "Health Connect is not enabled for this app"))
|
|
149
|
+
"Wallet" -> wallet(method, args, done)
|
|
147
150
|
"KeepAwake" -> keepAwake(args.optBoolean(0, true), done)
|
|
148
151
|
"Brightness" -> brightness(method, args, done)
|
|
149
152
|
"StatusBar" -> statusBar(args.optString(0, "default"), done)
|
|
@@ -176,6 +179,28 @@ internal class CraftNativeServices(
|
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
private fun wallet(method: String, args: JSONArray, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
183
|
+
if (!config.optBoolean("enableWallet")) return done(failure("CAPABILITY_DISABLED", "Wallet is disabled"))
|
|
184
|
+
when (method) {
|
|
185
|
+
"isAvailable" -> done(success(runCatching {
|
|
186
|
+
context.packageManager.getLaunchIntentForPackage("com.google.android.apps.walletnfcrel") != null
|
|
187
|
+
}.getOrDefault(false)))
|
|
188
|
+
"add" -> {
|
|
189
|
+
val source = args.optJSONObject(0) ?: return done(failure("INVALID_ARGUMENT", "Wallet.add needs a source object"))
|
|
190
|
+
val address = source.optString("url").trim()
|
|
191
|
+
if (!address.startsWith("https://")) return done(failure("INVALID_ARGUMENT", "Android Wallet.add needs an HTTPS save URL"))
|
|
192
|
+
val host = activity() ?: return done(failure("NEEDS_ACTIVITY", "No screen is showing to open Wallet"))
|
|
193
|
+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(address))
|
|
194
|
+
if (intent.resolveActivity(host.packageManager) == null) return done(failure("UNAVAILABLE", "Google Wallet cannot open this save URL"))
|
|
195
|
+
runCatching { host.startActivity(intent) }
|
|
196
|
+
.onSuccess { done(success(true)) }
|
|
197
|
+
.onFailure { done(failure("PRESENTATION_FAILED", it.message ?: "Google Wallet could not open")) }
|
|
198
|
+
}
|
|
199
|
+
"has" -> done(failure("UNSUPPORTED", "Google Wallet pass lookup is not available through the save-link API"))
|
|
200
|
+
else -> done(failure("UNKNOWN_ACTION", "Unsupported Wallet method"))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
// MARK: Dialogs
|
|
180
205
|
|
|
181
206
|
private fun dialog(method: String, options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
@@ -447,6 +447,24 @@ internal object CraftNativePrelude {
|
|
|
447
447
|
// address the host through `window.craft`. Keep native screens
|
|
448
448
|
// compatible without pretending a document exists.
|
|
449
449
|
if (typeof globalThis.window === 'undefined') globalThis.window = globalThis;
|
|
450
|
+
// JavaScriptSandbox intentionally has no browser URL/encoding
|
|
451
|
+
// globals. Install named failures instead of letting a shared module
|
|
452
|
+
// die with an unhelpful `URLSearchParams is not defined` halfway
|
|
453
|
+
// through its load path. The full browser implementations belong to
|
|
454
|
+
// the shared STX runtime; these diagnostics keep the native boundary
|
|
455
|
+
// explicit until that runtime is present.
|
|
456
|
+
(function() {
|
|
457
|
+
function unavailable(name, hint) {
|
|
458
|
+
var failure = function() {
|
|
459
|
+
throw new ReferenceError(name + ' is not available on a native screen — ' + hint);
|
|
460
|
+
};
|
|
461
|
+
failure.prototype.constructor = failure;
|
|
462
|
+
return failure;
|
|
463
|
+
}
|
|
464
|
+
if (typeof globalThis.URL === 'undefined') globalThis.URL = unavailable('URL', 'use an absolute URL or build the path with encodeURIComponent');
|
|
465
|
+
if (typeof globalThis.URLSearchParams === 'undefined') globalThis.URLSearchParams = unavailable('URLSearchParams', 'build query strings with encodeURIComponent');
|
|
466
|
+
if (typeof globalThis.TextEncoder === 'undefined') globalThis.TextEncoder = unavailable('TextEncoder', 'use a native-supported encoding helper');
|
|
467
|
+
})();
|
|
450
468
|
(function() {
|
|
451
469
|
var forward = null;
|
|
452
470
|
var listeners = [];
|
|
@@ -671,9 +689,11 @@ internal object CraftNativePrelude {
|
|
|
671
689
|
// Older native bundles call the page-shaped `craft.dialog` API
|
|
672
690
|
// directly. Keep that API on the capability bridge so JavaScript
|
|
673
691
|
// Core/JavaScriptSandbox screens never fall back to a DOM sheet.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
692
|
+
// Native capability ownership wins over any browser-shaped dialog
|
|
693
|
+
// object the bundle installed before the bridge ran. Otherwise an
|
|
694
|
+
// action sheet silently falls back to the DOM renderer.
|
|
695
|
+
var nextDialogRequest = 0;
|
|
696
|
+
function dialogRequest(method, args) {
|
|
677
697
|
return new Promise(function(resolve, reject) {
|
|
678
698
|
var id = 'craft-dialog-' + (++nextDialogRequest);
|
|
679
699
|
var remove = __stxNativeBridge.addMessageListener(function(raw) {
|
|
@@ -702,13 +722,19 @@ internal object CraftNativePrelude {
|
|
|
702
722
|
},
|
|
703
723
|
}));
|
|
704
724
|
});
|
|
725
|
+
}
|
|
726
|
+
function installNativeDialog() {
|
|
727
|
+
var craft = globalThis.craft = globalThis.craft || {};
|
|
728
|
+
if (Array.isArray(__stxNativeBridge.capabilities) && __stxNativeBridge.capabilities.indexOf('dialog') >= 0) {
|
|
729
|
+
craft.dialog = {
|
|
730
|
+
alert: function(options) { return dialogRequest('alert', [options || {}]).then(function() {}); },
|
|
731
|
+
confirm: function(options) { return dialogRequest('confirm', [options || {}]).then(Boolean); },
|
|
732
|
+
actionSheet: function(options) { return dialogRequest('actionSheet', [options || {}]).then(function(id) { return typeof id === 'string' ? id : null; }); },
|
|
733
|
+
};
|
|
705
734
|
}
|
|
706
|
-
craft.dialog = {
|
|
707
|
-
alert: function(options) { return dialogRequest('alert', [options || {}]).then(function() {}); },
|
|
708
|
-
confirm: function(options) { return dialogRequest('confirm', [options || {}]).then(Boolean); },
|
|
709
|
-
actionSheet: function(options) { return dialogRequest('actionSheet', [options || {}]).then(function(id) { return typeof id === 'string' ? id : null; }); },
|
|
710
|
-
};
|
|
711
735
|
}
|
|
736
|
+
globalThis.__craftInstallNativeDialog = installNativeDialog;
|
|
737
|
+
installNativeDialog();
|
|
712
738
|
})();
|
|
713
739
|
""".trimIndent()
|
|
714
740
|
}
|
|
@@ -740,6 +766,8 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
740
766
|
private val tabIconTints = mutableMapOf<String, Int>()
|
|
741
767
|
private val tabIconPulses = mutableSetOf<String>()
|
|
742
768
|
private val selectedSymbols = mutableMapOf<String, String>()
|
|
769
|
+
/** The page's own symbol for a tab while it says so, in place of the configured one. */
|
|
770
|
+
private val tabSymbols = mutableMapOf<String, String>()
|
|
743
771
|
private val selectedIconTints = mutableMapOf<String, Int>()
|
|
744
772
|
private val selectedIconPulses = mutableMapOf<String, Boolean>()
|
|
745
773
|
private val tabImageURLs = mutableMapOf<String, String>()
|
|
@@ -974,6 +1002,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
974
1002
|
if (item.has("iconPulse")) {
|
|
975
1003
|
if (item.optBoolean("iconPulse", false)) tabIconPulses.add(id) else tabIconPulses.remove(id)
|
|
976
1004
|
}
|
|
1005
|
+
if (item.has("symbol")) {
|
|
1006
|
+
tabSymbols.remove(id)
|
|
1007
|
+
item.optString("symbol").trim().takeUnless { item.isNull("symbol") || it.isBlank() }?.let { tabSymbols[id] = it }
|
|
1008
|
+
}
|
|
977
1009
|
if (item.has("selectedSymbol")) {
|
|
978
1010
|
selectedSymbols.remove(id)
|
|
979
1011
|
item.optString("selectedSymbol").trim().takeUnless { item.isNull("selectedSymbol") || it.isBlank() }?.let { selectedSymbols[id] = it }
|
|
@@ -1016,7 +1048,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
1016
1048
|
val tab = tabs[index]
|
|
1017
1049
|
val selected = index == selectedTab
|
|
1018
1050
|
return CraftTabIconAppearance(
|
|
1019
|
-
if (selected) selectedSymbols[tab.id] ?: tab.symbol else tab.symbol,
|
|
1051
|
+
if (selected) selectedSymbols[tab.id] ?: tabSymbols[tab.id] ?: tab.symbol else tabSymbols[tab.id] ?: tab.symbol,
|
|
1020
1052
|
if (selected) selectedIconTints[tab.id] ?: tabIconTints[tab.id] else tabIconTints[tab.id],
|
|
1021
1053
|
(if (selected) selectedIconPulses[tab.id] ?: tabIconPulses.contains(tab.id) else tabIconPulses.contains(tab.id)) && motionAllowed,
|
|
1022
1054
|
)
|