craft-native 0.0.237 → 0.0.241
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 +71 -0
- package/dist/android/src/emulator.d.ts +23 -0
- package/dist/android/src/index.d.ts +24 -2
- package/dist/android/src/index.js +362 -103
- package/dist/android/src/native-map.d.ts +7 -0
- package/dist/android/templates/CraftBridge.kt.template +67 -55
- package/dist/android/templates/CraftNative.kt.template +27 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +36 -5
- 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/CraftNativeServices.kt.template +45 -35
- package/dist/android/templates/CraftNativeShell.kt.template +103 -20
- package/dist/android/templates/LocationRecordingService.kt.template +255 -47
- package/dist/android/templates/MainActivityNative.kt.template +7 -428
- package/dist/android/templates/build.gradle.kts.app.template +1 -1
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/android-adb.d.ts +2 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/mobile.d.ts +49 -14
- package/dist/browser/storage.d.ts +3 -0
- package/dist/browser.js +163 -51
- package/dist/cli.js +1 -1
- package/dist/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/index.d.ts +5 -0
- package/dist/ios/src/index.js +17 -1
- package/dist/ios/src/testing.d.ts +16 -2
- package/dist/ios/templates/CraftApp.swift +418 -93
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +82 -17
- package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
- package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +45 -1
- package/dist/macos/templates/native_layout.zig +41 -5
- package/dist/mobile-testing.js +30 -4
- package/dist/mobile.js +169 -17
- 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())
|
|
@@ -7000,6 +6575,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7000
6575
|
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
7001
6576
|
?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" }
|
|
7002
6577
|
val value = craftNativeOptionalString(props.opt("accessibilityValue"))
|
|
6578
|
+
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
7003
6579
|
view.contentDescription = when {
|
|
7004
6580
|
label != null && value != null -> "$label, $value"
|
|
7005
6581
|
label != null -> label
|
|
@@ -7024,6 +6600,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
7024
6600
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
7025
6601
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
7026
6602
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
6603
|
+
// Expose STX test identities to Android's native automation
|
|
6604
|
+
// tree without replacing the label read by TalkBack.
|
|
6605
|
+
info.viewIdResourceName = testId
|
|
7027
6606
|
info.className = when (role) {
|
|
7028
6607
|
"button" -> Button::class.java.name
|
|
7029
6608
|
"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/api/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type { HttpClientOptions, RequestOptions, HttpResponse, WebSocketOptions
|
|
|
17
17
|
export { crypto, uuid, randomString, hmac, timingSafeEqual, hashPassword, verifyPassword } from './crypto.js';
|
|
18
18
|
export { env, getPlatform, isDesktop, isMobile, isCraft, getSystemInfo, exec, spawn, ChildProcess, cwd, homeDir, tempDir, exit, argv, open } from './process.js';
|
|
19
19
|
export type { Platform, SystemInfo, ExecOptions, ExecResult, SpawnOptions } from './process.js';
|
|
20
|
-
export { device, haptics, permissions, camera, biometrics, secureStorage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
|
|
20
|
+
export { device, haptics, permissions, camera, biometrics, secureStorage, storage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
|
|
21
21
|
export type { DeviceInfo, DeviceCapabilities, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, CameraOptions as MobileCameraOptions, PhotoResult, BiometricType, Location, LocationOptions, ShareOptions, AppState, NotificationOptions as MobileNotificationOptions } from './mobile.js';
|
|
22
22
|
export { carplay, appClips, liveActivities, sharePlay, storeKit, appIntents, tipKit, focusFilters } from './ios-advanced.js';
|
|
23
23
|
export type { CarPlayTemplateType, CarPlayListItem, CarPlayGridItem, CarPlayTemplate, AppClipInvocation, LiveActivityContentState, LiveActivityAttributes, LiveActivityConfig, LiveActivityHandle, SharePlaySessionState, SharePlayParticipant, SharePlayActivity, ProductType, Product, Transaction, IntentParameterType, IntentParameter, AppIntent, TipDisplayFrequency, Tip, FocusStatus, FocusFilter } from './ios-advanced.js';
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -459,6 +459,8 @@ export declare const secureStorage: {
|
|
|
459
459
|
* Location coordinates.
|
|
460
460
|
*/
|
|
461
461
|
export interface Location {
|
|
462
|
+
/** Owner captured when a native recording emits this sample. */
|
|
463
|
+
recordingId?: string;
|
|
462
464
|
latitude: number;
|
|
463
465
|
longitude: number;
|
|
464
466
|
altitude?: number;
|
|
@@ -478,6 +480,30 @@ export interface LocationOptions {
|
|
|
478
480
|
/** Maximum age of cached position in milliseconds */
|
|
479
481
|
maximumAge?: number;
|
|
480
482
|
}
|
|
483
|
+
/** Persist this ID with the owning intent before starting a native recording. */
|
|
484
|
+
export interface RecordingStartOptions {
|
|
485
|
+
recordingId?: string;
|
|
486
|
+
}
|
|
487
|
+
/** Fence a recording operation to the ID returned by start or explicit recovery. */
|
|
488
|
+
export interface RecordingOwnershipOptions {
|
|
489
|
+
expectedId: string;
|
|
490
|
+
}
|
|
491
|
+
/** Secure IDs supplied by the platform, including WebView-free mobile hosts. */
|
|
492
|
+
export declare const crypto: {
|
|
493
|
+
randomUUID(): Promise<string>;
|
|
494
|
+
};
|
|
495
|
+
/** Native storage, including an existing-key atomic checkpoint. */
|
|
496
|
+
export declare const storage: {
|
|
497
|
+
supportsSetIfAbsent(): boolean;
|
|
498
|
+
supportsCompareAndSet(): boolean;
|
|
499
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
500
|
+
set(key: string, value: unknown): Promise<boolean>;
|
|
501
|
+
remove(key: string): Promise<boolean>;
|
|
502
|
+
clear(): Promise<boolean>;
|
|
503
|
+
keys(): Promise<string[]>;
|
|
504
|
+
setIfAbsent(key: string, value: unknown): Promise<boolean>;
|
|
505
|
+
compareAndSet(key: string, expectedValue: unknown, nextValue: unknown): Promise<boolean>;
|
|
506
|
+
};
|
|
481
507
|
export interface LocationRecordingState {
|
|
482
508
|
id: string | null;
|
|
483
509
|
active: boolean;
|
|
@@ -507,6 +533,8 @@ export interface LocationRecordingResult extends LocationRecordingState {
|
|
|
507
533
|
* ```
|
|
508
534
|
*/
|
|
509
535
|
export declare const location: {
|
|
536
|
+
supportsRecordingOwnership: () => boolean;
|
|
537
|
+
supportsRecordingIntentIdentity: () => boolean;
|
|
510
538
|
/**
|
|
511
539
|
* Get current position.
|
|
512
540
|
*
|
|
@@ -528,12 +556,13 @@ export declare const location: {
|
|
|
528
556
|
* @param watchId - Watch ID returned from watchPosition
|
|
529
557
|
*/
|
|
530
558
|
clearWatch(watchId: number): void;
|
|
531
|
-
startRecording(options?: LocationOptions): Promise<LocationRecordingState>;
|
|
532
|
-
pauseRecording(): Promise<LocationRecordingState>;
|
|
533
|
-
resumeRecording(): Promise<LocationRecordingState>;
|
|
534
|
-
stopRecording(): Promise<LocationRecordingResult>;
|
|
535
|
-
getRecordingState(): Promise<LocationRecordingState>;
|
|
536
|
-
|
|
559
|
+
startRecording(options?: LocationOptions & RecordingStartOptions): Promise<LocationRecordingState>;
|
|
560
|
+
pauseRecording(options?: RecordingOwnershipOptions): Promise<LocationRecordingState>;
|
|
561
|
+
resumeRecording(options?: RecordingOwnershipOptions): Promise<LocationRecordingState>;
|
|
562
|
+
stopRecording(options?: RecordingOwnershipOptions): Promise<LocationRecordingResult>;
|
|
563
|
+
getRecordingState(options?: RecordingOwnershipOptions): Promise<LocationRecordingState>;
|
|
564
|
+
acknowledgeRecording(options: RecordingOwnershipOptions): Promise<boolean>;
|
|
565
|
+
readRecording(options?: RecordingOwnershipOptions): Promise<Location[]>;
|
|
537
566
|
};
|
|
538
567
|
/**
|
|
539
568
|
* Share options.
|
|
@@ -914,7 +943,7 @@ export interface HeartRateSample {
|
|
|
914
943
|
/** Milliseconds between beats, when the strap sent them. */
|
|
915
944
|
rr?: number[];
|
|
916
945
|
}
|
|
917
|
-
export type HeartRateMethod = 'getState' | 'requestAuthorization' | 'scan' | 'stopScan' | 'getDevices' | 'connect' | 'reconnect' | 'disconnect' | 'startRecording' | 'pauseRecording' | 'resumeRecording' | 'stopRecording' | 'getRecordingState' | 'readRecording';
|
|
946
|
+
export type HeartRateMethod = 'getState' | 'requestAuthorization' | 'scan' | 'stopScan' | 'getDevices' | 'connect' | 'reconnect' | 'disconnect' | 'startRecording' | 'pauseRecording' | 'resumeRecording' | 'stopRecording' | 'getRecordingState' | 'readRecording' | 'acknowledgeRecording';
|
|
918
947
|
export declare function normalizeHeartRateDevice(value: unknown): HeartRateDevice | null;
|
|
919
948
|
/** A state from native, made safe to render whatever an older or newer build sent. */
|
|
920
949
|
export declare function normalizeHeartRateState(value: unknown): HeartRateState;
|
|
@@ -923,6 +952,8 @@ export declare function normalizeHeartRateReading(value: unknown): HeartRateRead
|
|
|
923
952
|
/** A recording's samples, oldest first, one per timestamp, implausible ones left out. */
|
|
924
953
|
export declare function normalizeHeartRateSamples(value: unknown): HeartRateSample[];
|
|
925
954
|
export declare const heartRate: {
|
|
955
|
+
supportsRecordingOwnership: () => boolean;
|
|
956
|
+
supportsRecordingIntentIdentity: () => boolean;
|
|
926
957
|
/** Whether this app can read a heart-rate strap. */
|
|
927
958
|
isAvailable(): boolean;
|
|
928
959
|
/** Whether this app build answers `method`; an older build may lack one. */
|
|
@@ -955,18 +986,20 @@ export declare const heartRate: {
|
|
|
955
986
|
forget?: boolean;
|
|
956
987
|
}) => Promise<HeartRateState>;
|
|
957
988
|
/** Keep every reading natively from now on: screen locked, page suspended or app closed. */
|
|
958
|
-
startRecording
|
|
959
|
-
pauseRecording
|
|
960
|
-
resumeRecording
|
|
989
|
+
startRecording(options?: RecordingStartOptions): Promise<HeartRateRecordingState>;
|
|
990
|
+
pauseRecording(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState>;
|
|
991
|
+
resumeRecording(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState>;
|
|
961
992
|
/** Ends the recording and hands back every reading it kept. */
|
|
962
|
-
stopRecording
|
|
993
|
+
stopRecording(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState & {
|
|
963
994
|
samples: HeartRateSample[];
|
|
964
995
|
}>;
|
|
965
|
-
getRecordingState
|
|
996
|
+
getRecordingState(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState>;
|
|
997
|
+
acknowledgeRecording(options: RecordingOwnershipOptions): Promise<boolean>;
|
|
966
998
|
/** The recording so far; only what came after `since` (ms) when given. */
|
|
967
|
-
readRecording
|
|
999
|
+
readRecording(options?: {
|
|
968
1000
|
since?: number;
|
|
969
|
-
|
|
1001
|
+
expectedId?: string;
|
|
1002
|
+
}): Promise<HeartRateSample[]>;
|
|
970
1003
|
/** Every reading from the connected strap, about once a second. */
|
|
971
1004
|
onReading: (callback: (reading: HeartRateReading) => void) => (() => void);
|
|
972
1005
|
/** Each strap a scan finds. */
|
|
@@ -1685,6 +1718,8 @@ export declare const watchConnectivity: {
|
|
|
1685
1718
|
onReachabilityChange(callback: (reachable: boolean) => void): () => void;
|
|
1686
1719
|
};
|
|
1687
1720
|
declare const mobile: {
|
|
1721
|
+
crypto: typeof crypto;
|
|
1722
|
+
storage: typeof storage;
|
|
1688
1723
|
device: typeof device;
|
|
1689
1724
|
haptics: typeof haptics;
|
|
1690
1725
|
permissions: typeof permissions;
|
|
@@ -6,8 +6,11 @@ export interface BrowserStorageOptions {
|
|
|
6
6
|
accountKeys?: string[];
|
|
7
7
|
hash: (value: string) => Promise<string>;
|
|
8
8
|
changed: (name: string) => void;
|
|
9
|
+
/** A host lock primitive; browsers use their native Web Locks implementation. */
|
|
10
|
+
lock?: (<T>(name: string, operation: () => T | Promise<T>) => Promise<T>) | null;
|
|
9
11
|
}
|
|
10
12
|
export interface BrowserStorage {
|
|
13
|
+
atomicStorage: boolean;
|
|
11
14
|
craft: {
|
|
12
15
|
storage: {
|
|
13
16
|
getSync: (key: string) => unknown;
|