@thelacanians/vue-native-cli 0.4.15 → 0.6.0
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 +329 -15
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +118 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
- package/native/android/gradle.properties +1 -0
- package/native/android/gradlew +1 -1
- package/native/ios/VueNativeCore/Package.swift +1 -1
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +1 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +143 -5
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
- package/native/macos/VueNativeMacOS/Package.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
- package/native/shared/VueNativeShared/AGENTS.md +129 -0
- package/native/shared/VueNativeShared/Package.swift +14 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
- package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
- package/package.json +8 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
|
|
4
|
+
/// Native module for audio playback and recording.
|
|
5
|
+
/// Uses AVFoundation which is available on both iOS and macOS.
|
|
6
|
+
///
|
|
7
|
+
/// Methods:
|
|
8
|
+
/// - play(uri: String, options?: Object) -- play audio from URI
|
|
9
|
+
/// - pause() -- pause playback
|
|
10
|
+
/// - resume() -- resume playback
|
|
11
|
+
/// - stop() -- stop playback and release player
|
|
12
|
+
/// - seek(position: Number) -- seek to position in seconds
|
|
13
|
+
/// - setVolume(volume: Number) -- set volume 0.0-1.0
|
|
14
|
+
/// - startRecording(options?: Object) -- start audio recording
|
|
15
|
+
/// - stopRecording() -- stop recording, returns { uri, duration }
|
|
16
|
+
/// - pauseRecording() -- pause recording
|
|
17
|
+
/// - resumeRecording() -- resume recording
|
|
18
|
+
/// - getStatus() -- returns current playback status
|
|
19
|
+
///
|
|
20
|
+
/// Events (via eventDispatcher.dispatchGlobalEvent):
|
|
21
|
+
/// - audio:progress { currentTime, duration }
|
|
22
|
+
/// - audio:complete {}
|
|
23
|
+
/// - audio:error { message }
|
|
24
|
+
public final class AudioModule: NSObject, NativeModule {
|
|
25
|
+
public let moduleName = "Audio"
|
|
26
|
+
|
|
27
|
+
private var player: AVAudioPlayer?
|
|
28
|
+
private var recorder: AVAudioRecorder?
|
|
29
|
+
private var progressTimer: Timer?
|
|
30
|
+
private var isPlaying = false
|
|
31
|
+
private weak var eventDispatcher: NativeEventDispatcher?
|
|
32
|
+
|
|
33
|
+
// MARK: - Delegate to forward completion events
|
|
34
|
+
private var playerDelegate: AudioPlayerDelegateImpl?
|
|
35
|
+
|
|
36
|
+
public init(eventDispatcher: NativeEventDispatcher) {
|
|
37
|
+
self.eventDispatcher = eventDispatcher
|
|
38
|
+
super.init()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
42
|
+
switch method {
|
|
43
|
+
case "play":
|
|
44
|
+
let uri = args.first as? String ?? ""
|
|
45
|
+
let options = (args.count > 1 ? args[1] as? [String: Any] : nil) ?? [:]
|
|
46
|
+
play(uri: uri, options: options, callback: callback)
|
|
47
|
+
|
|
48
|
+
case "pause":
|
|
49
|
+
pause(callback: callback)
|
|
50
|
+
|
|
51
|
+
case "resume":
|
|
52
|
+
resume(callback: callback)
|
|
53
|
+
|
|
54
|
+
case "stop":
|
|
55
|
+
stop(callback: callback)
|
|
56
|
+
|
|
57
|
+
case "seek":
|
|
58
|
+
let position = (args.first as? Double) ?? (args.first as? Int).map(Double.init) ?? 0
|
|
59
|
+
seek(position: position, callback: callback)
|
|
60
|
+
|
|
61
|
+
case "setVolume":
|
|
62
|
+
let volume = (args.first as? Double) ?? (args.first as? Int).map(Double.init) ?? 1.0
|
|
63
|
+
setVolume(Float(volume), callback: callback)
|
|
64
|
+
|
|
65
|
+
case "startRecording":
|
|
66
|
+
let options = args.first as? [String: Any] ?? [:]
|
|
67
|
+
startRecording(options: options, callback: callback)
|
|
68
|
+
|
|
69
|
+
case "stopRecording":
|
|
70
|
+
stopRecording(callback: callback)
|
|
71
|
+
|
|
72
|
+
case "pauseRecording":
|
|
73
|
+
pauseRecording(callback: callback)
|
|
74
|
+
|
|
75
|
+
case "resumeRecording":
|
|
76
|
+
resumeRecording(callback: callback)
|
|
77
|
+
|
|
78
|
+
case "getStatus":
|
|
79
|
+
getStatus(callback: callback)
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
callback(nil, "AudioModule: Unknown method '\(method)'")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Playback
|
|
87
|
+
|
|
88
|
+
private func play(uri: String, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
89
|
+
DispatchQueue.main.async { [weak self] in
|
|
90
|
+
guard let self = self else { return }
|
|
91
|
+
|
|
92
|
+
// Stop any existing playback
|
|
93
|
+
self.stopProgressReporting()
|
|
94
|
+
self.player?.stop()
|
|
95
|
+
|
|
96
|
+
guard let url = URL(string: uri) else {
|
|
97
|
+
callback(nil, "Invalid audio URI: \(uri)")
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if it's a remote URL — download first
|
|
102
|
+
if url.scheme == "http" || url.scheme == "https" {
|
|
103
|
+
self.downloadAndPlay(url: url, options: options, callback: callback)
|
|
104
|
+
} else {
|
|
105
|
+
self.playLocal(url: url, options: options, callback: callback)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func downloadAndPlay(url: URL, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
111
|
+
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
|
|
112
|
+
DispatchQueue.main.async {
|
|
113
|
+
guard let self = self else { return }
|
|
114
|
+
if let error = error {
|
|
115
|
+
callback(nil, "Failed to download audio: \(error.localizedDescription)")
|
|
116
|
+
self.eventDispatcher?.dispatchGlobalEvent("audio:error", payload: ["message": error.localizedDescription])
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
guard let data = data else {
|
|
120
|
+
callback(nil, "No audio data received")
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
do {
|
|
124
|
+
let player = try AVAudioPlayer(data: data)
|
|
125
|
+
self.setupPlayer(player, options: options, callback: callback)
|
|
126
|
+
} catch {
|
|
127
|
+
callback(nil, "Failed to initialize player: \(error.localizedDescription)")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}.resume()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func playLocal(url: URL, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
134
|
+
do {
|
|
135
|
+
let player = try AVAudioPlayer(contentsOf: url)
|
|
136
|
+
setupPlayer(player, options: options, callback: callback)
|
|
137
|
+
} catch {
|
|
138
|
+
callback(nil, "Failed to play audio: \(error.localizedDescription)")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func setupPlayer(_ player: AVAudioPlayer, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
143
|
+
let volume = Float(options["volume"] as? Double ?? 1.0)
|
|
144
|
+
let loop = options["loop"] as? Bool ?? false
|
|
145
|
+
|
|
146
|
+
player.volume = volume
|
|
147
|
+
player.numberOfLoops = loop ? -1 : 0
|
|
148
|
+
|
|
149
|
+
let delegate = AudioPlayerDelegateImpl { [weak self] successfully in
|
|
150
|
+
guard let self = self else { return }
|
|
151
|
+
self.isPlaying = false
|
|
152
|
+
self.stopProgressReporting()
|
|
153
|
+
DispatchQueue.main.async { [weak self] in
|
|
154
|
+
self?.eventDispatcher?.dispatchGlobalEvent("audio:complete", payload: [:])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
player.delegate = delegate
|
|
158
|
+
self.playerDelegate = delegate
|
|
159
|
+
self.player = player
|
|
160
|
+
|
|
161
|
+
player.prepareToPlay()
|
|
162
|
+
player.play()
|
|
163
|
+
self.isPlaying = true
|
|
164
|
+
self.startProgressReporting()
|
|
165
|
+
|
|
166
|
+
callback([
|
|
167
|
+
"duration": player.duration,
|
|
168
|
+
"currentTime": 0.0,
|
|
169
|
+
], nil)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func pause(callback: @escaping (Any?, String?) -> Void) {
|
|
173
|
+
DispatchQueue.main.async { [weak self] in
|
|
174
|
+
self?.player?.pause()
|
|
175
|
+
self?.isPlaying = false
|
|
176
|
+
self?.stopProgressReporting()
|
|
177
|
+
callback(nil, nil)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func resume(callback: @escaping (Any?, String?) -> Void) {
|
|
182
|
+
DispatchQueue.main.async { [weak self] in
|
|
183
|
+
guard let self = self else { return }
|
|
184
|
+
self.player?.play()
|
|
185
|
+
self.isPlaying = true
|
|
186
|
+
self.startProgressReporting()
|
|
187
|
+
callback(nil, nil)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func stop(callback: @escaping (Any?, String?) -> Void) {
|
|
192
|
+
DispatchQueue.main.async { [weak self] in
|
|
193
|
+
guard let self = self else { return }
|
|
194
|
+
self.player?.stop()
|
|
195
|
+
self.player = nil
|
|
196
|
+
self.playerDelegate = nil
|
|
197
|
+
self.isPlaying = false
|
|
198
|
+
self.stopProgressReporting()
|
|
199
|
+
callback(nil, nil)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func seek(position: Double, callback: @escaping (Any?, String?) -> Void) {
|
|
204
|
+
DispatchQueue.main.async { [weak self] in
|
|
205
|
+
self?.player?.currentTime = position
|
|
206
|
+
callback(nil, nil)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private func setVolume(_ volume: Float, callback: @escaping (Any?, String?) -> Void) {
|
|
211
|
+
DispatchQueue.main.async { [weak self] in
|
|
212
|
+
self?.player?.volume = max(0, min(1, volume))
|
|
213
|
+
callback(nil, nil)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// MARK: - Progress Reporting (Timer-based, cross-platform)
|
|
218
|
+
|
|
219
|
+
private func startProgressReporting() {
|
|
220
|
+
stopProgressReporting()
|
|
221
|
+
// Report at ~4 Hz using a Timer (cross-platform, no CADisplayLink dependency)
|
|
222
|
+
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
223
|
+
self?.reportProgress()
|
|
224
|
+
}
|
|
225
|
+
RunLoop.main.add(progressTimer!, forMode: .common)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private func stopProgressReporting() {
|
|
229
|
+
progressTimer?.invalidate()
|
|
230
|
+
progressTimer = nil
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func reportProgress() {
|
|
234
|
+
guard player != nil, isPlaying else { return }
|
|
235
|
+
DispatchQueue.main.async { [weak self] in
|
|
236
|
+
guard let self = self, let player = self.player else { return }
|
|
237
|
+
self.eventDispatcher?.dispatchGlobalEvent("audio:progress", payload: [
|
|
238
|
+
"currentTime": player.currentTime,
|
|
239
|
+
"duration": player.duration,
|
|
240
|
+
])
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// MARK: - Recording
|
|
245
|
+
|
|
246
|
+
private func startRecording(options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
247
|
+
DispatchQueue.main.async { [weak self] in
|
|
248
|
+
guard let self = self else { return }
|
|
249
|
+
|
|
250
|
+
let quality = options["quality"] as? String ?? "medium"
|
|
251
|
+
let format = options["format"] as? String ?? "m4a"
|
|
252
|
+
|
|
253
|
+
let ext = format == "wav" ? "wav" : "m4a"
|
|
254
|
+
let url = FileManager.default.temporaryDirectory
|
|
255
|
+
.appendingPathComponent(UUID().uuidString + ".\(ext)")
|
|
256
|
+
|
|
257
|
+
var settings: [String: Any] = [:]
|
|
258
|
+
if format == "wav" {
|
|
259
|
+
settings = [
|
|
260
|
+
AVFormatIDKey: Int(kAudioFormatLinearPCM),
|
|
261
|
+
AVSampleRateKey: quality == "high" ? 44100.0 : 22050.0,
|
|
262
|
+
AVNumberOfChannelsKey: 1,
|
|
263
|
+
AVLinearPCMBitDepthKey: 16,
|
|
264
|
+
AVLinearPCMIsFloatKey: false,
|
|
265
|
+
]
|
|
266
|
+
} else {
|
|
267
|
+
let sampleRate: Double
|
|
268
|
+
let bitRate: Int
|
|
269
|
+
switch quality {
|
|
270
|
+
case "low": sampleRate = 22050; bitRate = 32000
|
|
271
|
+
case "high": sampleRate = 44100; bitRate = 128000
|
|
272
|
+
default: sampleRate = 44100; bitRate = 64000
|
|
273
|
+
}
|
|
274
|
+
settings = [
|
|
275
|
+
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
|
276
|
+
AVSampleRateKey: sampleRate,
|
|
277
|
+
AVNumberOfChannelsKey: 1,
|
|
278
|
+
AVEncoderAudioQualityKey: AVAudioQuality.medium.rawValue,
|
|
279
|
+
AVEncoderBitRateKey: bitRate,
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
do {
|
|
284
|
+
let recorder = try AVAudioRecorder(url: url, settings: settings)
|
|
285
|
+
recorder.prepareToRecord()
|
|
286
|
+
recorder.record()
|
|
287
|
+
self.recorder = recorder
|
|
288
|
+
callback(["uri": url.absoluteString], nil)
|
|
289
|
+
} catch {
|
|
290
|
+
callback(nil, "Failed to start recording: \(error.localizedDescription)")
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private func stopRecording(callback: @escaping (Any?, String?) -> Void) {
|
|
296
|
+
DispatchQueue.main.async { [weak self] in
|
|
297
|
+
guard let self = self, let recorder = self.recorder else {
|
|
298
|
+
callback(nil, "No active recording")
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
let duration = recorder.currentTime
|
|
302
|
+
let uri = recorder.url.absoluteString
|
|
303
|
+
recorder.stop()
|
|
304
|
+
self.recorder = nil
|
|
305
|
+
callback(["uri": uri, "duration": duration], nil)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private func pauseRecording(callback: @escaping (Any?, String?) -> Void) {
|
|
310
|
+
DispatchQueue.main.async { [weak self] in
|
|
311
|
+
self?.recorder?.pause()
|
|
312
|
+
callback(nil, nil)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private func resumeRecording(callback: @escaping (Any?, String?) -> Void) {
|
|
317
|
+
DispatchQueue.main.async { [weak self] in
|
|
318
|
+
self?.recorder?.record()
|
|
319
|
+
callback(nil, nil)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// MARK: - Status
|
|
324
|
+
|
|
325
|
+
private func getStatus(callback: @escaping (Any?, String?) -> Void) {
|
|
326
|
+
DispatchQueue.main.async { [weak self] in
|
|
327
|
+
guard let self = self else { callback(nil, nil); return }
|
|
328
|
+
var status: [String: Any] = [
|
|
329
|
+
"isPlaying": self.isPlaying,
|
|
330
|
+
"isRecording": self.recorder?.isRecording ?? false,
|
|
331
|
+
]
|
|
332
|
+
if let player = self.player {
|
|
333
|
+
status["currentTime"] = player.currentTime
|
|
334
|
+
status["duration"] = player.duration
|
|
335
|
+
status["volume"] = player.volume
|
|
336
|
+
}
|
|
337
|
+
callback(status, nil)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// MARK: - AVAudioPlayerDelegate wrapper
|
|
343
|
+
|
|
344
|
+
private final class AudioPlayerDelegateImpl: NSObject, AVAudioPlayerDelegate {
|
|
345
|
+
private let onComplete: (Bool) -> Void
|
|
346
|
+
|
|
347
|
+
init(onComplete: @escaping (Bool) -> Void) {
|
|
348
|
+
self.onComplete = onComplete
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
352
|
+
onComplete(flag)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
|
|
356
|
+
// Treat decode error as completion failure
|
|
357
|
+
onComplete(false)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SQLite3
|
|
3
|
+
|
|
4
|
+
/// Native module for SQLite database access.
|
|
5
|
+
/// Uses the sqlite3 C API (built into both iOS and macOS, no external dependencies).
|
|
6
|
+
/// Supports multiple named databases, parameterized queries, and transactions.
|
|
7
|
+
public final class DatabaseModule: NativeModule {
|
|
8
|
+
public let moduleName = "Database"
|
|
9
|
+
|
|
10
|
+
/// Open database handles keyed by database name.
|
|
11
|
+
private var databases: [String: OpaquePointer] = [:]
|
|
12
|
+
|
|
13
|
+
/// Directory for database files.
|
|
14
|
+
private var dbDirectory: URL {
|
|
15
|
+
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
16
|
+
.appendingPathComponent("databases", isDirectory: true)
|
|
17
|
+
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
18
|
+
return dir
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public init() {}
|
|
22
|
+
|
|
23
|
+
public func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
24
|
+
switch method {
|
|
25
|
+
case "open":
|
|
26
|
+
let name = args.first as? String ?? "default"
|
|
27
|
+
open(name: name, callback: callback)
|
|
28
|
+
case "close":
|
|
29
|
+
let name = args.first as? String ?? "default"
|
|
30
|
+
close(name: name, callback: callback)
|
|
31
|
+
case "execute":
|
|
32
|
+
let name = args.count > 0 ? (args[0] as? String ?? "default") : "default"
|
|
33
|
+
let sql = args.count > 1 ? (args[1] as? String ?? "") : ""
|
|
34
|
+
let params = args.count > 2 ? (args[2] as? [Any] ?? []) : []
|
|
35
|
+
execute(name: name, sql: sql, params: params, callback: callback)
|
|
36
|
+
case "query":
|
|
37
|
+
let name = args.count > 0 ? (args[0] as? String ?? "default") : "default"
|
|
38
|
+
let sql = args.count > 1 ? (args[1] as? String ?? "") : ""
|
|
39
|
+
let params = args.count > 2 ? (args[2] as? [Any] ?? []) : []
|
|
40
|
+
query(name: name, sql: sql, params: params, callback: callback)
|
|
41
|
+
case "executeTransaction":
|
|
42
|
+
let name = args.count > 0 ? (args[0] as? String ?? "default") : "default"
|
|
43
|
+
let statements = args.count > 1 ? (args[1] as? [[String: Any]] ?? []) : []
|
|
44
|
+
executeTransaction(name: name, statements: statements, callback: callback)
|
|
45
|
+
default:
|
|
46
|
+
callback(nil, "DatabaseModule: unknown method '\(method)'")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MARK: - Open / Close
|
|
51
|
+
|
|
52
|
+
private func open(name: String, callback: @escaping (Any?, String?) -> Void) {
|
|
53
|
+
if databases[name] != nil {
|
|
54
|
+
callback(true, nil)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let path = dbDirectory.appendingPathComponent("\(name).sqlite").path
|
|
59
|
+
var db: OpaquePointer?
|
|
60
|
+
|
|
61
|
+
let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX
|
|
62
|
+
let result = sqlite3_open_v2(path, &db, flags, nil)
|
|
63
|
+
if result == SQLITE_OK, let db = db {
|
|
64
|
+
// Enable WAL mode for better concurrent read/write performance
|
|
65
|
+
sqlite3_exec(db, "PRAGMA journal_mode=WAL", nil, nil, nil)
|
|
66
|
+
databases[name] = db
|
|
67
|
+
callback(true, nil)
|
|
68
|
+
} else {
|
|
69
|
+
let errorMsg = db != nil ? String(cString: sqlite3_errmsg(db)) : "Unknown error"
|
|
70
|
+
if db != nil { sqlite3_close(db) }
|
|
71
|
+
callback(nil, "Failed to open database '\(name)': \(errorMsg)")
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func close(name: String, callback: @escaping (Any?, String?) -> Void) {
|
|
76
|
+
guard let db = databases.removeValue(forKey: name) else {
|
|
77
|
+
callback(nil, nil)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
sqlite3_close(db)
|
|
81
|
+
callback(nil, nil)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - Execute (INSERT, UPDATE, DELETE, CREATE TABLE, etc.)
|
|
85
|
+
|
|
86
|
+
private func execute(name: String, sql: String, params: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
87
|
+
guard let db = getOrOpen(name: name, callback: callback) else { return }
|
|
88
|
+
|
|
89
|
+
var stmt: OpaquePointer?
|
|
90
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
|
|
91
|
+
let err = String(cString: sqlite3_errmsg(db))
|
|
92
|
+
callback(nil, "SQL prepare error: \(err)")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
defer { sqlite3_finalize(stmt) }
|
|
96
|
+
|
|
97
|
+
bindParams(stmt: stmt!, params: params)
|
|
98
|
+
|
|
99
|
+
let stepResult = sqlite3_step(stmt)
|
|
100
|
+
if stepResult == SQLITE_DONE || stepResult == SQLITE_ROW {
|
|
101
|
+
let rowsAffected = sqlite3_changes(db)
|
|
102
|
+
let lastInsertId = sqlite3_last_insert_rowid(db)
|
|
103
|
+
var result: [String: Any] = ["rowsAffected": Int(rowsAffected)]
|
|
104
|
+
if lastInsertId > 0 {
|
|
105
|
+
result["insertId"] = Int(lastInsertId)
|
|
106
|
+
}
|
|
107
|
+
callback(result, nil)
|
|
108
|
+
} else {
|
|
109
|
+
let err = String(cString: sqlite3_errmsg(db))
|
|
110
|
+
callback(nil, "SQL execute error: \(err)")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - Query (SELECT)
|
|
115
|
+
|
|
116
|
+
private func query(name: String, sql: String, params: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
117
|
+
guard let db = getOrOpen(name: name, callback: callback) else { return }
|
|
118
|
+
|
|
119
|
+
var stmt: OpaquePointer?
|
|
120
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
|
|
121
|
+
let err = String(cString: sqlite3_errmsg(db))
|
|
122
|
+
callback(nil, "SQL prepare error: \(err)")
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
defer { sqlite3_finalize(stmt) }
|
|
126
|
+
|
|
127
|
+
bindParams(stmt: stmt!, params: params)
|
|
128
|
+
|
|
129
|
+
var rows: [[String: Any]] = []
|
|
130
|
+
let columnCount = sqlite3_column_count(stmt)
|
|
131
|
+
|
|
132
|
+
while sqlite3_step(stmt) == SQLITE_ROW {
|
|
133
|
+
var row: [String: Any] = [:]
|
|
134
|
+
for i in 0..<columnCount {
|
|
135
|
+
let colName = String(cString: sqlite3_column_name(stmt, i))
|
|
136
|
+
row[colName] = columnValue(stmt: stmt!, index: i)
|
|
137
|
+
}
|
|
138
|
+
rows.append(row)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
callback(rows, nil)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Transaction
|
|
145
|
+
|
|
146
|
+
private func executeTransaction(name: String, statements: [[String: Any]], callback: @escaping (Any?, String?) -> Void) {
|
|
147
|
+
guard let db = getOrOpen(name: name, callback: callback) else { return }
|
|
148
|
+
|
|
149
|
+
sqlite3_exec(db, "BEGIN TRANSACTION", nil, nil, nil)
|
|
150
|
+
|
|
151
|
+
var results: [[String: Any]] = []
|
|
152
|
+
|
|
153
|
+
for stmtData in statements {
|
|
154
|
+
let sql = stmtData["sql"] as? String ?? ""
|
|
155
|
+
let params = stmtData["params"] as? [Any] ?? []
|
|
156
|
+
|
|
157
|
+
var stmt: OpaquePointer?
|
|
158
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
|
|
159
|
+
let err = String(cString: sqlite3_errmsg(db))
|
|
160
|
+
sqlite3_exec(db, "ROLLBACK", nil, nil, nil)
|
|
161
|
+
callback(nil, "Transaction SQL prepare error: \(err)")
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
bindParams(stmt: stmt!, params: params)
|
|
166
|
+
|
|
167
|
+
let stepResult = sqlite3_step(stmt)
|
|
168
|
+
sqlite3_finalize(stmt)
|
|
169
|
+
|
|
170
|
+
if stepResult == SQLITE_DONE || stepResult == SQLITE_ROW {
|
|
171
|
+
let rowsAffected = sqlite3_changes(db)
|
|
172
|
+
let lastInsertId = sqlite3_last_insert_rowid(db)
|
|
173
|
+
var result: [String: Any] = ["rowsAffected": Int(rowsAffected)]
|
|
174
|
+
if lastInsertId > 0 {
|
|
175
|
+
result["insertId"] = Int(lastInsertId)
|
|
176
|
+
}
|
|
177
|
+
results.append(result)
|
|
178
|
+
} else {
|
|
179
|
+
let err = String(cString: sqlite3_errmsg(db))
|
|
180
|
+
sqlite3_exec(db, "ROLLBACK", nil, nil, nil)
|
|
181
|
+
callback(nil, "Transaction execute error: \(err)")
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
sqlite3_exec(db, "COMMIT", nil, nil, nil)
|
|
187
|
+
callback(results, nil)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// MARK: - Helpers
|
|
191
|
+
|
|
192
|
+
/// Get an existing database handle or auto-open it.
|
|
193
|
+
private func getOrOpen(name: String, callback: @escaping (Any?, String?) -> Void) -> OpaquePointer? {
|
|
194
|
+
if let db = databases[name] { return db }
|
|
195
|
+
|
|
196
|
+
// Auto-open
|
|
197
|
+
let path = dbDirectory.appendingPathComponent("\(name).sqlite").path
|
|
198
|
+
var db: OpaquePointer?
|
|
199
|
+
let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX
|
|
200
|
+
let result = sqlite3_open_v2(path, &db, flags, nil)
|
|
201
|
+
if result == SQLITE_OK, let db = db {
|
|
202
|
+
sqlite3_exec(db, "PRAGMA journal_mode=WAL", nil, nil, nil)
|
|
203
|
+
databases[name] = db
|
|
204
|
+
return db
|
|
205
|
+
} else {
|
|
206
|
+
let errorMsg = db != nil ? String(cString: sqlite3_errmsg(db)) : "Unknown error"
|
|
207
|
+
if db != nil { sqlite3_close(db) }
|
|
208
|
+
callback(nil, "Failed to auto-open database '\(name)': \(errorMsg)")
|
|
209
|
+
return nil
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Bind parameter array to a prepared statement. Supports String, Int, Double, Bool, nil/NSNull.
|
|
214
|
+
private func bindParams(stmt: OpaquePointer, params: [Any]) {
|
|
215
|
+
for (i, param) in params.enumerated() {
|
|
216
|
+
let idx = Int32(i + 1)
|
|
217
|
+
if param is NSNull {
|
|
218
|
+
sqlite3_bind_null(stmt, idx)
|
|
219
|
+
} else if let s = param as? String {
|
|
220
|
+
sqlite3_bind_text(stmt, idx, (s as NSString).utf8String, -1, unsafeBitCast(-1, to: sqlite3_destructor_type.self))
|
|
221
|
+
} else if let n = param as? Int {
|
|
222
|
+
sqlite3_bind_int64(stmt, idx, Int64(n))
|
|
223
|
+
} else if let d = param as? Double {
|
|
224
|
+
sqlite3_bind_double(stmt, idx, d)
|
|
225
|
+
} else if let b = param as? Bool {
|
|
226
|
+
sqlite3_bind_int(stmt, idx, b ? 1 : 0)
|
|
227
|
+
} else {
|
|
228
|
+
// Fallback: bind as text
|
|
229
|
+
let str = "\(param)"
|
|
230
|
+
sqlite3_bind_text(stmt, idx, (str as NSString).utf8String, -1, unsafeBitCast(-1, to: sqlite3_destructor_type.self))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Extract a column value from the current row of a statement.
|
|
236
|
+
private func columnValue(stmt: OpaquePointer, index: Int32) -> Any {
|
|
237
|
+
let type = sqlite3_column_type(stmt, index)
|
|
238
|
+
switch type {
|
|
239
|
+
case SQLITE_INTEGER:
|
|
240
|
+
return Int(sqlite3_column_int64(stmt, index))
|
|
241
|
+
case SQLITE_FLOAT:
|
|
242
|
+
return sqlite3_column_double(stmt, index)
|
|
243
|
+
case SQLITE_TEXT:
|
|
244
|
+
return String(cString: sqlite3_column_text(stmt, index))
|
|
245
|
+
case SQLITE_BLOB:
|
|
246
|
+
// Return blob as base64 string
|
|
247
|
+
if let data = sqlite3_column_blob(stmt, index) {
|
|
248
|
+
let bytes = sqlite3_column_bytes(stmt, index)
|
|
249
|
+
let d = Data(bytes: data, count: Int(bytes))
|
|
250
|
+
return d.base64EncodedString()
|
|
251
|
+
}
|
|
252
|
+
return NSNull()
|
|
253
|
+
case SQLITE_NULL:
|
|
254
|
+
return NSNull()
|
|
255
|
+
default:
|
|
256
|
+
return NSNull()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|