Squash merge feat/expo-app: app-expo, .cursor, workflows, package.json, .husky; remove app-android, app-ios, react-app

This commit is contained in:
Kevin
2026-03-19 01:12:17 +08:00
parent 9e4f301ab9
commit b4f4369b7d
544 changed files with 23707 additions and 67151 deletions

View File

@@ -0,0 +1,83 @@
import { setAudioModeAsync } from 'expo-audio';
type AudioOwner = 'recorder' | 'player' | null;
type OwnerChangeListener = (owner: AudioOwner) => void;
let currentOwner: AudioOwner = null;
const listeners = new Set<OwnerChangeListener>();
function notify() {
for (const listener of listeners) {
listener(currentOwner);
}
}
/**
* Global audio focus coordinator.
* Ensures recording and playback are mutually exclusive at the app level,
* not just within a single hook's lifecycle.
*/
export const audioFocus = {
async acquireForRecording(): Promise<boolean> {
if (currentOwner === 'recorder') return true;
if (currentOwner === 'player') {
await this.release();
}
await setAudioModeAsync({
playsInSilentMode: true,
allowsRecording: true,
});
currentOwner = 'recorder';
notify();
return true;
},
async acquireForPlayback(): Promise<boolean> {
if (currentOwner === 'player') return true;
if (currentOwner === 'recorder') {
return false;
}
await setAudioModeAsync({
playsInSilentMode: true,
allowsRecording: false,
});
currentOwner = 'player';
notify();
return true;
},
async release(): Promise<void> {
if (!currentOwner) return;
await setAudioModeAsync({
playsInSilentMode: false,
allowsRecording: false,
});
currentOwner = null;
notify();
},
getCurrentOwner(): AudioOwner {
return currentOwner;
},
isRecording(): boolean {
return currentOwner === 'recorder';
},
isPlaying(): boolean {
return currentOwner === 'player';
},
onOwnerChange(listener: OwnerChangeListener): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
},
} as const;