craft-native 0.0.319 → 0.0.329

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.
@@ -0,0 +1,75 @@
1
+ import AVFoundation
2
+ import Foundation
3
+ import UIKit
4
+
5
+ /// One owner at a time, including the asynchronous permission prompt. A late
6
+ /// permission answer cannot start recording after its screen/account left.
7
+ final class CraftVoiceRecording {
8
+ static let shared = CraftVoiceRecording()
9
+ private var owner: String?
10
+ private var recorder: AVAudioRecorder?
11
+ private var file: URL?
12
+ private var backgroundObserver: NSObjectProtocol?
13
+
14
+ private init() {
15
+ backgroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in self?.clear() }
16
+ }
17
+
18
+ private func clear() {
19
+ owner = nil
20
+ recorder?.stop()
21
+ recorder = nil
22
+ if let file { CraftNativeFiles.remove(file.path) }
23
+ file = nil
24
+ try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
25
+ }
26
+
27
+ func perform(method: String, args: [Any], isActive: @escaping () -> Bool = { true }, completion: @escaping (Result<Any, CraftNativeActionError>) -> Void) {
28
+ DispatchQueue.main.async { self.performOnMain(method: method, args: args, isActive: isActive, completion: completion) }
29
+ }
30
+
31
+ private func performOnMain(method: String, args: [Any], isActive: @escaping () -> Bool, completion: @escaping (Result<Any, CraftNativeActionError>) -> Void) {
32
+ func fail(_ code: String, _ message: String) { completion(.failure(CraftNativeActionError(code: code, message: message))) }
33
+ guard let key = args.first as? String, UUID(uuidString: key) != nil else { fail("INVALID_ARGUMENT", "A recording owner UUID is required"); return }
34
+ if method == "cancel" {
35
+ if owner == key { clear() }
36
+ completion(.success(true))
37
+ return
38
+ }
39
+ if method == "stop" {
40
+ guard owner == key, let file else { fail("RECORDING_NOT_FOUND", "This recording is no longer available"); return }
41
+ recorder?.stop()
42
+ recorder = nil
43
+ try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
44
+ guard let audio = try? AVAudioFile(forReading: file), audio.length > 0 else { clear(); fail("EMPTY_RECORDING", "The recording was empty"); return }
45
+ let duration = Double(audio.length) / audio.processingFormat.sampleRate
46
+ completion(.success(CraftNativeFiles.describe(file, type: "audio/mp4", extra: ["durationSeconds": duration])))
47
+ return
48
+ }
49
+ guard method == "start" else { fail("UNKNOWN_ACTION", "Unknown voice recording action"); return }
50
+ guard isActive() else { fail("RECORDING_CANCELLED", "The requesting screen has closed"); return }
51
+ guard owner == nil else { fail("RECORDING_BUSY", "Another microphone session is still active"); return }
52
+ let options = args.count > 1 ? args[1] as? [String: Any] ?? [:] : [:]
53
+ let seconds = (options["maxSeconds"] as? NSNumber)?.doubleValue ?? 300
54
+ guard seconds.isFinite, seconds >= 1, seconds <= 300 else { fail("INVALID_ARGUMENT", "Recordings are limited to five minutes"); return }
55
+ owner = key
56
+ AVAudioSession.sharedInstance().requestRecordPermission { allowed in
57
+ DispatchQueue.main.async {
58
+ guard self.owner == key, isActive() else { if self.owner == key { self.clear() }; fail("RECORDING_CANCELLED", "Recording was cancelled"); return }
59
+ guard allowed else { self.clear(); fail("PERMISSION_DENIED", "Microphone permission was refused"); return }
60
+ do {
61
+ let session = AVAudioSession.sharedInstance()
62
+ try session.setCategory(.record, mode: .measurement)
63
+ try session.setActive(true)
64
+ let file = try CraftNativeFiles.newFile(named: "voice-feedback.m4a")
65
+ self.file = file
66
+ let recorder = try AVAudioRecorder(url: file, settings: [AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 16000, AVNumberOfChannelsKey: 1, AVEncoderBitRateKey: 32000])
67
+ self.recorder = recorder
68
+ guard recorder.record(forDuration: max(0.5, seconds - 0.25)) else { throw CraftNativeActionError(code: "RECORDING_FAILED", message: "The microphone could not start") }
69
+ completion(.success(true))
70
+ }
71
+ catch { self.clear(); fail("RECORDING_FAILED", "The microphone could not start") }
72
+ }
73
+ }
74
+ }
75
+ }
@@ -14,6 +14,12 @@ settings:
14
14
  CRAFT_APNS_ENVIRONMENT: development
15
15
  release:
16
16
  CRAFT_APNS_ENVIRONMENT: production
17
+ SWIFT_OPTIMIZATION_LEVEL: "-O"
18
+ SWIFT_COMPILATION_MODE: wholemodule
19
+ GCC_OPTIMIZATION_LEVEL: s
20
+ DEAD_CODE_STRIPPING: YES
21
+ STRIP_INSTALLED_PRODUCT: YES
22
+ COPY_PHASE_STRIP: YES
17
23
  targets:
18
24
  {{APP_NAME}}:
19
25
  type: application
package/dist/types.d.ts CHANGED
@@ -2374,8 +2374,8 @@ export interface CraftTimersAPI {
2374
2374
  /** Native grouping policy. Unselected application icons are always protected. */
2375
2375
  export interface MenubarInitOptions {
2376
2376
  backend?: 'legacy' | 'nativeVisibility';
2377
- /** Defaults to true. False accepts native suppression of Apple modules,
2378
- * including Focus, Now Playing and camera/microphone indicators. */
2377
+ /** Defaults to true. False groups identifiable Apple controls by position
2378
+ * and accepts native suppression of Focus, Now Playing and privacy indicators. */
2379
2379
  preserveSystemItems?: boolean;
2380
2380
  }
2381
2381
  /** Settled native menu-bar state; private visibility capability is explicit. */
@@ -2388,6 +2388,7 @@ export interface MenubarState {
2388
2388
  authorized?: boolean;
2389
2389
  busy?: boolean;
2390
2390
  hiddenAppCount?: number;
2391
+ hiddenSystemItemCount?: number;
2391
2392
  preserveSystemItems?: boolean;
2392
2393
  markerWidth?: number;
2393
2394
  error?: 'unavailable' | 'permissionRequired' | 'layoutUnavailable' | 'emptyGroup' | 'visibilityMismatch' | 'activationFailed' | 'activationTimeout' | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.319",
3
+ "version": "0.0.329",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",