mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
* Upgrade to Expo 57 and unify Explorer across layouts * feat(app): retain Explorer across layout and appearance changes Keep one workspace Explorer host and destination-aware navigation so folding, theme updates and compact sheets preserve the current panel. Scope pull request discovery and timeline demand to active surfaces. * fix(app): keep Explorer resize handles stretched through the dock The shared frame moved the divider into an absolute column, leaving its horizontal handle without height. Restore the row so pointer drags reach it, preserving the existing resize and performance contract. * fix(app): keep Explorer drag widths out of the shared stylesheet Carry the existing inline sizing policy into the shared frame so pointer previews do not accumulate CSS rules. * fix(test): align isolated test runtimes with Expo 57 Hoisted React consumers need the matching DOM renderer at the repository root. Automated Metro processes must not install native DevTools into disposable homes, and development toast overlays must not cover compact actions. Keep Explorer journeys on the shared host and synchronize setup fixtures on daemon output rather than a one-second file-marker window. A delayed real child start reproduces the readiness failure on every platform. * fix(test): await retained shell layout after composer resize The gesture host now stays mounted across breakpoints, so its presence no longer identifies compact layout. Report the current layout on the retained host and synchronize composer journeys with that state. * fix(app): preserve browser interactions in retained workspace hosts Initially disabled gesture hosts still registered pointer and wheel listeners, while hover cards used window positions inside a workspace-local sheet portal. Respect initial gesture enablement and position cards relative to their measured floating host. Keep developer-toast dismissal independent of pointer input and replacement toast visibility so modal, hover, and focus checks retain their input state. * fix(test): share developer toast handling with Electron browser runs The standalone Electron browser runner bypassed the browser fixture, allowing developer errors to cover Settings. Use the same non-pointer dismissal in both runners while retaining their console logs.
230 lines
7.3 KiB
Swift
230 lines
7.3 KiB
Swift
import ExpoModulesCore
|
|
|
|
let ON_MIC_DATA_EVENT_NAME = "onMicrophoneData"
|
|
let ON_INPUT_VOLUME_LEVEL_EVENT_NAME = "onInputVolumeLevelData"
|
|
let ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME = "onOutputVolumeLevelData"
|
|
let ON_RECORDING_CHANGE_EVENT_NAME = "onRecordingChange"
|
|
let ON_AUDIO_INTERRUPTION_EVENT_NAME = "onAudioInterruption"
|
|
|
|
public class ExpoTwoWayAudioModule: Module {
|
|
private var audioEngine: AudioEngine?
|
|
public func definition() -> ModuleDefinition {
|
|
Name("ExpoTwoWayAudio")
|
|
|
|
OnCreate {
|
|
let permissionsManager = self.appContext?.permissions
|
|
EXPermissionsMethodsDelegate.register(
|
|
[
|
|
MicrophonePermissionRequester()
|
|
],
|
|
withPermissionsManager: permissionsManager
|
|
)
|
|
|
|
}
|
|
|
|
AsyncFunction("initialize") { () -> Bool in
|
|
do {
|
|
if self.audioEngine != nil {
|
|
return true
|
|
}
|
|
self.audioEngine = try AudioEngine()
|
|
self.setupMicrophoneCallback()
|
|
self.setupInputAudioLevelCallback()
|
|
self.setupOutputAudioLevelCallback()
|
|
self.setupAudioInterruptionCallback()
|
|
return true
|
|
} catch {
|
|
print("Failed to initialize AudioEngine: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Safety net for the JS-driven release in `stopCapture`/`processQueue`: never leave
|
|
// the non-mixing `.playAndRecord` session active while backgrounded, or iOS
|
|
// re-asserts it on the next foreground and kills the user's music again.
|
|
OnAppEntersBackground {
|
|
guard self.audioEngine?.isRecording == false else { return }
|
|
self.audioEngine?.releaseAudioSession()
|
|
}
|
|
|
|
Function("releaseAudioSession") {
|
|
self.audioEngine?.releaseAudioSession()
|
|
}
|
|
|
|
Function("isRecording") { () -> Bool in
|
|
guard let audioEngine = self.audioEngine else {
|
|
print("AudioEngine not initialized")
|
|
return false
|
|
}
|
|
return audioEngine.isRecording
|
|
}
|
|
|
|
Function("startDictation") { () -> Bool in
|
|
let isRecording = self.audioEngine?.toggleRecording(true) ?? false
|
|
self.sendEvent(ON_RECORDING_CHANGE_EVENT_NAME, ["data": isRecording])
|
|
return isRecording
|
|
}
|
|
|
|
Function("toggleRecording") { (val: Bool) -> Bool in
|
|
guard let audioEngine = self.audioEngine else {
|
|
print("AudioEngine not initialized")
|
|
return false
|
|
}
|
|
let isRecording = audioEngine.toggleRecording(val)
|
|
self.sendEvent(
|
|
ON_RECORDING_CHANGE_EVENT_NAME,
|
|
[
|
|
"data": isRecording
|
|
])
|
|
return isRecording
|
|
}
|
|
|
|
Function("getMicrophoneModeIOS") { () -> String in
|
|
if #available(iOS 15.0, *) {
|
|
let mode = AVCaptureDevice.preferredMicrophoneMode.rawValue
|
|
var micMode = ""
|
|
|
|
switch mode {
|
|
case 1:
|
|
micMode = MicrophoneMode.wideSpectrum.rawValue
|
|
case 2:
|
|
micMode = MicrophoneMode.voiceIsolation.rawValue
|
|
default:
|
|
micMode = MicrophoneMode.standard.rawValue
|
|
}
|
|
return micMode
|
|
}
|
|
print("Please update your ios")
|
|
|
|
return ""
|
|
}
|
|
|
|
Function("setMicrophoneModeIOS") {
|
|
if #available(iOS 15, *) {
|
|
if AVCaptureDevice.preferredMicrophoneMode != .voiceIsolation {
|
|
AVCaptureDevice.showSystemUserInterface(.microphoneModes)
|
|
}
|
|
} else {
|
|
print("This code only runs on iOS 14 and lower")
|
|
}
|
|
}
|
|
|
|
Function("tearDown") {
|
|
self.audioEngine?.tearDown()
|
|
self.audioEngine = nil
|
|
}
|
|
|
|
Function("restart") {
|
|
self.audioEngine?.resumeRecordingAndPlayer()
|
|
self.sendEvent(
|
|
ON_RECORDING_CHANGE_EVENT_NAME,
|
|
[
|
|
"data": audioEngine?.isRecording
|
|
])
|
|
|
|
}
|
|
|
|
Function("playPCMData") { (pcmData: Data) in
|
|
self.audioEngine?.playPCMData(pcmData)
|
|
}
|
|
|
|
Function("bypassVoiceProcessing") { (bypass: Bool) in
|
|
self.audioEngine?.bypassVoiceProcessing(bypass)
|
|
}
|
|
|
|
Function("isPlaying") { () -> Bool in
|
|
return self.audioEngine?.isPlaying ?? false
|
|
}
|
|
|
|
Function("stopPlayback") {
|
|
self.audioEngine?.stopPlayback()
|
|
}
|
|
|
|
Function("pausePlayback") {
|
|
self.audioEngine?.pausePlayback()
|
|
}
|
|
|
|
Function("resumePlayback") {
|
|
self.audioEngine?.resumePlayback()
|
|
}
|
|
|
|
AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in
|
|
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
|
|
self.appContext?.permissions,
|
|
withRequester: MicrophonePermissionRequester.self,
|
|
resolve: promise.legacyResolver,
|
|
reject: promise.legacyRejecter
|
|
)
|
|
}
|
|
|
|
AsyncFunction("requestMicrophonePermissionsAsync") { (promise: Promise) in
|
|
EXPermissionsMethodsDelegate.askForPermission(
|
|
withPermissionsManager: self.appContext?.permissions,
|
|
withRequester: MicrophonePermissionRequester.self,
|
|
resolve: promise.legacyResolver,
|
|
reject: promise.legacyRejecter
|
|
)
|
|
}
|
|
|
|
// Define the events that can be emitted
|
|
Events([
|
|
ON_MIC_DATA_EVENT_NAME,
|
|
ON_INPUT_VOLUME_LEVEL_EVENT_NAME,
|
|
ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME,
|
|
ON_RECORDING_CHANGE_EVENT_NAME,
|
|
ON_AUDIO_INTERRUPTION_EVENT_NAME,
|
|
])
|
|
}
|
|
|
|
private func setupMicrophoneCallback() {
|
|
audioEngine?.onMicDataCallback = { [weak self] data in
|
|
self?.sendEvent(
|
|
ON_MIC_DATA_EVENT_NAME,
|
|
[
|
|
"data": data
|
|
])
|
|
}
|
|
}
|
|
|
|
private func setupInputAudioLevelCallback() {
|
|
audioEngine?.onInputVolumeCallback = { [weak self] level in
|
|
self?.sendEvent(
|
|
ON_INPUT_VOLUME_LEVEL_EVENT_NAME,
|
|
[
|
|
"data": level
|
|
])
|
|
}
|
|
}
|
|
|
|
private func setupOutputAudioLevelCallback() {
|
|
audioEngine?.onOutputVolumeCallback = { [weak self] level in
|
|
self?.sendEvent(
|
|
ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME,
|
|
[
|
|
"data": level
|
|
])
|
|
}
|
|
}
|
|
|
|
private func setupAudioInterruptionCallback() {
|
|
audioEngine?.onAudioInterruptionCallback = { [weak self] data in
|
|
self?.sendEvent(
|
|
ON_AUDIO_INTERRUPTION_EVENT_NAME,
|
|
[
|
|
"data": data
|
|
])
|
|
self?.sendEvent(
|
|
ON_RECORDING_CHANGE_EVENT_NAME,
|
|
[
|
|
"data": self?.audioEngine?.isRecording
|
|
])
|
|
}
|
|
}
|
|
|
|
enum MicrophoneMode: String, Enumerable {
|
|
case standard
|
|
case voiceIsolation
|
|
case wideSpectrum
|
|
}
|
|
}
|