craft-native 0.0.238 → 0.0.242
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 +23 -1
- package/dist/android/src/index.js +102 -9
- package/dist/android/src/native-map.d.ts +7 -0
- package/dist/android/templates/CraftBridge.kt.template +2 -1
- package/dist/android/templates/CraftNativeBackground.kt.template +28 -2
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +440 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +438 -0
- package/dist/android/templates/CraftNativeMessagingService.kt.template +3 -1
- package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +2 -1
- package/dist/android/templates/LocationRecordingService.kt.template +3 -1
- package/dist/android/templates/MainActivityNative.kt.template +14 -433
- package/dist/android/templates/build.gradle.kts.app.template +1 -1
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +12 -1
- package/dist/ios/src/index.js +32 -7
- package/dist/ios/src/testing.d.ts +14 -2
- package/dist/ios/templates/CraftApp.swift +117 -29
- package/dist/ios/templates/CraftHybrid.swift +193 -29
- package/dist/ios/templates/CraftNativeActions.swift +26 -27
- package/dist/ios/templates/CraftNativeFiles.swift +205 -1
- package/dist/ios/templates/CraftNativeScreen.swift +230 -33
- package/dist/ios/templates/testing/DriverTests.swift.template +37 -0
- package/dist/macos/templates/native_layout.zig +41 -5
- package/package.json +1 -1
|
@@ -110,17 +110,7 @@ import androidx.javascriptengine.Message
|
|
|
110
110
|
import androidx.javascriptengine.MessagePort
|
|
111
111
|
import androidx.javascriptengine.MessagePortClient
|
|
112
112
|
import com.craft.runtime.CraftNative
|
|
113
|
-
|
|
114
|
-
import com.google.android.gms.maps.GoogleMap
|
|
115
|
-
import com.google.android.gms.maps.MapView
|
|
116
|
-
import com.google.android.gms.maps.OnMapReadyCallback
|
|
117
|
-
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
118
|
-
import com.google.android.gms.maps.model.LatLng
|
|
119
|
-
import com.google.android.gms.maps.model.LatLngBounds
|
|
120
|
-
import com.google.android.gms.maps.model.Marker
|
|
121
|
-
import com.google.android.gms.maps.model.MarkerOptions
|
|
122
|
-
import com.google.android.gms.maps.model.Polyline
|
|
123
|
-
import com.google.android.gms.maps.model.PolylineOptions
|
|
113
|
+
{{NATIVE_MAP_IMPORTS}}
|
|
124
114
|
import org.json.JSONArray
|
|
125
115
|
import org.json.JSONObject
|
|
126
116
|
import java.io.File
|
|
@@ -597,422 +587,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
597
587
|
private data class Quadruple<T1, T2, T3, T4>(val first: T1, val second: T2, val third: T3, val fourth: T4)
|
|
598
588
|
}
|
|
599
589
|
|
|
600
|
-
|
|
601
|
-
private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
|
|
602
|
-
private val mapView = MapView(context)
|
|
603
|
-
private val scaleView = LinearLayout(context).apply {
|
|
604
|
-
orientation = LinearLayout.VERTICAL
|
|
605
|
-
setPadding(4, 2, 4, 2)
|
|
606
|
-
background = GradientDrawable().apply { setColor(Color.argb(230, 255, 255, 255)); cornerRadius = 4f }
|
|
607
|
-
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
608
|
-
}
|
|
609
|
-
private val scaleLabel = TextView(context).apply { textSize = 11f; setTextColor(Color.rgb(37, 51, 33)) }
|
|
610
|
-
private val scaleLine = View(context).apply { setBackgroundColor(Color.rgb(37, 51, 33)) }
|
|
611
|
-
private var googleMap: GoogleMap? = null
|
|
612
|
-
private var tileOverlay: com.google.android.gms.maps.model.TileOverlay? = null
|
|
613
|
-
private var tileTemplate: String? = null
|
|
614
|
-
private val tileAttribution = TextView(context).apply { textSize = 9f; setTextColor(Color.DKGRAY); setBackgroundColor(Color.argb(220, 255, 255, 255)); maxLines = 2; visibility = View.GONE }
|
|
615
|
-
private var pendingProps = JSONObject()
|
|
616
|
-
private var pendingChildren = JSONArray()
|
|
617
|
-
private val markers = mutableMapOf<String, Marker>()
|
|
618
|
-
private val polylines = mutableMapOf<String, Polyline>()
|
|
619
|
-
private val markerHandlers = mutableMapOf<String, String>()
|
|
620
|
-
private var onMarkerTap: ((String) -> Unit)? = null
|
|
621
|
-
var onMapPress: ((LatLng) -> Unit)? = null
|
|
622
|
-
var onRegionChangeComplete: ((JSONObject) -> Unit)? = null
|
|
623
|
-
private var userChangingCamera = false
|
|
624
|
-
private var markerBearings = mutableMapOf<String, Float>()
|
|
625
|
-
private var directionIcon: com.google.android.gms.maps.model.BitmapDescriptor? = null
|
|
626
|
-
private var lifecycleStarted = false
|
|
627
|
-
private var destroyed = false
|
|
628
|
-
private var initialCameraKey: String? = null
|
|
629
|
-
|
|
630
|
-
init {
|
|
631
|
-
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
632
|
-
scaleView.addView(scaleLabel)
|
|
633
|
-
scaleView.addView(scaleLine, LinearLayout.LayoutParams(1, 2))
|
|
634
|
-
addView(scaleView, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.START).apply {
|
|
635
|
-
leftMargin = (8 * resources.displayMetrics.density).roundToInt()
|
|
636
|
-
bottomMargin = (24 * resources.displayMetrics.density).roundToInt()
|
|
637
|
-
})
|
|
638
|
-
scaleView.visibility = View.GONE
|
|
639
|
-
addView(tileAttribution, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.END).apply { rightMargin = (8 * resources.displayMetrics.density).roundToInt(); bottomMargin = (8 * resources.displayMetrics.density).roundToInt() })
|
|
640
|
-
mapView.onCreate(null)
|
|
641
|
-
mapView.getMapAsync(this)
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
override fun onMapReady(map: GoogleMap) {
|
|
645
|
-
if (destroyed) return
|
|
646
|
-
googleMap = map
|
|
647
|
-
map.setOnMapClickListener { point -> onMapPress?.invoke(point) }
|
|
648
|
-
map.setOnCameraMoveStartedListener { reason ->
|
|
649
|
-
if (reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE) userChangingCamera = true
|
|
650
|
-
}
|
|
651
|
-
map.setOnCameraMoveListener { updateScale() }
|
|
652
|
-
map.setOnCameraIdleListener {
|
|
653
|
-
updateScale()
|
|
654
|
-
if (userChangingCamera) {
|
|
655
|
-
userChangingCamera = false
|
|
656
|
-
val bounds = map.projection.visibleRegion.latLngBounds
|
|
657
|
-
val position = map.cameraPosition
|
|
658
|
-
val longitudeDelta = (bounds.northeast.longitude - bounds.southwest.longitude + 360) % 360
|
|
659
|
-
onRegionChangeComplete?.invoke(JSONObject()
|
|
660
|
-
.put("center", JSONObject().put("latitude", position.target.latitude).put("longitude", position.target.longitude))
|
|
661
|
-
.put("span", JSONObject().put("latitudeDelta", bounds.northeast.latitude - bounds.southwest.latitude).put("longitudeDelta", longitudeDelta))
|
|
662
|
-
.put("heading", position.bearing).put("pitch", position.tilt))
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
map.setOnMarkerClickListener { marker ->
|
|
666
|
-
val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
|
|
667
|
-
onMarkerTap?.invoke(id)
|
|
668
|
-
// The native contract dispatches the marker event itself. Consume
|
|
669
|
-
// the click so Google Maps does not also show an unsolicited info
|
|
670
|
-
// window for the marker title.
|
|
671
|
-
true
|
|
672
|
-
}
|
|
673
|
-
applyPending()
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
fun configure(props: JSONObject, children: JSONArray, markerHandler: (String) -> Unit) {
|
|
677
|
-
pendingProps = props
|
|
678
|
-
pendingChildren = children
|
|
679
|
-
onMarkerTap = markerHandler
|
|
680
|
-
applyPending()
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
fun markerHandler(id: String): String? = markerHandlers[id]
|
|
684
|
-
|
|
685
|
-
private fun applyPending() {
|
|
686
|
-
val map = googleMap ?: return
|
|
687
|
-
applyMapPadding()
|
|
688
|
-
map.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
|
|
689
|
-
// The toolbar is replaced by authored actions; the scale uses the native projection.
|
|
690
|
-
map.uiSettings.isMapToolbarEnabled = false
|
|
691
|
-
map.uiSettings.isScrollGesturesEnabled = craftNativeBoolean(pendingProps.opt("scrollEnabled"), true)
|
|
692
|
-
map.uiSettings.isZoomGesturesEnabled = craftNativeBoolean(pendingProps.opt("zoomEnabled"), true)
|
|
693
|
-
map.uiSettings.isRotateGesturesEnabled = craftNativeBoolean(pendingProps.opt("rotateEnabled"))
|
|
694
|
-
map.mapType = when (craftNativeOptionalString(pendingProps.opt("mapType")).orEmpty().lowercase()) {
|
|
695
|
-
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
696
|
-
"hybrid" -> GoogleMap.MAP_TYPE_HYBRID
|
|
697
|
-
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
698
|
-
else -> GoogleMap.MAP_TYPE_NORMAL
|
|
699
|
-
}
|
|
700
|
-
applyTileOverlay()
|
|
701
|
-
val showUserLocation = craftNativeBoolean(pendingProps.opt("showsUserLocation")) && hasLocationPermission()
|
|
702
|
-
runCatching { map.isMyLocationEnabled = showUserLocation }
|
|
703
|
-
val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
|
|
704
|
-
val nextBearings = mutableMapOf<String, Float>()
|
|
705
|
-
val nextHandlers = mutableMapOf<String, String>()
|
|
706
|
-
val nextPolylines = mutableMapOf<String, Triple<List<LatLng>, Int, Float>>()
|
|
707
|
-
for (index in 0 until pendingChildren.length()) {
|
|
708
|
-
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
709
|
-
val type = child.optString("type").lowercase()
|
|
710
|
-
val props = child.optJSONObject("props") ?: JSONObject()
|
|
711
|
-
val events = child.optJSONObject("events") ?: JSONObject()
|
|
712
|
-
val id = craftNativeOptionalString(props.opt("id"))
|
|
713
|
-
?: craftNativeOptionalString(child.opt("id"))
|
|
714
|
-
?: "overlay-$index"
|
|
715
|
-
val points = coordinates(props.optJSONArray("geometry"))
|
|
716
|
-
if (type == "mapmarker" || type == "marker") {
|
|
717
|
-
points.firstOrNull()?.let { point ->
|
|
718
|
-
nextMarkers[id] = point to (props.opt("title") as? String)
|
|
719
|
-
(props.opt("bearing") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.let { nextBearings[id] = (it % 360).toFloat() }
|
|
720
|
-
listOf(events.opt("onPress"), events.opt("onMarkerPress"))
|
|
721
|
-
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
722
|
-
?.let { nextHandlers[id] = it }
|
|
723
|
-
}
|
|
724
|
-
} else if (type == "mappolyline" || type == "polyline") {
|
|
725
|
-
if (points.size > 1) {
|
|
726
|
-
// Keep malformed authored widths inert, matching the iOS
|
|
727
|
-
// host. An omitted or null width still gets the shared
|
|
728
|
-
// default; an attacker-controlled NaN, infinity, zero,
|
|
729
|
-
// or negative value must not become a valid overlay.
|
|
730
|
-
val rawWidth = props.opt("strokeWidth")
|
|
731
|
-
val authoredWidth = when (rawWidth) {
|
|
732
|
-
null, JSONObject.NULL -> 3.0
|
|
733
|
-
is Number -> rawWidth.toDouble()
|
|
734
|
-
else -> 3.0
|
|
735
|
-
}
|
|
736
|
-
// MapKit's lineWidth is expressed in points, while the
|
|
737
|
-
// Google Maps SDK consumes screen pixels. Keep the
|
|
738
|
-
// protocol in logical points and convert only at this
|
|
739
|
-
// Android host boundary so dense displays stay in parity.
|
|
740
|
-
val width = (authoredWidth * resources.displayMetrics.density).toFloat()
|
|
741
|
-
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
742
|
-
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
markerBearings = nextBearings
|
|
747
|
-
markers.keys.filter { nextMarkers[it] == null }.forEach { markers.remove(it)?.remove() }
|
|
748
|
-
for ((id, value) in nextMarkers) {
|
|
749
|
-
val marker = markers[id]
|
|
750
|
-
if (marker != null) {
|
|
751
|
-
marker.position = value.first
|
|
752
|
-
marker.title = value.second
|
|
753
|
-
marker.setIcon(if (markerBearings.containsKey(id)) directionMarkerIcon() else BitmapDescriptorFactory.defaultMarker())
|
|
754
|
-
marker.isFlat = markerBearings.containsKey(id)
|
|
755
|
-
marker.rotation = markerBearings[id] ?: 0f
|
|
756
|
-
marker.setAnchor(0.5f, if (markerBearings.containsKey(id)) 0.5f else 1f)
|
|
757
|
-
} else {
|
|
758
|
-
map.addMarker(MarkerOptions().position(value.first).title(value.second).icon(if (markerBearings.containsKey(id)) directionMarkerIcon() else BitmapDescriptorFactory.defaultMarker()).flat(markerBearings.containsKey(id)).rotation(markerBearings[id] ?: 0f).anchor(0.5f, if (markerBearings.containsKey(id)) 0.5f else 1f))?.let { markers[id] = it }
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
markerHandlers.clear()
|
|
762
|
-
markerHandlers.putAll(nextHandlers)
|
|
763
|
-
polylines.keys.filter { nextPolylines[it] == null }.forEach { id ->
|
|
764
|
-
polylines.remove(id)?.remove()
|
|
765
|
-
}
|
|
766
|
-
for ((id, value) in nextPolylines) {
|
|
767
|
-
val polyline = polylines[id]
|
|
768
|
-
if (polyline != null) {
|
|
769
|
-
polyline.points = value.first
|
|
770
|
-
polyline.color = value.second
|
|
771
|
-
polyline.width = value.third
|
|
772
|
-
} else {
|
|
773
|
-
map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))?.let { polylines[id] = it }
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
777
|
-
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
778
|
-
if (explicitRegion != null) {
|
|
779
|
-
// A controlled region is an exact camera request. The padding
|
|
780
|
-
// used for fitToBounds is framing chrome, not part of the region
|
|
781
|
-
// contract, and would otherwise zoom every Android screen out.
|
|
782
|
-
moveCamera(explicitRegion, padding = 0)
|
|
783
|
-
initialCameraKey = null
|
|
784
|
-
} else if (initialRegion != null) {
|
|
785
|
-
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
786
|
-
if (initialCameraKey != key) {
|
|
787
|
-
moveCamera(initialRegion, padding = 0)
|
|
788
|
-
initialCameraKey = key
|
|
789
|
-
}
|
|
790
|
-
} else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
|
|
791
|
-
val key = "fit:${overlayKey(nextMarkers, nextPolylines)}:${pendingProps.optJSONObject("edgePadding")}"
|
|
792
|
-
if (initialCameraKey != key) {
|
|
793
|
-
if (fitToBounds()) initialCameraKey = key
|
|
794
|
-
}
|
|
795
|
-
} else {
|
|
796
|
-
initialCameraKey = null
|
|
797
|
-
}
|
|
798
|
-
applyCameraOrientation()
|
|
799
|
-
updateScale()
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
private fun applyTileOverlay() {
|
|
803
|
-
val map = googleMap ?: return
|
|
804
|
-
val options = pendingProps.optJSONObject("tileOverlay")
|
|
805
|
-
val template = options?.optString("urlTemplate").orEmpty()
|
|
806
|
-
val credit = options?.optString("attribution").orEmpty()
|
|
807
|
-
val next = template.takeIf { it.startsWith("https://") && listOf("{z}", "{x}", "{y}").all { key -> it.contains(key) } && credit.isNotBlank() }
|
|
808
|
-
if (next != tileTemplate) {
|
|
809
|
-
tileOverlay?.remove(); tileOverlay = null; tileTemplate = next
|
|
810
|
-
if (next != null) {
|
|
811
|
-
val provider = object : com.google.android.gms.maps.model.UrlTileProvider(256, 256) {
|
|
812
|
-
override fun getTileUrl(x: Int, y: Int, zoom: Int): java.net.URL? = runCatching { java.net.URL(next.replace("{x}", x.toString()).replace("{y}", y.toString()).replace("{z}", zoom.toString())) }.getOrNull()
|
|
813
|
-
}
|
|
814
|
-
tileOverlay = map.addTileOverlay(com.google.android.gms.maps.model.TileOverlayOptions().tileProvider(provider).zIndex(-1f))
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
if (next != null) map.mapType = GoogleMap.MAP_TYPE_NONE
|
|
818
|
-
tileAttribution.text = credit
|
|
819
|
-
tileAttribution.visibility = if (next == null) View.GONE else View.VISIBLE
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
private fun applyMapPadding() {
|
|
823
|
-
val authored = pendingProps.optJSONObject("edgePadding")
|
|
824
|
-
fun inset(name: String): Double = (authored?.opt(name) as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, 10_000.0)?.times(resources.displayMetrics.density) ?: 0.0
|
|
825
|
-
var left = inset("left"); var top = inset("top"); var right = inset("right"); var bottom = inset("bottom")
|
|
826
|
-
val horizontal = left + right
|
|
827
|
-
val vertical = top + bottom
|
|
828
|
-
val availableWidth = (width - 40 * resources.displayMetrics.density).coerceAtLeast(0f).toDouble()
|
|
829
|
-
val availableHeight = (height - 40 * resources.displayMetrics.density).coerceAtLeast(0f).toDouble()
|
|
830
|
-
if (horizontal > availableWidth && horizontal > 0) { left *= availableWidth / horizontal; right *= availableWidth / horizontal }
|
|
831
|
-
if (vertical > availableHeight && vertical > 0) { top *= availableHeight / vertical; bottom *= availableHeight / vertical }
|
|
832
|
-
googleMap?.setPadding(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt())
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
private fun updateScale() {
|
|
836
|
-
val map = googleMap ?: return
|
|
837
|
-
if (!craftNativeBoolean(pendingProps.opt("showsScale")) || width <= 0 || height <= 0) { scaleView.visibility = View.GONE; return }
|
|
838
|
-
val maximumWidth = (80 * resources.displayMetrics.density).roundToInt()
|
|
839
|
-
val origin = map.projection.fromScreenLocation(android.graphics.Point(width / 2, height / 2))
|
|
840
|
-
val end = map.projection.fromScreenLocation(android.graphics.Point(width / 2 + maximumWidth, height / 2))
|
|
841
|
-
val distance = FloatArray(1)
|
|
842
|
-
android.location.Location.distanceBetween(origin.latitude, origin.longitude, end.latitude, end.longitude, distance)
|
|
843
|
-
val metres = distance[0].toDouble()
|
|
844
|
-
if (!metres.isFinite() || metres <= 0) { scaleView.visibility = View.GONE; return }
|
|
845
|
-
val magnitude = Math.pow(10.0, kotlin.math.floor(kotlin.math.log10(metres)))
|
|
846
|
-
val length = listOf(5.0, 2.0, 1.0).firstOrNull { it * magnitude <= metres }?.times(magnitude) ?: magnitude
|
|
847
|
-
scaleLabel.text = if (length >= 1000) "${length / 1000} km" else "${length.roundToInt()} m"
|
|
848
|
-
scaleLine.layoutParams = LinearLayout.LayoutParams((maximumWidth * length / metres).roundToInt().coerceAtLeast(1), 2)
|
|
849
|
-
scaleView.visibility = View.VISIBLE
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
private fun applyCameraOrientation() {
|
|
853
|
-
val map = googleMap ?: return
|
|
854
|
-
val camera = map.cameraPosition
|
|
855
|
-
val pitch = (pendingProps.opt("cameraPitch") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, 60.0)?.toFloat() ?: camera.tilt
|
|
856
|
-
val heading = (pendingProps.opt("cameraHeading") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.toFloat() ?: camera.bearing
|
|
857
|
-
if (kotlin.math.abs(camera.tilt - pitch) > 0.01f || kotlin.math.abs(camera.bearing - heading) > 0.01f) {
|
|
858
|
-
map.moveCamera(CameraUpdateFactory.newCameraPosition(com.google.android.gms.maps.model.CameraPosition.Builder(camera).tilt(pitch).bearing(heading).build()))
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
private fun directionMarkerIcon(): com.google.android.gms.maps.model.BitmapDescriptor {
|
|
863
|
-
directionIcon?.let { return it }
|
|
864
|
-
val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
|
|
865
|
-
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
866
|
-
val canvas = Canvas(bitmap)
|
|
867
|
-
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
868
|
-
paint.color = Color.WHITE
|
|
869
|
-
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
|
|
870
|
-
paint.color = Color.rgb(36, 114, 75)
|
|
871
|
-
canvas.drawCircle(size / 2f, size / 2f, size * 0.43f, paint)
|
|
872
|
-
paint.color = Color.WHITE
|
|
873
|
-
paint.textSize = size * 0.76f
|
|
874
|
-
paint.textAlign = Paint.Align.CENTER
|
|
875
|
-
paint.typeface = Typeface.DEFAULT_BOLD
|
|
876
|
-
canvas.drawText("↑", size / 2f, size / 2f - (paint.ascent() + paint.descent()) / 2, paint)
|
|
877
|
-
return BitmapDescriptorFactory.fromBitmap(bitmap).also { directionIcon = it }
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
|
881
|
-
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
|
882
|
-
applyMapPadding()
|
|
883
|
-
updateScale()
|
|
884
|
-
// MapView can become ready before its host has a usable viewport. Do
|
|
885
|
-
// not mark a fit as complete until the camera update can be issued.
|
|
886
|
-
if (width > 0 && height > 0 && craftNativeBoolean(pendingProps.opt("fitToBounds")) && initialCameraKey == null) {
|
|
887
|
-
applyPending()
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
private fun moveCamera(bounds: LatLngBounds, padding: Int = 24) {
|
|
892
|
-
post {
|
|
893
|
-
if (width > 0 && height > 0) {
|
|
894
|
-
googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
|
|
895
|
-
applyCameraOrientation()
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
private fun overlayKey(
|
|
901
|
-
markers: Map<String, Pair<LatLng, String?>>,
|
|
902
|
-
polylines: Map<String, Triple<List<LatLng>, Int, Float>>,
|
|
903
|
-
): String {
|
|
904
|
-
val markerKey = markers.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
905
|
-
"$id:${value.first.latitude},${value.first.longitude}"
|
|
906
|
-
}
|
|
907
|
-
val polylineKey = polylines.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
908
|
-
"$id:" + value.first.joinToString(";") { point -> "${point.latitude},${point.longitude}" }
|
|
909
|
-
}
|
|
910
|
-
return "m[$markerKey]p[$polylineKey]"
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
private fun fitToBounds(): Boolean {
|
|
914
|
-
val map = googleMap ?: return false
|
|
915
|
-
if (width <= 0 || height <= 0) return false
|
|
916
|
-
val builder = LatLngBounds.Builder()
|
|
917
|
-
var count = 0
|
|
918
|
-
markers.values.forEach { builder.include(it.position); count++ }
|
|
919
|
-
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
920
|
-
if (count == 0) return false
|
|
921
|
-
post {
|
|
922
|
-
if (width > 0 && height > 0) {
|
|
923
|
-
applyMapPadding()
|
|
924
|
-
val padding = if (pendingProps.has("edgePadding")) 0 else (24 * resources.displayMetrics.density).roundToInt()
|
|
925
|
-
map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), padding))
|
|
926
|
-
applyCameraOrientation()
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
return true
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
override fun onAttachedToWindow() {
|
|
933
|
-
super.onAttachedToWindow()
|
|
934
|
-
if (destroyed) return
|
|
935
|
-
mapView.onStart()
|
|
936
|
-
mapView.onResume()
|
|
937
|
-
lifecycleStarted = true
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
override fun onDetachedFromWindow() {
|
|
941
|
-
if (lifecycleStarted) {
|
|
942
|
-
mapView.onPause()
|
|
943
|
-
mapView.onStop()
|
|
944
|
-
lifecycleStarted = false
|
|
945
|
-
}
|
|
946
|
-
super.onDetachedFromWindow()
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
fun destroy() {
|
|
950
|
-
if (destroyed) return
|
|
951
|
-
destroyed = true
|
|
952
|
-
if (lifecycleStarted) {
|
|
953
|
-
mapView.onPause()
|
|
954
|
-
mapView.onStop()
|
|
955
|
-
lifecycleStarted = false
|
|
956
|
-
}
|
|
957
|
-
onMarkerTap = null
|
|
958
|
-
onMapPress = null
|
|
959
|
-
onRegionChangeComplete = null
|
|
960
|
-
directionIcon = null
|
|
961
|
-
tileOverlay?.remove(); tileOverlay = null
|
|
962
|
-
markerHandlers.clear()
|
|
963
|
-
markers.values.forEach { it.remove() }
|
|
964
|
-
markers.clear()
|
|
965
|
-
polylines.values.forEach { it.remove() }
|
|
966
|
-
polylines.clear()
|
|
967
|
-
mapView.onDestroy()
|
|
968
|
-
googleMap = null
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
fun lowMemory() {
|
|
972
|
-
if (!destroyed) mapView.onLowMemory()
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private fun hasLocationPermission(): Boolean =
|
|
976
|
-
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
977
|
-
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
978
|
-
|
|
979
|
-
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
980
|
-
if (geometry == null) return emptyList()
|
|
981
|
-
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
982
|
-
val latitude = finiteNumber(geometry.opt(index)) ?: return@mapNotNull null
|
|
983
|
-
val longitude = finiteNumber(geometry.opt(index + 1)) ?: return@mapNotNull null
|
|
984
|
-
if (latitude.isFinite() && longitude.isFinite() &&
|
|
985
|
-
latitude >= -90.0 && latitude <= 90.0 &&
|
|
986
|
-
longitude >= -180.0 && longitude <= 180.0
|
|
987
|
-
) LatLng(latitude, longitude) else null
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
private fun region(value: JSONObject?): LatLngBounds? {
|
|
992
|
-
val center = value?.optJSONObject("center") ?: return null
|
|
993
|
-
val span = value.optJSONObject("span") ?: return null
|
|
994
|
-
val latitude = finiteNumber(center.opt("latitude")) ?: return null
|
|
995
|
-
val longitude = finiteNumber(center.opt("longitude")) ?: return null
|
|
996
|
-
val latitudeDelta = finiteNumber(span.opt("latitudeDelta"))
|
|
997
|
-
?: finiteNumber(span.opt("latitude_delta"))
|
|
998
|
-
?: return null
|
|
999
|
-
val longitudeDelta = finiteNumber(span.opt("longitudeDelta"))
|
|
1000
|
-
?: finiteNumber(span.opt("longitude_delta"))
|
|
1001
|
-
?: return null
|
|
1002
|
-
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite() ||
|
|
1003
|
-
latitude < -90.0 || latitude > 90.0 || longitude < -180.0 || longitude > 180.0 ||
|
|
1004
|
-
latitudeDelta <= 0.0 || latitudeDelta > 180.0 ||
|
|
1005
|
-
longitudeDelta <= 0.0 || longitudeDelta > 360.0 ||
|
|
1006
|
-
latitude - latitudeDelta / 2.0 < -90.0 || latitude + latitudeDelta / 2.0 > 90.0
|
|
1007
|
-
) return null
|
|
1008
|
-
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
private fun finiteNumber(value: Any?): Double? =
|
|
1012
|
-
(value as? Number)?.toDouble()?.takeIf { it.isFinite() }
|
|
1013
|
-
|
|
1014
|
-
private fun color(value: String): Int = runCatching { craftNativeColor(value) }.getOrDefault(Color.BLUE)
|
|
1015
|
-
}
|
|
590
|
+
{{NATIVE_MAP_HOST}}
|
|
1016
591
|
|
|
1017
592
|
/** A finite celebration owned by a View, never by the JavaScript frame loop. */
|
|
1018
593
|
internal class CraftNativeCelebration(private val view: View) {
|
|
@@ -1882,7 +1457,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1882
1457
|
putEnum(base, 16, if (root) sharedAlignItemsCode(alignItems) else 3)
|
|
1883
1458
|
putEnum(base, 17, if (root) 0 else sharedAlignSelfCode(style.alignSelf))
|
|
1884
1459
|
putEnum(base, 18, if (root && wrap) 1 else 0)
|
|
1885
|
-
putEnum(base, 19, if (root) alignContentCode() else
|
|
1460
|
+
putEnum(base, 19, if (root) alignContentCode() else 5)
|
|
1886
1461
|
put(base, 20, margins[2].toFloat())
|
|
1887
1462
|
put(base, 21, margins[1].toFloat())
|
|
1888
1463
|
put(base, 22, margins[3].toFloat())
|
|
@@ -4108,7 +3683,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
4108
3683
|
private val backgroundWaiters = mutableMapOf<String, (Boolean) -> Unit>()
|
|
4109
3684
|
/** A tapped notification that opened the app, for the app script once it runs. */
|
|
4110
3685
|
private var pendingNotificationTap: JSONObject? = null
|
|
4111
|
-
private var bundle = ""
|
|
4112
3686
|
private lateinit var config: JSONObject
|
|
4113
3687
|
private lateinit var capabilities: CraftNativeCapabilities
|
|
4114
3688
|
private val capabilityHandler = Handler(Looper.getMainLooper())
|
|
@@ -4219,9 +3793,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4219
3793
|
for (index in 0 until schemes.length()) urlSchemes.add(schemes.optString(index).lowercase())
|
|
4220
3794
|
if (!routes.isEmpty) installChrome()
|
|
4221
3795
|
refreshThemeDefaults()
|
|
4222
|
-
|
|
4223
|
-
assets.open("native-screen.js").bufferedReader().use { it.readText() }
|
|
4224
|
-
}.getOrElse {
|
|
3796
|
+
if (!CraftNativeBundle.isAvailable(this)) {
|
|
4225
3797
|
showError("Missing native-screen.js. Build with --native-bundle after compiling STX.")
|
|
4226
3798
|
return
|
|
4227
3799
|
}
|
|
@@ -4787,7 +4359,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
4787
4359
|
if (!activeScreen.active) return@addListener
|
|
4788
4360
|
try {
|
|
4789
4361
|
setup.get()
|
|
4790
|
-
val
|
|
4362
|
+
val source = CraftNativeBundle.source(this, route)
|
|
4363
|
+
if (source == null) {
|
|
4364
|
+
if (activeScreen.active) showError("Missing native-screen.js. Build with --native-bundle after compiling STX.")
|
|
4365
|
+
return@addListener
|
|
4366
|
+
}
|
|
4367
|
+
val load = isolate.evaluateJavaScriptAsync(source)
|
|
4791
4368
|
load.addListener({
|
|
4792
4369
|
try {
|
|
4793
4370
|
load.get()
|
|
@@ -7000,6 +6577,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7000
6577
|
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
7001
6578
|
?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" }
|
|
7002
6579
|
val value = craftNativeOptionalString(props.opt("accessibilityValue"))
|
|
6580
|
+
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
7003
6581
|
view.contentDescription = when {
|
|
7004
6582
|
label != null && value != null -> "$label, $value"
|
|
7005
6583
|
label != null -> label
|
|
@@ -7024,6 +6602,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
7024
6602
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
7025
6603
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
7026
6604
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
6605
|
+
// Expose STX test identities to Android's native automation
|
|
6606
|
+
// tree without replacing the label read by TalkBack.
|
|
6607
|
+
info.viewIdResourceName = testId
|
|
7027
6608
|
info.className = when (role) {
|
|
7028
6609
|
"button" -> Button::class.java.name
|
|
7029
6610
|
"image" -> ImageView::class.java.name
|
|
@@ -99,7 +99,7 @@ dependencies {
|
|
|
99
99
|
// Google Play Location Services (for geolocation)
|
|
100
100
|
implementation("com.google.android.gms:play-services-location:21.1.0")
|
|
101
101
|
|
|
102
|
-
{{
|
|
102
|
+
{{NATIVE_MAP_DEPENDENCY}}
|
|
103
103
|
|
|
104
104
|
// Google Play In-App Review
|
|
105
105
|
implementation("com.google.android.play:review:2.0.1")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
2
2
|
plugins {
|
|
3
3
|
id("com.android.application") version "8.9.1" apply false
|
|
4
|
-
id("org.jetbrains.kotlin.android") version "2.
|
|
4
|
+
id("org.jetbrains.kotlin.android") version "2.2.10" apply false
|
|
5
5
|
{{GOOGLE_SERVICES_PLUGIN}}
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.242";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -223,7 +223,12 @@ export interface CraftConfig {
|
|
|
223
223
|
* opens in the in-app browser on the app's site.
|
|
224
224
|
*/
|
|
225
225
|
nativeScreens?: Record<string, string>;
|
|
226
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* The bundle `stx native compile --format bundle` wrote, copied to
|
|
228
|
+
* `dist/native-screen.js`; or a directory of route bundles (stx's
|
|
229
|
+
* `routesDir`: `<Screen>.js` and `__app.js`), copied to
|
|
230
|
+
* `dist/native-screens/`, so each screen evaluates only its own file.
|
|
231
|
+
*/
|
|
227
232
|
nativeBundle?: string;
|
|
228
233
|
/**
|
|
229
234
|
* The bundle route of a screen-less script that runs once for the app's
|
|
@@ -307,6 +312,12 @@ export declare function usesNativeBundle(config: Pick<CraftConfig, 'renderer' |
|
|
|
307
312
|
export declare function nativeStartPath(config: Pick<CraftConfig, 'devServerURL'>): string;
|
|
308
313
|
/** What is wrong with a config's hybrid and tab settings, one sentence each. */
|
|
309
314
|
export declare function hybridConfigProblems(config: Pick<CraftConfig, 'renderer' | 'nativeScreens' | 'tabs' | 'shareStorage' | 'devServerURL' | 'nativeApp'>): string[];
|
|
315
|
+
/**
|
|
316
|
+
* The builder installs a directory of route bundles (stx's `routesDir`) as
|
|
317
|
+
* well as the one-file bundle, and the app runs each screen from its own
|
|
318
|
+
* route's file. Stacks asks for route bundles only when this is true.
|
|
319
|
+
*/
|
|
320
|
+
export declare const nativeRouteBundles = true;
|
|
310
321
|
export interface CraftBackgroundRefresh {
|
|
311
322
|
enabled: boolean;
|
|
312
323
|
/** The BGTaskScheduler identifier. Default `<bundleId>.refresh`. */
|
package/dist/ios/src/index.js
CHANGED
|
@@ -228,10 +228,18 @@ function serializeSimulatorSteps(steps) {
|
|
|
228
228
|
if (!Number.isInteger(number(step.index)))
|
|
229
229
|
throw new Error("field index must be an integer");
|
|
230
230
|
arg = `${step.index} ${step.text}`;
|
|
231
|
+
} else if ("position" in step) {
|
|
232
|
+
if (number(step.position) > 1)
|
|
233
|
+
throw new Error("Slider position must be between 0 and 1");
|
|
234
|
+
arg = `${token(step.id)} ${step.position}`;
|
|
231
235
|
} else if ("id" in step)
|
|
232
236
|
arg = `${token(step.id)} ${step.text}`;
|
|
233
237
|
else if ("text" in step) {
|
|
234
238
|
arg = step.text;
|
|
239
|
+
if (step.command === "longpressid")
|
|
240
|
+
token(arg);
|
|
241
|
+
if (step.command === "expectapp" && !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(arg))
|
|
242
|
+
throw new Error("Expected an application bundle identifier");
|
|
235
243
|
if ("seconds" in step && step.seconds !== undefined) {
|
|
236
244
|
if (arg.includes("|"))
|
|
237
245
|
throw new Error("A timed expectation cannot contain |");
|
|
@@ -476,13 +484,29 @@ function hybridConfigProblems(config) {
|
|
|
476
484
|
}
|
|
477
485
|
return problems;
|
|
478
486
|
}
|
|
487
|
+
var nativeRouteBundles = true;
|
|
479
488
|
function installNativeBundle(source, output) {
|
|
480
489
|
if (!existsSync(source))
|
|
481
490
|
throw new Error(`Native bundle not found: ${source}`);
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
491
|
+
const single = join3(output, "dist", "native-screen.js");
|
|
492
|
+
const routes = join3(output, "dist", "native-screens");
|
|
493
|
+
mkdirSync2(dirname2(single), { recursive: true });
|
|
494
|
+
if (statSync(source).isDirectory()) {
|
|
495
|
+
if (!readdirSync2(source).some((name) => name.endsWith(".js")))
|
|
496
|
+
throw new Error(`Native route bundles not found in ${source}`);
|
|
497
|
+
if (resolve(source) !== resolve(routes)) {
|
|
498
|
+
rmSync2(routes, { recursive: true, force: true });
|
|
499
|
+
cpSync(source, routes, { recursive: true });
|
|
500
|
+
}
|
|
501
|
+
rmSync2(single, { force: true });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (resolve(source) !== resolve(single))
|
|
505
|
+
cpSync(source, single);
|
|
506
|
+
rmSync2(routes, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
function hasInstalledNativeBundle(output) {
|
|
509
|
+
return existsSync(join3(output, "dist", "native-screen.js")) || existsSync(join3(output, "dist", "native-screens"));
|
|
486
510
|
}
|
|
487
511
|
var DEFAULT_CONFIG = {
|
|
488
512
|
renderer: "web",
|
|
@@ -1345,7 +1369,7 @@ async function build(options) {
|
|
|
1345
1369
|
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
1346
1370
|
if (bundle)
|
|
1347
1371
|
installNativeBundle(bundle, output);
|
|
1348
|
-
if (!
|
|
1372
|
+
if (!hasInstalledNativeBundle(output)) {
|
|
1349
1373
|
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
1350
1374
|
}
|
|
1351
1375
|
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
@@ -1370,8 +1394,8 @@ async function build(options) {
|
|
|
1370
1394
|
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
1371
1395
|
if (bundle) {
|
|
1372
1396
|
installNativeBundle(bundle, output);
|
|
1373
|
-
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
1374
|
-
} else if (!
|
|
1397
|
+
console.log(` Native screens: ${bundle} \u2192 ${statSync(bundle).isDirectory() ? "dist/native-screens/" : "dist/native-screen.js"}`);
|
|
1398
|
+
} else if (!hasInstalledNativeBundle(output)) {
|
|
1375
1399
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
1376
1400
|
}
|
|
1377
1401
|
}
|
|
@@ -1544,6 +1568,7 @@ export {
|
|
|
1544
1568
|
open,
|
|
1545
1569
|
normalizeNativePath,
|
|
1546
1570
|
nativeStartPath,
|
|
1571
|
+
nativeRouteBundles,
|
|
1547
1572
|
matchNativePath,
|
|
1548
1573
|
isHybrid,
|
|
1549
1574
|
installRuntime,
|