craft-native 0.0.262 → 0.0.276
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 +22 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +32 -1
- package/dist/android/templates/CraftNativeMutation.kt.template +2 -0
- package/dist/android/templates/MainActivityNative.kt.template +131 -10
- package/dist/browser/lifecycle.d.ts +7 -0
- package/dist/browser.js +126 -24
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +19 -3
- package/dist/ios/templates/CraftHybrid.swift +25 -2
- package/dist/ios/templates/CraftNativeActions.swift +108 -21
- package/dist/ios/templates/CraftNativeFlatList.swift +156 -16
- package/dist/ios/templates/CraftNativeMutation.swift +3 -1
- package/dist/ios/templates/CraftNativeScreen.swift +362 -29
- package/dist/macos/templates/CraftNativeMutation.swift +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -646,3 +646,25 @@ An explicit discard may ACK the captured old ID only after a fresh committed
|
|
|
646
646
|
native read proves that its previously registered intent was deleted or
|
|
647
647
|
replaced. A cached missing draft or an unsuccessful initial registration is
|
|
648
648
|
not evidence of deletion.
|
|
649
|
+
|
|
650
|
+
### Browser lifecycle
|
|
651
|
+
|
|
652
|
+
`createBrowserPlatform()` from `craft-native/browser` supplies
|
|
653
|
+
`craft.lifecycle.getState()` and `onStateChange(callback)` (also `onChange`).
|
|
654
|
+
States are `active` and `background`. The host emits the native-compatible
|
|
655
|
+
`APP_STATE { state }` event on visibility changes and page cache navigation,
|
|
656
|
+
coalesces duplicate transitions, and releases listeners in `dispose()`.
|
|
657
|
+
This lets browser simulations checkpoint on backgrounding with the same contract
|
|
658
|
+
as a native shell. Storage failures must still be handled by the caller.
|
|
659
|
+
|
|
660
|
+
Await `platform.call('Storage', 'set', [key, value], signal)` before reporting
|
|
661
|
+
a saved checkpoint. `craft.storage.setSync` updates the immediate UI cache and
|
|
662
|
+
queues its write when Web Locks are available; it cannot acknowledge durability
|
|
663
|
+
or propagate a later quota failure. Background events are an opportunity to
|
|
664
|
+
checkpoint, but browsers can terminate a page before queued work finishes, so
|
|
665
|
+
also save periodically during play.
|
|
666
|
+
|
|
667
|
+
Browser audio assigns one owner per sound ID. A newer play or stop invalidates
|
|
668
|
+
pending media resolution for that ID; stopping all sounds, changing accounts,
|
|
669
|
+
aborting a call or disposing the host also releases active audio. Superseded
|
|
670
|
+
play calls reject with `AbortError`.
|
|
@@ -16,6 +16,8 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
16
16
|
private var pendingChildren = JSONArray()
|
|
17
17
|
private var styleKey: String? = null
|
|
18
18
|
private var styleVersion = 0
|
|
19
|
+
/** The prefix of a style key that is a vector style's URL, not raster style JSON. */
|
|
20
|
+
private val vectorStylePrefix = "uri:"
|
|
19
21
|
private var readyStyle: Style? = null
|
|
20
22
|
private var initialCameraKey: String? = null
|
|
21
23
|
private var reportedRegion: LatLngBounds? = null
|
|
@@ -146,7 +148,10 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
146
148
|
styleKey = nextStyle
|
|
147
149
|
readyStyle = null
|
|
148
150
|
val version = ++styleVersion
|
|
149
|
-
|
|
151
|
+
// A vector style (the standard basemap) loads from its URL; the
|
|
152
|
+
// raster ones are composed here as style JSON.
|
|
153
|
+
val builder = if (nextStyle.startsWith(vectorStylePrefix)) Style.Builder().fromUri(nextStyle.removePrefix(vectorStylePrefix)) else Style.Builder().fromJson(nextStyle)
|
|
154
|
+
nativeMap.setStyle(builder) { loaded ->
|
|
150
155
|
if (!destroyed && version == styleVersion) {
|
|
151
156
|
readyStyle = loaded
|
|
152
157
|
installOverlays(loaded)
|
|
@@ -183,6 +188,20 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
private fun basemapStyle(): String {
|
|
191
|
+
// `basemap`: the named presets, drawn as well as each can be here.
|
|
192
|
+
// Standard is OpenFreeMap's vector style - sharp at any density, no
|
|
193
|
+
// key - where it was raster tiles from tile.openstreetmap.org, whose
|
|
194
|
+
// policy does not allow an app's traffic. Outdoors is topography with
|
|
195
|
+
// contours; satellite and hybrid are Esri imagery.
|
|
196
|
+
when (pendingProps.optString("basemap", "").lowercase()) {
|
|
197
|
+
"standard" -> {
|
|
198
|
+
attribution.text = "OpenFreeMap © OpenMapTiles · Data © OpenStreetMap contributors"
|
|
199
|
+
return vectorStylePrefix + "https://tiles.openfreemap.org/styles/liberty"
|
|
200
|
+
}
|
|
201
|
+
"outdoors" -> return rasterStyle("https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", "Topography: Esri, USGS, NOAA, OpenStreetMap contributors", labels = false)
|
|
202
|
+
"satellite" -> return rasterStyle("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "Imagery © Esri, Maxar, Earthstar Geographics, and the GIS User Community", labels = false)
|
|
203
|
+
"hybrid" -> return rasterStyle("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", "Imagery © Esri, Maxar, Earthstar Geographics, and the GIS User Community", labels = true)
|
|
204
|
+
}
|
|
186
205
|
val custom = pendingProps.optJSONObject("tileOverlay")
|
|
187
206
|
val template = custom?.optString("urlTemplate").orEmpty()
|
|
188
207
|
val credit = custom?.optString("attribution").orEmpty()
|
|
@@ -212,6 +231,18 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
|
|
|
212
231
|
return JSONObject().put("version", 8).put("sources", sources).put("layers", layers).toString()
|
|
213
232
|
}
|
|
214
233
|
|
|
234
|
+
private fun rasterStyle(url: String, credit: String, labels: Boolean): String {
|
|
235
|
+
val sources = JSONObject().put("basemap", rasterSource(url, credit))
|
|
236
|
+
val layers = JSONArray().put(JSONObject().put("id", "basemap").put("type", "raster").put("source", "basemap"))
|
|
237
|
+
attribution.text = credit
|
|
238
|
+
if (labels) {
|
|
239
|
+
sources.put("labels", rasterSource("https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}", "Labels © Esri"))
|
|
240
|
+
layers.put(JSONObject().put("id", "labels").put("type", "raster").put("source", "labels"))
|
|
241
|
+
attribution.text = "$credit · Labels © Esri"
|
|
242
|
+
}
|
|
243
|
+
return JSONObject().put("version", 8).put("sources", sources).put("layers", layers).toString()
|
|
244
|
+
}
|
|
245
|
+
|
|
215
246
|
private fun rasterSource(url: String, credit: String): JSONObject = JSONObject()
|
|
216
247
|
.put("type", "raster").put("tiles", JSONArray().put(url)).put("tileSize", 256).put("maxzoom", 19).put("attribution", credit)
|
|
217
248
|
|
|
@@ -309,6 +309,8 @@ class CraftNativeMutationDocument {
|
|
|
309
309
|
"View", "SafeAreaView", "ScrollView", "KeyboardAvoidingView", "Modal", "Pressable",
|
|
310
310
|
"TouchableOpacity", "TouchableHighlight", "ImageBackground", "FlatList", "SectionList",
|
|
311
311
|
"Fragment", "Svg", "G", "Map",
|
|
312
|
+
// A shape holds its `Animate`: SMIL keyframes.
|
|
313
|
+
"Path", "Line", "Rect", "Circle", "Ellipse", "Polyline", "Polygon",
|
|
312
314
|
)
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -351,10 +351,131 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
351
351
|
var drawing: JSONObject = JSONObject()
|
|
352
352
|
set(value) {
|
|
353
353
|
field = value
|
|
354
|
+
animates = hasAnimation(value.optJSONArray("children"))
|
|
354
355
|
invalidate()
|
|
355
356
|
requestLayout()
|
|
357
|
+
updateFrames()
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
/** Whether a shape carries keyframes (SMIL `<animate attributeName="d">`, stx's `Animate`). */
|
|
361
|
+
private var animates = false
|
|
362
|
+
/** When this drawing's repetition began (System.nanoTime), 0 while still. */
|
|
363
|
+
private var animationStart = 0L
|
|
364
|
+
private var frameScheduled = false
|
|
365
|
+
private val frameCallback = android.view.Choreographer.FrameCallback { nextFrame() }
|
|
366
|
+
|
|
367
|
+
/** Moving now: shown, and animations are on (off is Android's reduced motion). */
|
|
368
|
+
val isAnimating: Boolean
|
|
369
|
+
get() = animates && isAttachedToWindow && isShown && ValueAnimator.areAnimatorsEnabled()
|
|
370
|
+
|
|
371
|
+
override fun onAttachedToWindow() {
|
|
372
|
+
super.onAttachedToWindow()
|
|
373
|
+
updateFrames()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
override fun onDetachedFromWindow() {
|
|
377
|
+
super.onDetachedFromWindow()
|
|
378
|
+
updateFrames()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
|
382
|
+
super.onVisibilityChanged(changedView, visibility)
|
|
383
|
+
updateFrames()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** One frame at a time from the display (Choreographer) while moving; none while still or away. */
|
|
387
|
+
private fun updateFrames() {
|
|
388
|
+
if (isAnimating) {
|
|
389
|
+
if (animationStart == 0L) animationStart = System.nanoTime()
|
|
390
|
+
if (!frameScheduled) {
|
|
391
|
+
frameScheduled = true
|
|
392
|
+
android.view.Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
animationStart = 0L
|
|
396
|
+
if (frameScheduled) {
|
|
397
|
+
frameScheduled = false
|
|
398
|
+
android.view.Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private fun nextFrame() {
|
|
404
|
+
frameScheduled = false
|
|
405
|
+
if (!isAnimating) return updateFrames()
|
|
406
|
+
// Drawn only while some of it is on screen; a row scrolled away keeps its time.
|
|
407
|
+
if (getLocalVisibleRect(android.graphics.Rect())) invalidate()
|
|
408
|
+
frameScheduled = true
|
|
409
|
+
android.view.Choreographer.getInstance().postFrameCallback(frameCallback)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private fun hasAnimation(children: JSONArray?): Boolean {
|
|
413
|
+
if (children == null) return false
|
|
414
|
+
return (0 until children.length()).any { index ->
|
|
415
|
+
val node = children.optJSONObject(index) ?: return@any false
|
|
416
|
+
if (node.optString("type") == "Animate") node.optJSONObject("props")?.has("frames") == true
|
|
417
|
+
else hasAnimation(node.optJSONArray("children"))
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A shape's outline now: between its keyframes while moving, else its own. */
|
|
422
|
+
private fun outline(node: JSONObject): List<Float>? {
|
|
423
|
+
if (isAnimating && animationStart != 0L) {
|
|
424
|
+
val children = node.optJSONArray("children")
|
|
425
|
+
val animate = children?.let { list -> (0 until list.length()).mapNotNull { list.optJSONObject(it) }.firstOrNull { it.optString("type") == "Animate" } }
|
|
426
|
+
animate?.optJSONObject("props")?.let { keyframe(it, (System.nanoTime() - animationStart) / 1e9) }?.let { return it }
|
|
427
|
+
}
|
|
428
|
+
val geometry = node.optJSONObject("props")?.optJSONArray("geometry") ?: return null
|
|
429
|
+
return (0 until geometry.length()).map { craftNativeNumber(geometry.opt(it))?.toFloat() ?: Float.NaN }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
companion object {
|
|
433
|
+
/**
|
|
434
|
+
* The outline `elapsed` seconds into an `Animate`'s repetitions, between
|
|
435
|
+
* the keyframes either side of now (SMIL's linear `calcMode`); null when
|
|
436
|
+
* it does not animate path data or its repetitions are over.
|
|
437
|
+
*/
|
|
438
|
+
fun keyframe(props: JSONObject, elapsed: Double): List<Float>? {
|
|
439
|
+
if (props.optString("attributeName") != "d" || elapsed < 0) return null
|
|
440
|
+
val duration = craftNativeNumber(props.opt("duration"))?.takeIf { it.isFinite() && it > 0 } ?: return null
|
|
441
|
+
val raw = props.optJSONArray("frames") ?: return null
|
|
442
|
+
val frames = (0 until raw.length()).mapNotNull { index ->
|
|
443
|
+
raw.optJSONArray(index)?.let { frame -> (0 until frame.length()).map { craftNativeNumber(frame.opt(it))?.toFloat() ?: Float.NaN } }
|
|
444
|
+
}
|
|
445
|
+
if (frames.size < 2 || frames.size != raw.length()) return null
|
|
446
|
+
craftNativeNumber(props.opt("repeatCount"))?.let { if (it.isFinite() && elapsed >= it * duration) return null }
|
|
447
|
+
val given = props.optJSONArray("frameTimes")?.let { times -> (0 until times.length()).mapNotNull { craftNativeNumber(times.opt(it)) } }.orEmpty()
|
|
448
|
+
val times = if (given.size == frames.size) given else frames.indices.map { it.toDouble() / (frames.size - 1) }
|
|
449
|
+
val phase = (elapsed % duration) / duration
|
|
450
|
+
var index = 0
|
|
451
|
+
while (index < frames.size - 2 && times[index + 1] <= phase) index++
|
|
452
|
+
val span = times[index + 1] - times[index]
|
|
453
|
+
val share = if (span > 0) ((phase - times[index]) / span).coerceIn(0.0, 1.0).toFloat() else 0f
|
|
454
|
+
return interpolate(frames[index], frames[index + 1], share)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Two outlines with the same commands, a share of the way between; the first when they differ. */
|
|
458
|
+
fun interpolate(from: List<Float>, to: List<Float>, share: Float): List<Float> {
|
|
459
|
+
if (from.size != to.size || share <= 0f) return from
|
|
460
|
+
val out = from.toMutableList()
|
|
461
|
+
var index = 0
|
|
462
|
+
while (index < from.size) {
|
|
463
|
+
if (to[index] != from[index]) return from
|
|
464
|
+
val count = when (from[index].toInt()) {
|
|
465
|
+
0, 1 -> 2
|
|
466
|
+
2 -> 6
|
|
467
|
+
3 -> 4
|
|
468
|
+
4 -> 0
|
|
469
|
+
else -> return from
|
|
470
|
+
}
|
|
471
|
+
if (index + count >= from.size) return from
|
|
472
|
+
for (offset in 1..count) out[index + offset] = from[index + offset] + (to[index + offset] - from[index + offset]) * share
|
|
473
|
+
index += count + 1
|
|
474
|
+
}
|
|
475
|
+
return out
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
358
479
|
private data class SvgPaint(
|
|
359
480
|
val fill: String? = "#000000",
|
|
360
481
|
val stroke: String? = null,
|
|
@@ -489,25 +610,25 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
489
610
|
when (node.optString("type")) {
|
|
490
611
|
"G" -> drawNodes(canvas, node.optJSONArray("children") ?: JSONArray(), paint)
|
|
491
612
|
"SvgText" -> drawText(canvas, node, paint)
|
|
613
|
+
"Animate" -> Unit
|
|
492
614
|
else -> drawPath(canvas, node, paint)
|
|
493
615
|
}
|
|
494
616
|
}
|
|
495
617
|
}
|
|
496
618
|
|
|
497
619
|
private fun drawPath(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
498
|
-
val
|
|
499
|
-
|
|
500
|
-
if ((0 until geometry.length()).any { craftNativeNumber(geometry.opt(it))?.toFloat()?.isFinite() != true }) return
|
|
620
|
+
val geometry = outline(node) ?: return
|
|
621
|
+
if (geometry.any { !it.isFinite() }) return
|
|
501
622
|
val path = Path()
|
|
502
623
|
var index = 0
|
|
503
|
-
while (index < geometry.
|
|
504
|
-
val command =
|
|
505
|
-
fun number(offset: Int): Float =
|
|
624
|
+
while (index < geometry.size) {
|
|
625
|
+
val command = geometry[index].toInt()
|
|
626
|
+
fun number(offset: Int): Float = geometry[index + offset]
|
|
506
627
|
when (command) {
|
|
507
|
-
0 -> if (index + 2 < geometry.
|
|
508
|
-
1 -> if (index + 2 < geometry.
|
|
509
|
-
2 -> if (index + 6 < geometry.
|
|
510
|
-
3 -> if (index + 4 < geometry.
|
|
628
|
+
0 -> if (index + 2 < geometry.size) { path.moveTo(number(1), number(2)); index += 3 } else break
|
|
629
|
+
1 -> if (index + 2 < geometry.size) { path.lineTo(number(1), number(2)); index += 3 } else break
|
|
630
|
+
2 -> if (index + 6 < geometry.size) { path.cubicTo(number(1), number(2), number(3), number(4), number(5), number(6)); index += 7 } else break
|
|
631
|
+
3 -> if (index + 4 < geometry.size) { path.quadTo(number(1), number(2), number(3), number(4)); index += 5 } else break
|
|
511
632
|
4 -> { path.close(); index += 1 }
|
|
512
633
|
else -> break
|
|
513
634
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type BrowserAppState = 'active' | 'background';
|
|
2
|
+
/** Browser lifecycle shares the native APP_STATE contract, including bfcache navigation. */
|
|
3
|
+
export declare function createBrowserLifecycle(page: Pick<Document, 'hidden' | 'addEventListener' | 'removeEventListener'>, host: Pick<Window, 'addEventListener' | 'removeEventListener'>, changed: (state: BrowserAppState) => void): {
|
|
4
|
+
getState: () => BrowserAppState;
|
|
5
|
+
onStateChange(callback: (state: BrowserAppState) => void): () => void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
};
|
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
// src/browser/lifecycle.ts
|
|
2
|
+
function createBrowserLifecycle(page, host, changed) {
|
|
3
|
+
let state = page.hidden ? "background" : "active";
|
|
4
|
+
const listeners = new Set;
|
|
5
|
+
let disposed = false;
|
|
6
|
+
const set = (next) => {
|
|
7
|
+
if (disposed || state === next)
|
|
8
|
+
return;
|
|
9
|
+
state = next;
|
|
10
|
+
changed(state);
|
|
11
|
+
for (const listener of [...listeners]) {
|
|
12
|
+
try {
|
|
13
|
+
listener(state);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("craft.lifecycle listener failed", error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const visibility = () => set(page.hidden ? "background" : "active");
|
|
20
|
+
const hide = () => set("background");
|
|
21
|
+
page.addEventListener("visibilitychange", visibility);
|
|
22
|
+
host.addEventListener("pagehide", hide);
|
|
23
|
+
host.addEventListener("pageshow", visibility);
|
|
24
|
+
return {
|
|
25
|
+
getState: () => state,
|
|
26
|
+
onStateChange(callback) {
|
|
27
|
+
if (typeof callback !== "function")
|
|
28
|
+
throw new TypeError("lifecycle.onStateChange needs a function");
|
|
29
|
+
if (disposed)
|
|
30
|
+
return () => {};
|
|
31
|
+
listeners.add(callback);
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(callback);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
dispose() {
|
|
37
|
+
if (disposed)
|
|
38
|
+
return;
|
|
39
|
+
disposed = true;
|
|
40
|
+
page.removeEventListener("visibilitychange", visibility);
|
|
41
|
+
host.removeEventListener("pagehide", hide);
|
|
42
|
+
host.removeEventListener("pageshow", visibility);
|
|
43
|
+
listeners.clear();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
1
48
|
// src/bridge/ids.ts
|
|
2
49
|
function secureUUID() {
|
|
3
50
|
const c = globalThis.crypto;
|
|
@@ -377,6 +424,8 @@ async function createBrowserPlatform(options = {}) {
|
|
|
377
424
|
const files = new Map;
|
|
378
425
|
const mediaURLs = new Map;
|
|
379
426
|
const sounds = new Map;
|
|
427
|
+
const soundRequests = new Map;
|
|
428
|
+
const soundCleanup = new Map;
|
|
380
429
|
const fades = new Map;
|
|
381
430
|
const cancelFade = (sound) => {
|
|
382
431
|
const fade = fades.get(sound);
|
|
@@ -414,11 +463,12 @@ async function createBrowserPlatform(options = {}) {
|
|
|
414
463
|
if (!sound)
|
|
415
464
|
return;
|
|
416
465
|
await fade(sound, 0, sound.paused ? 0 : milliseconds);
|
|
417
|
-
|
|
418
|
-
|
|
466
|
+
soundCleanup.get(sound)?.();
|
|
467
|
+
soundCleanup.delete(sound);
|
|
419
468
|
sound.pause();
|
|
420
469
|
sound.removeAttribute("src");
|
|
421
|
-
sounds.
|
|
470
|
+
if (sounds.get(id) === sound)
|
|
471
|
+
sounds.delete(id);
|
|
422
472
|
};
|
|
423
473
|
const lifetime = new AbortController;
|
|
424
474
|
let account = new AbortController;
|
|
@@ -454,6 +504,7 @@ async function createBrowserPlatform(options = {}) {
|
|
|
454
504
|
const resetAccount = async () => {
|
|
455
505
|
const previousCache = `${namespace}:media:${mediaScope}`;
|
|
456
506
|
account.abort();
|
|
507
|
+
soundRequests.clear();
|
|
457
508
|
account = new AbortController;
|
|
458
509
|
for (const path of files.keys())
|
|
459
510
|
URL.revokeObjectURL(path);
|
|
@@ -462,10 +513,12 @@ async function createBrowserPlatform(options = {}) {
|
|
|
462
513
|
files.clear();
|
|
463
514
|
mediaURLs.clear();
|
|
464
515
|
for (const audio of sounds.values()) {
|
|
516
|
+
soundCleanup.get(audio)?.();
|
|
465
517
|
cancelFade(audio);
|
|
466
518
|
audio.pause();
|
|
467
519
|
audio.src = "";
|
|
468
520
|
}
|
|
521
|
+
soundCleanup.clear();
|
|
469
522
|
sounds.clear();
|
|
470
523
|
await persistence.accountChanged();
|
|
471
524
|
mediaScope = persistence.scope();
|
|
@@ -480,7 +533,10 @@ async function createBrowserPlatform(options = {}) {
|
|
|
480
533
|
});
|
|
481
534
|
};
|
|
482
535
|
window.addEventListener("storage", storageEvent);
|
|
483
|
-
const
|
|
536
|
+
const lifecycle = typeof document !== "undefined" ? createBrowserLifecycle(document, window, (state) => emit("APP_STATE", { state })) : null;
|
|
537
|
+
const capabilityNames = ["storage", "secureStorage", "dialog", "browser", "linking", "files", "share", "device"];
|
|
538
|
+
if (lifecycle)
|
|
539
|
+
capabilityNames.push("lifecycle");
|
|
484
540
|
if (persistence.atomicStorage)
|
|
485
541
|
capabilityNames.push("storageCompareAndSet", "storageSetIfAbsent");
|
|
486
542
|
if (typeof globalThis.crypto?.randomUUID === "function" || typeof globalThis.crypto?.getRandomValues === "function")
|
|
@@ -500,6 +556,7 @@ async function createBrowserPlatform(options = {}) {
|
|
|
500
556
|
const platform = {
|
|
501
557
|
craft: {
|
|
502
558
|
...persistence.craft,
|
|
559
|
+
...lifecycle ? { lifecycle: { getState: lifecycle.getState, onStateChange: lifecycle.onStateChange, onChange: lifecycle.onStateChange } } : {},
|
|
503
560
|
session: {
|
|
504
561
|
key: sessionTokenKey,
|
|
505
562
|
token: () => persistence.craft.secureStorage.getSync(sessionTokenKey),
|
|
@@ -755,29 +812,74 @@ async function createBrowserPlatform(options = {}) {
|
|
|
755
812
|
const id = String(args[0]);
|
|
756
813
|
const sound = sounds.get(id);
|
|
757
814
|
if (method === "play") {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
audio.onended = () => {
|
|
765
|
-
sounds.delete(id);
|
|
766
|
-
emit("SOUND_EVENT", { id, event: "ended" });
|
|
815
|
+
const request = {};
|
|
816
|
+
soundRequests.set(id, request);
|
|
817
|
+
const current = () => {
|
|
818
|
+
signal.throwIfAborted();
|
|
819
|
+
if (soundRequests.get(id) !== request)
|
|
820
|
+
throw new DOMException("Playback replaced", "AbortError");
|
|
767
821
|
};
|
|
768
|
-
audio
|
|
769
|
-
|
|
770
|
-
|
|
822
|
+
let audio;
|
|
823
|
+
const release = () => {
|
|
824
|
+
if (!audio)
|
|
825
|
+
return;
|
|
826
|
+
signal.removeEventListener("abort", release);
|
|
827
|
+
soundCleanup.delete(audio);
|
|
828
|
+
cancelFade(audio);
|
|
829
|
+
audio.pause();
|
|
830
|
+
audio.removeAttribute("src");
|
|
831
|
+
if (sounds.get(id) === audio)
|
|
832
|
+
sounds.delete(id);
|
|
771
833
|
};
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
834
|
+
try {
|
|
835
|
+
await stopSound(id);
|
|
836
|
+
current();
|
|
837
|
+
const url = await platform.resolveMedia(String(args[1]));
|
|
838
|
+
current();
|
|
839
|
+
audio = new Audio(url);
|
|
840
|
+
const settings = object(args[2]);
|
|
841
|
+
audio.loop = Boolean(settings.loop);
|
|
842
|
+
const volume = Math.max(0, Math.min(1, Number(settings.volume ?? 1)));
|
|
843
|
+
audio.volume = Number(settings.fadeIn) > 0 ? 0 : volume;
|
|
844
|
+
audio.onended = () => {
|
|
845
|
+
release();
|
|
846
|
+
if (soundRequests.get(id) === request) {
|
|
847
|
+
soundRequests.delete(id);
|
|
848
|
+
emit("SOUND_EVENT", { id, event: "ended" });
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
audio.onerror = () => {
|
|
852
|
+
const message = audio?.error?.message;
|
|
853
|
+
release();
|
|
854
|
+
if (soundRequests.get(id) === request) {
|
|
855
|
+
soundRequests.delete(id);
|
|
856
|
+
emit("SOUND_EVENT", { id, event: "error", message });
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
sounds.set(id, audio);
|
|
860
|
+
soundCleanup.set(audio, release);
|
|
861
|
+
signal.addEventListener("abort", release, { once: true });
|
|
862
|
+
await audio.play();
|
|
863
|
+
current();
|
|
864
|
+
fade(audio, volume, Number(settings.fadeIn ?? 0));
|
|
865
|
+
return true;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
release();
|
|
868
|
+
if (soundRequests.get(id) === request)
|
|
869
|
+
soundRequests.delete(id);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
776
872
|
}
|
|
777
873
|
if (method === "stopAll") {
|
|
874
|
+
soundRequests.clear();
|
|
778
875
|
await Promise.all([...sounds.keys()].map((key) => stopSound(key, Number(first.fadeOut ?? 0))));
|
|
779
876
|
return true;
|
|
780
877
|
}
|
|
878
|
+
if (method === "stop") {
|
|
879
|
+
soundRequests.delete(id);
|
|
880
|
+
await stopSound(id, Number(object(args[1]).fadeOut ?? 0));
|
|
881
|
+
return Boolean(sound);
|
|
882
|
+
}
|
|
781
883
|
if (!sound)
|
|
782
884
|
return false;
|
|
783
885
|
if (method === "pause") {
|
|
@@ -788,10 +890,6 @@ async function createBrowserPlatform(options = {}) {
|
|
|
788
890
|
await sound.play();
|
|
789
891
|
return true;
|
|
790
892
|
}
|
|
791
|
-
if (method === "stop") {
|
|
792
|
-
await stopSound(id, Number(object(args[1]).fadeOut ?? 0));
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
893
|
if (method === "setVolume") {
|
|
796
894
|
await fade(sound, Number(args[1]), Number(object(args[2]).fade ?? 0));
|
|
797
895
|
return true;
|
|
@@ -873,6 +971,8 @@ async function createBrowserPlatform(options = {}) {
|
|
|
873
971
|
if (disposed)
|
|
874
972
|
return;
|
|
875
973
|
disposed = true;
|
|
974
|
+
soundRequests.clear();
|
|
975
|
+
lifecycle?.dispose();
|
|
876
976
|
window.removeEventListener("storage", storageEvent);
|
|
877
977
|
lifetime.abort();
|
|
878
978
|
account.abort();
|
|
@@ -883,10 +983,12 @@ async function createBrowserPlatform(options = {}) {
|
|
|
883
983
|
files.clear();
|
|
884
984
|
mediaURLs.clear();
|
|
885
985
|
for (const audio of sounds.values()) {
|
|
986
|
+
soundCleanup.get(audio)?.();
|
|
886
987
|
cancelFade(audio);
|
|
887
988
|
audio.pause();
|
|
888
989
|
audio.src = "";
|
|
889
990
|
}
|
|
991
|
+
soundCleanup.clear();
|
|
890
992
|
sounds.clear();
|
|
891
993
|
wakeLock?.release();
|
|
892
994
|
listeners.clear();
|
package/dist/cli.js
CHANGED
|
@@ -2842,7 +2842,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
2842
2842
|
`);
|
|
2843
2843
|
}
|
|
2844
2844
|
// package.json
|
|
2845
|
-
var version = "0.0.
|
|
2845
|
+
var version = "0.0.276";
|
|
2846
2846
|
|
|
2847
2847
|
// bin/cli.ts
|
|
2848
2848
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -797,10 +797,21 @@ final class CraftChrome: ObservableObject {
|
|
|
797
797
|
tabAccessory = next
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
-
/// A
|
|
801
|
-
|
|
800
|
+
/// A screen going away gives its accessory up without taking it off the
|
|
801
|
+
/// bar (a retained or prepared screen cannot clear another's): the screen coming in shows it until it says what it wants. Taken
|
|
802
|
+
/// off at once, the pill stretched across the bar and back while the
|
|
803
|
+
/// next screen (Messages, opened from the accessory; a tab opened for the
|
|
804
|
+
/// first time) started up and set the same accessory again.
|
|
805
|
+
func releaseTabAccessory(ownedBy owner: AnyObject) {
|
|
802
806
|
guard tabAccessoryOwner === owner else { return }
|
|
803
|
-
|
|
807
|
+
tabAccessoryOwner = nil
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/// Takes off an accessory nobody claims any more: the screen now showing
|
|
811
|
+
/// drew without asking for one, or is a page that never does.
|
|
812
|
+
func clearUnclaimedTabAccessory() {
|
|
813
|
+
guard tabAccessoryOwner == nil, tabAccessory != nil else { return }
|
|
814
|
+
tabAccessory = nil
|
|
804
815
|
}
|
|
805
816
|
|
|
806
817
|
func setHiddenByScreen(_ hidden: Bool) {
|
|
@@ -6347,6 +6358,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6347
6358
|
|
|
6348
6359
|
SecItemDelete(query as CFDictionary)
|
|
6349
6360
|
let status = SecItemAdd(query as CFDictionary, nil)
|
|
6361
|
+
CraftKeychain.forget(account: key)
|
|
6350
6362
|
return status == errSecSuccess
|
|
6351
6363
|
}
|
|
6352
6364
|
|
|
@@ -6373,12 +6385,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6373
6385
|
kSecAttrAccount as String: key
|
|
6374
6386
|
]
|
|
6375
6387
|
let status = SecItemDelete(query as CFDictionary)
|
|
6388
|
+
CraftKeychain.forget(account: key)
|
|
6376
6389
|
return status == errSecSuccess || status == errSecItemNotFound
|
|
6377
6390
|
}
|
|
6378
6391
|
|
|
6379
6392
|
private func secureClear() -> Bool {
|
|
6380
6393
|
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword]
|
|
6381
6394
|
let status = SecItemDelete(query as CFDictionary)
|
|
6395
|
+
CraftKeychain.forget(account: nil)
|
|
6382
6396
|
return status == errSecSuccess || status == errSecItemNotFound
|
|
6383
6397
|
}
|
|
6384
6398
|
|
|
@@ -8259,6 +8273,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
8259
8273
|
SecItemDelete(query as CFDictionary)
|
|
8260
8274
|
|
|
8261
8275
|
let status = SecItemAdd(query as CFDictionary, nil)
|
|
8276
|
+
CraftKeychain.forget(account: key)
|
|
8262
8277
|
if status == errSecSuccess {
|
|
8263
8278
|
resolveCallback(callbackId, result: ["success": true, "key": key])
|
|
8264
8279
|
} else {
|
|
@@ -8301,6 +8316,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
8301
8316
|
}
|
|
8302
8317
|
|
|
8303
8318
|
let status = SecItemDelete(query as CFDictionary)
|
|
8319
|
+
CraftKeychain.forget(account: key)
|
|
8304
8320
|
if status == errSecSuccess || status == errSecItemNotFound {
|
|
8305
8321
|
resolveCallback(callbackId, result: ["success": true, "key": key])
|
|
8306
8322
|
} else {
|