craft-native 0.0.149 → 0.0.151
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/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +13 -1
- package/dist/ios/templates/CraftNativeScreen.swift +24 -263
- package/dist/ios/templates/CraftNativeServices.swift +6 -3
- package/dist/ios/templates/CraftWebAudio.swift +2269 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.151";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.js
CHANGED
|
@@ -790,6 +790,7 @@ async function init(options) {
|
|
|
790
790
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
791
791
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
792
792
|
cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
|
|
793
|
+
cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
|
|
793
794
|
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
794
795
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
795
796
|
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
@@ -566,6 +566,18 @@ final class CraftChrome: ObservableObject {
|
|
|
566
566
|
tabBarVisible = true
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
/// A native screen that wants the whole screen (a workout under way) asks
|
|
570
|
+
/// the bar away while it shows (`tabBarHidden`, CraftNativeScreen.swift);
|
|
571
|
+
/// the next screen shown says again. The page's own hideTabBar is apart.
|
|
572
|
+
private(set) var hiddenByScreen = false
|
|
573
|
+
func setHiddenByScreen(_ hidden: Bool) {
|
|
574
|
+
guard hiddenByScreen != hidden else { return }
|
|
575
|
+
hiddenByScreen = hidden
|
|
576
|
+
pendingHide?.cancel()
|
|
577
|
+
withAnimation(Self.motion(.easeInOut(duration: 0.2))) { tabBarVisible = !hidden && !tabs.isEmpty }
|
|
578
|
+
publishLayout()
|
|
579
|
+
}
|
|
580
|
+
|
|
569
581
|
/// However the page behaves, the splash is gone after `seconds`
|
|
570
582
|
/// (`splashMaxSeconds`, three by default). It used to be ten: a page that
|
|
571
583
|
/// never painted held a logo on screen for ten seconds, which reads as a
|
|
@@ -595,7 +607,7 @@ final class CraftChrome: ObservableObject {
|
|
|
595
607
|
let dark = (body["tintDark"] as? String).flatMap { UIColor(hex: $0) }
|
|
596
608
|
tint = light.map { light in dark.map { dark in UIColor { $0.userInterfaceStyle == .dark ? dark : light } } ?? light }
|
|
597
609
|
}
|
|
598
|
-
withAnimation(Self.motion(.easeOut(duration: 0.2))) { tabBarVisible = !tabs.isEmpty }
|
|
610
|
+
withAnimation(Self.motion(.easeOut(duration: 0.2))) { tabBarVisible = !tabs.isEmpty && !hiddenByScreen }
|
|
599
611
|
publishLayout()
|
|
600
612
|
case "selectTab":
|
|
601
613
|
if let id = body["id"] as? String, id != selected {
|
|
@@ -360,196 +360,6 @@ private final class CraftNativeVideoView: UIView {
|
|
|
360
360
|
deinit { stop() }
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
/// The small Web Audio subset used by native STX screens: oscillator and gain
|
|
364
|
-
/// nodes connected to the context destination. JavaScriptCore has no Web Audio
|
|
365
|
-
/// implementation, so the screen installs a compatible surface and routes the
|
|
366
|
-
/// graph to one AVAudioEngine source node per oscillator.
|
|
367
|
-
private final class CraftNativeAudio {
|
|
368
|
-
private struct ParameterEvent {
|
|
369
|
-
let time: Double
|
|
370
|
-
let value: Double
|
|
371
|
-
let ramp: Bool
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
private final class Parameter {
|
|
375
|
-
private let lock = NSLock()
|
|
376
|
-
private var current: Double
|
|
377
|
-
private var events: [ParameterEvent] = []
|
|
378
|
-
|
|
379
|
-
init(_ value: Double) {
|
|
380
|
-
current = value
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
func set(_ value: Double) {
|
|
384
|
-
lock.lock(); defer { lock.unlock() }
|
|
385
|
-
current = value
|
|
386
|
-
events.removeAll()
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
func schedule(_ value: Double, at time: Double, ramp: Bool) {
|
|
390
|
-
lock.lock(); defer { lock.unlock() }
|
|
391
|
-
events.append(ParameterEvent(time: max(0, time), value: value, ramp: ramp))
|
|
392
|
-
events.sort { $0.time < $1.time }
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
func value(at time: Double) -> Double {
|
|
396
|
-
lock.lock(); defer { lock.unlock() }
|
|
397
|
-
var previousTime = 0.0
|
|
398
|
-
var previousValue = current
|
|
399
|
-
for event in events {
|
|
400
|
-
if time < event.time {
|
|
401
|
-
guard event.ramp, event.time > previousTime else { return previousValue }
|
|
402
|
-
let fraction = (time - previousTime) / (event.time - previousTime)
|
|
403
|
-
return previousValue + (event.value - previousValue) * fraction
|
|
404
|
-
}
|
|
405
|
-
previousTime = event.time
|
|
406
|
-
previousValue = event.value
|
|
407
|
-
}
|
|
408
|
-
return previousValue
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
private final class OscillatorState {
|
|
413
|
-
let lock = NSLock()
|
|
414
|
-
let frequency = Parameter(440)
|
|
415
|
-
var gain = Parameter(1)
|
|
416
|
-
var phase = 0.0
|
|
417
|
-
var active = false
|
|
418
|
-
let contextStart: TimeInterval
|
|
419
|
-
|
|
420
|
-
init(contextStart: TimeInterval) {
|
|
421
|
-
self.contextStart = contextStart
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
private let engine = AVAudioEngine()
|
|
426
|
-
private var format: AVAudioFormat?
|
|
427
|
-
private var sources: [String: (state: OscillatorState, node: AVAudioSourceNode)] = [:]
|
|
428
|
-
private var gains: [String: Parameter] = [:]
|
|
429
|
-
private var contextStarts: [String: TimeInterval] = [:]
|
|
430
|
-
private var closed = false
|
|
431
|
-
|
|
432
|
-
func handle(_ payload: [String: Any]) {
|
|
433
|
-
guard !closed, let action = payload["action"] as? String else { return }
|
|
434
|
-
switch action {
|
|
435
|
-
case "createContext":
|
|
436
|
-
closed = false
|
|
437
|
-
if let contextID = payload["contextId"] as? String {
|
|
438
|
-
contextStarts[contextID] = Date().timeIntervalSince1970
|
|
439
|
-
}
|
|
440
|
-
case "createOscillator":
|
|
441
|
-
guard let id = payload["nodeId"] as? String else { return }
|
|
442
|
-
let contextStart = (payload["contextId"] as? String).flatMap { contextStarts[$0] } ?? Date().timeIntervalSince1970
|
|
443
|
-
createOscillator(id: id, frequency: number(payload["frequency"]) ?? 440, contextStart: contextStart)
|
|
444
|
-
case "createGain":
|
|
445
|
-
guard let id = payload["nodeId"] as? String else { return }
|
|
446
|
-
gains[id] = Parameter(number(payload["gain"]) ?? 1)
|
|
447
|
-
case "connect":
|
|
448
|
-
guard let sourceID = payload["sourceId"] as? String else { return }
|
|
449
|
-
let destinationID = payload["destinationId"] as? String
|
|
450
|
-
if var source = sources[sourceID] {
|
|
451
|
-
source.state.lock.lock()
|
|
452
|
-
source.state.gain = destinationID.flatMap { gains[$0] } ?? Parameter(1)
|
|
453
|
-
source.state.lock.unlock()
|
|
454
|
-
sources[sourceID] = source
|
|
455
|
-
}
|
|
456
|
-
case "frequency":
|
|
457
|
-
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]), let source = sources[id] else { return }
|
|
458
|
-
source.state.frequency.set(max(0, value))
|
|
459
|
-
case "frequencyAtTime", "frequencyRampToTime":
|
|
460
|
-
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]), let time = number(payload["time"]), let source = sources[id] else { return }
|
|
461
|
-
source.state.frequency.schedule(max(0, value), at: time, ramp: action == "frequencyRampToTime")
|
|
462
|
-
case "gain":
|
|
463
|
-
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]) else { return }
|
|
464
|
-
gains[id]?.set(max(0, value))
|
|
465
|
-
case "gainAtTime", "gainRampToTime":
|
|
466
|
-
guard let id = payload["nodeId"] as? String, let value = number(payload["value"]), let time = number(payload["time"]) else { return }
|
|
467
|
-
gains[id]?.schedule(max(0, value), at: time, ramp: action == "gainRampToTime")
|
|
468
|
-
case "start", "stop":
|
|
469
|
-
guard let id = payload["nodeId"] as? String, let source = sources[id] else { return }
|
|
470
|
-
if action == "start" { start(source.state, after: max(0, number(payload["delay"]) ?? 0)) }
|
|
471
|
-
else { stop(source.state, after: max(0, number(payload["delay"]) ?? 0)) }
|
|
472
|
-
case "close":
|
|
473
|
-
close()
|
|
474
|
-
default:
|
|
475
|
-
break
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
func close() {
|
|
480
|
-
guard !closed else { return }
|
|
481
|
-
closed = true
|
|
482
|
-
sources.values.forEach { source in
|
|
483
|
-
source.state.lock.lock(); source.state.active = false; source.state.lock.unlock()
|
|
484
|
-
engine.detach(source.node)
|
|
485
|
-
}
|
|
486
|
-
sources.removeAll()
|
|
487
|
-
gains.removeAll()
|
|
488
|
-
contextStarts.removeAll()
|
|
489
|
-
engine.stop()
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private func createOscillator(id: String, frequency: Double, contextStart: TimeInterval) {
|
|
493
|
-
guard sources[id] == nil else { return }
|
|
494
|
-
guard let format = audioFormat() else { return }
|
|
495
|
-
let state = OscillatorState(contextStart: contextStart)
|
|
496
|
-
state.frequency.set(max(0, frequency))
|
|
497
|
-
let node = AVAudioSourceNode(format: format) { _, _, frameCount, audioBufferList in
|
|
498
|
-
let buffers = UnsafeMutableAudioBufferListPointer(audioBufferList)
|
|
499
|
-
state.lock.lock()
|
|
500
|
-
let active = state.active
|
|
501
|
-
let frequency = state.frequency
|
|
502
|
-
let gain = state.gain
|
|
503
|
-
var phase = state.phase
|
|
504
|
-
state.lock.unlock()
|
|
505
|
-
let startTime = max(0, Date().timeIntervalSince1970 - state.contextStart)
|
|
506
|
-
for buffer in buffers {
|
|
507
|
-
guard let data = buffer.mData else { continue }
|
|
508
|
-
let samples = data.assumingMemoryBound(to: Float.self)
|
|
509
|
-
for frame in 0..<Int(frameCount) {
|
|
510
|
-
let frameTime = startTime + Double(frame) / format.sampleRate
|
|
511
|
-
let frameFrequency = frequency.value(at: frameTime)
|
|
512
|
-
let frameGain = gain.value(at: frameTime)
|
|
513
|
-
samples[frame] = active ? Float(sin(phase) * frameGain) : 0
|
|
514
|
-
phase += 2 * Double.pi * frameFrequency / format.sampleRate
|
|
515
|
-
if phase >= 2 * Double.pi { phase -= 2 * Double.pi }
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
state.lock.lock(); state.phase = phase; state.lock.unlock()
|
|
519
|
-
return noErr
|
|
520
|
-
}
|
|
521
|
-
engine.attach(node)
|
|
522
|
-
engine.connect(node, to: engine.mainMixerNode, format: format)
|
|
523
|
-
sources[id] = (state, node)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
private func audioFormat() -> AVAudioFormat? {
|
|
527
|
-
if let format { return format }
|
|
528
|
-
let created = AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 2)
|
|
529
|
-
format = created
|
|
530
|
-
return created
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
private func start(_ state: OscillatorState, after delay: Double) {
|
|
534
|
-
if !engine.isRunning {
|
|
535
|
-
try? engine.start()
|
|
536
|
-
}
|
|
537
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
538
|
-
state.lock.lock(); state.active = true; state.lock.unlock()
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
private func stop(_ state: OscillatorState, after delay: Double) {
|
|
543
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
544
|
-
state.lock.lock(); state.active = false; state.lock.unlock()
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private func number(_ value: Any?) -> Double? {
|
|
549
|
-
(value as? NSNumber).map { $0.doubleValue }
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
363
|
/// A WebView-free host for the first stx native vertical slice. The bundled
|
|
554
364
|
/// JavaScript sends whole, compiled view trees to UIKit and receives control
|
|
555
365
|
/// events and Craft API replies through JavaScriptCore.
|
|
@@ -2249,7 +2059,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2249
2059
|
/// A drawing's shapes are not views: a change to one redraws its `Svg`.
|
|
2250
2060
|
private var svgOwners: [String: String] = [:]
|
|
2251
2061
|
private let mutationDocument = CraftNativeMutationDocument()
|
|
2252
|
-
|
|
2062
|
+
/// The screen's Web Audio (CraftWebAudio.swift): its `AudioContext`s.
|
|
2063
|
+
private let audio = CraftWebAudio()
|
|
2253
2064
|
/// `craft.audio`'s sounds and the streaming responses this screen reads.
|
|
2254
2065
|
private let sounds = CraftNativeSounds()
|
|
2255
2066
|
private let streams = CraftNativeEventStreams()
|
|
@@ -2335,9 +2146,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2335
2146
|
// a screen that hid its own.
|
|
2336
2147
|
navigationController?.setNavigationBarHidden(false, animated: animated)
|
|
2337
2148
|
}
|
|
2149
|
+
// Each screen says whether it wants the tab bar, so one that hid it
|
|
2150
|
+
// never leaves it hidden for the next.
|
|
2151
|
+
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
2338
2152
|
refreshTraitDefaults()
|
|
2339
2153
|
}
|
|
2340
2154
|
|
|
2155
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
2156
|
+
super.viewWillDisappear(animated)
|
|
2157
|
+
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
2158
|
+
// back for whatever shows next, a web page included.
|
|
2159
|
+
if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
2160
|
+
CraftChrome.shared.setHiddenByScreen(false)
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2341
2164
|
override func viewDidAppear(_ animated: Bool) {
|
|
2342
2165
|
super.viewDidAppear(animated)
|
|
2343
2166
|
screenVisibilityChanged(true)
|
|
@@ -2598,6 +2421,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2598
2421
|
jsContext.setObject(snapshotSet, forKeyedSubscript: "craftNativeSnapshotSet" as NSString)
|
|
2599
2422
|
jsContext.setObject(secureGet, forKeyedSubscript: "craftNativeSecureGet" as NSString)
|
|
2600
2423
|
jsContext.setObject(setOptions, forKeyedSubscript: "craftNativeSetOptions" as NSString)
|
|
2424
|
+
// `AudioContext` and the rest of Web Audio, rendered natively.
|
|
2425
|
+
audio.install(in: jsContext)
|
|
2601
2426
|
// `crypto` for code shared with the page (a sign-in's nonce and its
|
|
2602
2427
|
// SHA-256): JavaScriptCore has none. Random bytes from the system's
|
|
2603
2428
|
// generator, digests from CryptoKit, both as hex.
|
|
@@ -2754,76 +2579,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2754
2579
|
keep('navigation', {
|
|
2755
2580
|
setOptions: function(options) { craftNativeSetOptions(JSON.stringify(options || {})); }
|
|
2756
2581
|
});
|
|
2757
|
-
if (typeof globalThis.AudioContext !== 'function') {
|
|
2758
|
-
var nextAudioId = 0;
|
|
2759
|
-
function audioMessage(payload) {
|
|
2760
|
-
globalThis.__stxNativeBridge.postMessage({ type: 'AUDIO', payload: payload });
|
|
2761
|
-
}
|
|
2762
|
-
function audioParam(contextId, nodeId, action, initial) {
|
|
2763
|
-
var current = Number(initial) || 0;
|
|
2764
|
-
var param = {};
|
|
2765
|
-
Object.defineProperty(param, 'value', {
|
|
2766
|
-
configurable: true,
|
|
2767
|
-
enumerable: true,
|
|
2768
|
-
get: function() { return current; },
|
|
2769
|
-
set: function(value) {
|
|
2770
|
-
current = Number(value) || 0;
|
|
2771
|
-
audioMessage({ action: action, contextId: contextId, nodeId: nodeId, value: current });
|
|
2772
|
-
}
|
|
2773
|
-
});
|
|
2774
|
-
function schedule(value, when, ramp) {
|
|
2775
|
-
current = Number(value) || 0;
|
|
2776
|
-
var time = Number(when);
|
|
2777
|
-
audioMessage({ action: action + (ramp ? 'RampToTime' : 'AtTime'), contextId: contextId, nodeId: nodeId, value: current, time: isFinite(time) ? Math.max(0, time) : 0 });
|
|
2778
|
-
}
|
|
2779
|
-
param.setValueAtTime = function(value, when) { schedule(value, when, false); return param; };
|
|
2780
|
-
param.linearRampToValueAtTime = function(value, when) { schedule(value, when, true); return param; };
|
|
2781
|
-
return param;
|
|
2782
|
-
}
|
|
2783
|
-
function AudioContext() {
|
|
2784
|
-
this._id = 'audio-' + (++nextAudioId);
|
|
2785
|
-
this._started = Date.now() / 1000;
|
|
2786
|
-
this.state = 'running';
|
|
2787
|
-
this.destination = { _contextId: this._id, _nodeId: 'destination' };
|
|
2788
|
-
audioMessage({ action: 'createContext', contextId: this._id });
|
|
2789
|
-
}
|
|
2790
|
-
Object.defineProperty(AudioContext.prototype, 'currentTime', {
|
|
2791
|
-
get: function() { return Date.now() / 1000 - this._started; }
|
|
2792
|
-
});
|
|
2793
|
-
AudioContext.prototype.createOscillator = function() {
|
|
2794
|
-
var context = this;
|
|
2795
|
-
var node = { _contextId: context._id, _nodeId: context._id + '-osc-' + (++nextAudioId) };
|
|
2796
|
-
node.frequency = audioParam(context._id, node._nodeId, 'frequency', 440);
|
|
2797
|
-
node.connect = function(destination) {
|
|
2798
|
-
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2799
|
-
return destination;
|
|
2800
|
-
};
|
|
2801
|
-
node.start = function(when) {
|
|
2802
|
-
audioMessage({ action: 'start', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2803
|
-
};
|
|
2804
|
-
node.stop = function(when) {
|
|
2805
|
-
audioMessage({ action: 'stop', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2806
|
-
};
|
|
2807
|
-
audioMessage({ action: 'createOscillator', contextId: context._id, nodeId: node._nodeId, frequency: node.frequency.value });
|
|
2808
|
-
return node;
|
|
2809
|
-
};
|
|
2810
|
-
AudioContext.prototype.createGain = function() {
|
|
2811
|
-
var context = this;
|
|
2812
|
-
var node = { _contextId: context._id, _nodeId: context._id + '-gain-' + (++nextAudioId) };
|
|
2813
|
-
node.gain = audioParam(context._id, node._nodeId, 'gain', 1);
|
|
2814
|
-
node.connect = function(destination) {
|
|
2815
|
-
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2816
|
-
return destination;
|
|
2817
|
-
};
|
|
2818
|
-
audioMessage({ action: 'createGain', contextId: context._id, nodeId: node._nodeId, gain: node.gain.value });
|
|
2819
|
-
return node;
|
|
2820
|
-
};
|
|
2821
|
-
AudioContext.prototype.resume = function() { this.state = 'running'; return Promise.resolve(); };
|
|
2822
|
-
AudioContext.prototype.suspend = function() { this.state = 'suspended'; return Promise.resolve(); };
|
|
2823
|
-
AudioContext.prototype.close = function() { this.state = 'closed'; audioMessage({ action: 'close', contextId: this._id }); return Promise.resolve(); };
|
|
2824
|
-
globalThis.AudioContext = AudioContext;
|
|
2825
|
-
globalThis.webkitAudioContext = AudioContext;
|
|
2826
|
-
}
|
|
2827
2582
|
})();
|
|
2828
2583
|
""")
|
|
2829
2584
|
}
|
|
@@ -2852,6 +2607,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2852
2607
|
private var rootTopToSafeArea: NSLayoutConstraint?
|
|
2853
2608
|
private var rootTopToView: NSLayoutConstraint?
|
|
2854
2609
|
private var navigationBarHidden: Bool?
|
|
2610
|
+
/// The app's tab bar away while this screen shows (`tabBarHidden`).
|
|
2611
|
+
private var tabBarHidden = false
|
|
2855
2612
|
private var prefersLargeTitle: Bool?
|
|
2856
2613
|
|
|
2857
2614
|
/// Whether this screen asked for a bar of its own through `setOptions`: a
|
|
@@ -2879,6 +2636,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2879
2636
|
if largeTitle { navigationController?.navigationBar.prefersLargeTitles = true }
|
|
2880
2637
|
trackContentScrollView()
|
|
2881
2638
|
}
|
|
2639
|
+
if let hidden = options["tabBarHidden"] as? Bool {
|
|
2640
|
+
tabBarHidden = hidden
|
|
2641
|
+
if navigationController?.topViewController === self, view.window != nil {
|
|
2642
|
+
CraftChrome.shared.setHiddenByScreen(hidden)
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2882
2645
|
if let hidden = options["hidden"] as? Bool {
|
|
2883
2646
|
navigationBarHidden = hidden
|
|
2884
2647
|
if navigationController?.topViewController === self {
|
|
@@ -3341,8 +3104,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3341
3104
|
pendingCapabilityDeadlines.removeValue(forKey: requestToken)?.cancel()
|
|
3342
3105
|
screenRequestCancellers.removeValue(forKey: requestToken)?()
|
|
3343
3106
|
CraftNativeActions.cancel(requestToken: requestToken)
|
|
3344
|
-
case "AUDIO":
|
|
3345
|
-
audio.handle(payload)
|
|
3346
3107
|
default:
|
|
3347
3108
|
break
|
|
3348
3109
|
}
|
|
@@ -261,7 +261,7 @@ final class CraftNativeSounds {
|
|
|
261
261
|
private func remove(_ id: String) {
|
|
262
262
|
guard let sound = sounds.removeValue(forKey: id) else { return }
|
|
263
263
|
sound.end()
|
|
264
|
-
Self.
|
|
264
|
+
Self.releaseSession()
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/// Moves the volume in steps on the main run loop, a 30th of a second
|
|
@@ -284,7 +284,10 @@ final class CraftNativeSounds {
|
|
|
284
284
|
RunLoop.main.add(timer, forMode: .common)
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
/// Takes the session for one more playing sound. Native screens' Web
|
|
288
|
+
/// Audio (CraftWebAudio.swift) counts here too, so speech hands the
|
|
289
|
+
/// session back to whichever of them still plays.
|
|
290
|
+
static func claimSession(respectSilentSwitch: Bool) {
|
|
288
291
|
playing += 1
|
|
289
292
|
category = respectSilentSwitch ? .ambient : .playback
|
|
290
293
|
let session = AVAudioSession.sharedInstance()
|
|
@@ -297,7 +300,7 @@ final class CraftNativeSounds {
|
|
|
297
300
|
}
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
|
|
303
|
+
static func releaseSession() {
|
|
301
304
|
playing = max(0, playing - 1)
|
|
302
305
|
guard playing == 0, let previous = sessionToRestore else { return }
|
|
303
306
|
sessionToRestore = nil
|