ReviewOS

also looking at this

craft-native/craft

fix(native): interaction, audio, document and checkpoint gaps stranded after #401

#415
Merged glennmichael123 wants to merge codex/issues-388-400-344 into main
19 files +987 -50

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/guides/native-stx.mdmodified+2-0
Changes to docs/guides/native-stx.md
@@ -1378,6 +1378,8 @@ only when the system grants the `shouldResume` option. A permanent Android
13781378focus loss releases every player and sends each sound's error event; replacement,
13791379decode failure, cancellation, interruption, and teardown also release their
13801380player resources.
1381Control calls resolve `false` when their sound id is not owned by the screen,
1382matching the browser host instead of reporting a false success.
13811383
13821384Checkpoint names use the same bounded filename contract on both native hosts:
13831385letters, digits, `.`, `_`, and `-`, up to 128 characters, without a leading
packages/ios/src/index.test.tsmodified+3-0
Changes to packages/ios/src/index.test.ts
@@ -2139,6 +2139,9 @@ describe('Craft iOS builder', () => {
21392139 expect(services).toContain('AVAudioSession.interruptionNotification')
21402140 expect(services).toContain('sound.interrupted = true')
21412141 expect(services).toContain('AVAudioSession.InterruptionOptions.shouldResume')
2142 expect(screen).toContain('reply(.success(sounds.pause(id: id)))')
2143 expect(screen).toContain('reply(.success(sounds.resume(id: id)))')
2144 expect(screen).toContain('reply(.success(sounds.stop(id: id, fadeOut: number(1, "fadeOut") / 1000)))')
21422145 expect(plist).not.toContain('<string>audio</string>')
21432146 // A route change or an interruption stops AVAudioEngine; it starts again.
21442147 expect(audio).toContain('forName: .AVAudioEngineConfigurationChange, object: engine')
packages/ios/templates/CraftNativeScreen.swiftmodified+5-10
Changes to packages/ios/templates/CraftNativeScreen.swift
@@ -7762,20 +7762,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
77627762 screenRequestCancellers[requestToken] = { [weak self] in self?.sounds.stop(id: id, fadeOut: 0) }
77637763 sounds.play(id: id, url: url, options: args.count > 2 ? args[2] as? [String: Any] ?? [:] : [:], answer: reply)
77647764 case ("Audio", "pause"):
7765 sounds.pause(id: id)
7766 reply(.success(true))
7765 reply(.success(sounds.pause(id: id)))
77677766 case ("Audio", "resume"):
7768 sounds.resume(id: id)
7769 reply(.success(true))
7767 reply(.success(sounds.resume(id: id)))
77707768 case ("Audio", "setVolume"):
7771 sounds.setVolume(id: id, volume: number(1), fade: number(2, "fade") / 1000)
7772 reply(.success(true))
7769 reply(.success(sounds.setVolume(id: id, volume: number(1), fade: number(2, "fade") / 1000)))
77737770 case ("Audio", "stop"):
7774 sounds.stop(id: id, fadeOut: number(1, "fadeOut") / 1000)
7775 reply(.success(true))
7771 reply(.success(sounds.stop(id: id, fadeOut: number(1, "fadeOut") / 1000)))
77767772 case ("Audio", "stopAll"):
7777 sounds.stopAll(fadeOut: number(0, "fadeOut") / 1000)
7778 reply(.success(true))
7773 reply(.success(sounds.stopAll(fadeOut: number(0, "fadeOut") / 1000)))
77797774 default:
77807775 return false
77817776 }
packages/ios/templates/CraftNativeServices.swiftmodified+20-10
Changes to packages/ios/templates/CraftNativeServices.swift
@@ -395,42 +395,52 @@ final class CraftNativeSounds {
395395 if fadeIn > 0 { ramp(sound, to: volume, over: fadeIn) }
396396 }
397397
398 func pause(id: String) {
399 guard let sound = sounds[id] else { return }
398 @discardableResult
399 func pause(id: String) -> Bool {
400 guard let sound = sounds[id] else { return false }
400401 sound.paused = true
401402 sound.interrupted = false
402403 sound.fade?.invalidate()
403404 sound.player.pause()
405 return true
404406 }
405407
406 func resume(id: String) {
407 guard let sound = sounds[id], sound.paused else { return }
408 @discardableResult
409 func resume(id: String) -> Bool {
410 guard let sound = sounds[id], sound.paused else { return false }
408411 sound.paused = false
409412 sound.interrupted = false
410413 sound.player.volume = sound.volume
411414 sound.player.play()
415 return true
412416 }
413417
414418 /// Changes the volume, over `fade` seconds when given.
415 func setVolume(id: String, volume: Double, fade: Double) {
416 guard let sound = sounds[id] else { return }
419 @discardableResult
420 func setVolume(id: String, volume: Double, fade: Double) -> Bool {
421 guard let sound = sounds[id] else { return false }
417422 sound.volume = Float(min(max(volume, 0), 1))
418423 if fade > 0, !sound.paused { ramp(sound, to: sound.volume, over: fade) }
419424 else { sound.fade?.invalidate(); sound.player.volume = sound.volume }
425 return true
420426 }
421427
422428 /// Stops the sound, fading it out over `fadeOut` seconds first.
423 func stop(id: String, fadeOut: Double) {
424 guard let sound = sounds[id] else { return }
425 guard fadeOut > 0, !sound.paused else { remove(id); return }
429 @discardableResult
430 func stop(id: String, fadeOut: Double) -> Bool {
431 guard let sound = sounds[id] else { return false }
432 guard fadeOut > 0, !sound.paused else { remove(id); return true }
426433 ramp(sound, to: 0, over: fadeOut) { [weak self, weak sound] in
427434 guard let self, let sound, self.sounds[id] === sound else { return }
428435 self.remove(id)
429436 }
437 return true
430438 }
431439
432 func stopAll(fadeOut: Double) {
440 @discardableResult
441 func stopAll(fadeOut: Double) -> Bool {
433442 for id in Array(sounds.keys) { stop(id: id, fadeOut: fadeOut) }
443 return true
434444 }
435445
436446 /// The screen closed: everything stops at once.