craft-native 0.0.319 → 0.0.328
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/android/src/index.d.ts +3 -5
- package/dist/android/src/index.js +48 -5
- package/dist/android/templates/CraftBridge.kt.template +0 -90
- package/dist/android/templates/CraftNative.kt.template +3 -0
- package/dist/android/templates/CraftNativeBackground.kt.template +8 -2
- package/dist/android/templates/CraftNativeServices.kt.template +100 -1
- package/dist/android/templates/CraftPermissionPolicy.kt.template +96 -0
- package/dist/android/templates/MainActivityNative.kt.template +1 -0
- package/dist/android/templates/build.gradle.kts.app.template +1 -5
- package/dist/api/mobile.d.ts +9 -0
- package/dist/cli.js +37 -19
- package/dist/index.cjs +40 -19
- package/dist/index.js +40 -19
- package/dist/ios/src/index.d.ts +2 -5
- package/dist/ios/src/index.js +20 -0
- package/dist/ios/templates/CraftActivityAttributes.swift +29 -2
- package/dist/ios/templates/CraftApp.swift +1 -1
- package/dist/ios/templates/CraftHealth.swift +14 -0
- package/dist/ios/templates/CraftLiveActivities.swift +106 -2
- package/dist/ios/templates/CraftLiveActivityWidget.swift.template +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +9 -1
- package/dist/ios/templates/CraftNativeScreen.swift +51 -1
- package/dist/ios/templates/CraftVoiceRecording.swift +75 -0
- package/dist/ios/templates/project.yml.template +6 -0
- package/dist/types.d.ts +3 -2
- package/package.json +1 -1
|
@@ -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
|
|
2378
|
-
*
|
|
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;
|