expo-pro-video-editor 0.2.5 → 0.2.7

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.
@@ -48,5 +48,13 @@ export declare function VoiceRecorder(props: {
48
48
  };
49
49
  /** Sheet container to present in — defaults to a plain `Modal`. */
50
50
  Container?: VoiceRecorderContainer;
51
+ /**
52
+ * Set when embedding this inside a caller-provided sheet component that
53
+ * already supplies its own background, padding, and rounded top corners
54
+ * (e.g. via a custom `Container`) — drops this component's own matching
55
+ * styling so the two don't stack into two slightly different, visibly
56
+ * seamed surfaces. Leave unset for the default `Modal` presentation.
57
+ */
58
+ bare?: boolean;
51
59
  }): import("react").JSX.Element;
52
60
  //# sourceMappingURL=VoiceRecorder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceRecorder.d.ts","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAuCtD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC,CAAC;AAUH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAC9B;;4DAEwD;IACxD,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB;;4EAEoE;QACpE,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC;KACnE,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,sBAAsB,CAAC;CACpC,+BA6KA"}
1
+ {"version":3,"file":"VoiceRecorder.d.ts","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqDtD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC,CAAC;AAUH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAC9B;;4DAEwD;IACxD,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB;;4EAEoE;QACpE,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,KAAK,IAAI,CAAC;KACnE,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,+BA6KA"}
@@ -9,6 +9,20 @@ const styles = {
9
9
  backdrop: tv({ base: 'flex-1 justify-end bg-black/45' }),
10
10
  sheet: tv({
11
11
  base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',
12
+ variants: {
13
+ // For a caller embedding this inside their own sheet component (which
14
+ // already provides its own background, rounded top corners, and
15
+ // vertical spacing) — painting both stacks two slightly different
16
+ // surfaces with a visible seam between them (this component's own
17
+ // white/dark box on top of the outer sheet's own themed background).
18
+ // `bare` drops the background/rounding/vertical padding this
19
+ // component would otherwise add on top of the outer sheet's own, but
20
+ // keeps horizontal padding — the outer sheet only supplies its own
21
+ // top/bottom inset (e.g. handle spacing, safe-area), not left/right,
22
+ // so this content still needs its own side margins from the sheet's
23
+ // edges.
24
+ bare: { true: 'gap-4 bg-transparent px-5 py-0' },
25
+ },
12
26
  }),
13
27
  title: tv({ base: 'text-base font-semibold text-video-editor-text' }),
14
28
  errorText: tv({ base: 'text-center text-[13px] text-video-editor-text-muted' }),
@@ -51,7 +65,7 @@ function DefaultContainer(props) {
51
65
  * to a live mic-amplitude waveform — no video required.
52
66
  */
53
67
  export function VoiceRecorder(props) {
54
- const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer } = props;
68
+ const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false } = props;
55
69
  const [permissionError, setPermissionError] = useState(null);
56
70
  const [recordedUri, setRecordedUri] = useState(null);
57
71
  const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);
@@ -172,7 +186,7 @@ export function VoiceRecorder(props) {
172
186
  }
173
187
  return (<Container visible={visible} onRequestClose={onCancel}>
174
188
  {backgroundContent}
175
- <View className={styles.sheet()}>
189
+ <View className={styles.sheet({ bare })}>
176
190
  <Text className={styles.title()}>{title}</Text>
177
191
 
178
192
  {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceRecorder.js","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync, useAudioRecorder, useAudioRecorderState } from 'expo-audio';\nimport { Mic, Square } from 'lucide-react-native';\nimport type { ComponentType, ReactNode } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Modal, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport { RecordingIndicator } from './RecordingIndicator';\n\nconst RECORDING_OPTIONS = { ...RecordingPresets.HIGH_QUALITY, isMeteringEnabled: true };\n\nconst styles = {\n backdrop: tv({ base: 'flex-1 justify-end bg-black/45' }),\n sheet: tv({\n base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',\n }),\n title: tv({ base: 'text-base font-semibold text-video-editor-text' }),\n errorText: tv({ base: 'text-center text-[13px] text-video-editor-text-muted' }),\n controlsRow: tv({ base: 'flex-row items-center justify-center gap-6 py-2' }),\n controlSpacer: tv({ base: 'w-10' }),\n recordButtonRing: tv({\n base: 'h-[76px] w-[76px] items-center justify-center rounded-full border-4 border-video-editor-border',\n variants: { disabled: { true: 'opacity-50' } },\n }),\n recordCircle: tv({\n base: 'h-[60px] w-[60px] items-center justify-center rounded-full bg-video-editor-accent',\n variants: { disabled: { true: 'bg-video-editor-bar-unplayed' } },\n }),\n discardButton: tv({\n base: 'h-10 w-10 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-video-editor-surface',\n }),\n discardButtonText: tv({ base: 'text-base text-video-editor-text' }),\n bottomBar: tv({ base: 'flex-row items-center justify-between' }),\n cancelText: tv({ base: 'text-sm text-video-editor-text' }),\n doneButton: tv({\n base: 'h-[38px] items-center justify-center rounded-full bg-video-editor-accent px-5',\n variants: { disabled: { true: 'opacity-40' } },\n }),\n doneButtonText: tv({ base: 'text-sm font-semibold text-white' }),\n};\n\nexport type VoiceRecorderResult = {\n uri: string;\n durationSeconds: number;\n};\n\n/**\n * The container a `VoiceRecorder` presents in — defaults to a plain React\n * Native `Modal` rendered as a bottom sheet. Pass a real bottom-sheet\n * component (e.g. `@gorhom/bottom-sheet`'s `BottomSheetModal`, or a wrapper\n * like `@niibase/bottom-sheet-manager`'s `BottomSheet`) to get native sheet\n * behavior (drag-to-dismiss, snap points) instead — this package has no\n * opinion on which sheet library you use, and doesn't depend on one.\n */\nexport type VoiceRecorderContainer = ComponentType<{\n visible: boolean;\n onRequestClose: () => void;\n children: ReactNode;\n}>;\n\nfunction DefaultContainer(props: { visible: boolean; onRequestClose: () => void; children: ReactNode }) {\n return (\n <Modal visible={props.visible} transparent animationType=\"slide\" onRequestClose={props.onRequestClose}>\n <View className={styles.backdrop()}>{props.children}</View>\n </Modal>\n );\n}\n\n/**\n * A record/stop/discard/done voice recorder, presented as a bottom sheet.\n * Renders on top of whatever the caller passes as `backgroundContent` (e.g.\n * an editor's own video preview, kept alive and playing behind the sheet) —\n * or nothing, for a recorder with no visual context (e.g. a chat app).\n *\n * `progress`, if provided, drives a `RecordingIndicator` in `'filmstrip'`\n * mode (a video's own frames with a moving playhead) so the user can see a\n * fixed-length recording's end coming. Without it, the indicator falls back\n * to a live mic-amplitude waveform — no video required.\n */\nexport function VoiceRecorder(props: {\n visible: boolean;\n onCancel: () => void;\n onConfirm: (result: VoiceRecorderResult) => void;\n title?: string;\n width: number;\n backgroundContent?: ReactNode;\n /** Ties the recording indicator to a video's own playhead (filmstrip\n * mode) and auto-stops the take once the video reaches its end. Omit for\n * a plain live-amplitude waveform with no fixed end. */\n progress?: {\n videoUri: string | null;\n currentTime: number;\n totalDuration: number;\n /** Called every render while recording so the caller can drive its own\n * video player in lockstep (play on record, pause/rewind on\n * stop/discard) — this component has no video player of its own. */\n onPlaybackRequest: (action: 'play' | 'pause' | 'restart') => void;\n };\n /** Sheet container to present in — defaults to a plain `Modal`. */\n Container?: VoiceRecorderContainer;\n}) {\n const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer } = props;\n const [permissionError, setPermissionError] = useState<string | null>(null);\n const [recordedUri, setRecordedUri] = useState<string | null>(null);\n const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);\n /**\n * Where the filmstrip's played-overlay should freeze once a take is\n * stopped. The live `progress.currentTime` keeps moving after that — a\n * caller's own trim-preview effect may rewind its player once it reaches\n * the end — so the overlay needs its own frozen snapshot instead of\n * reading the live position, to stay put where the recording actually\n * ended.\n */\n const [frozenTime, setFrozenTime] = useState<number | null>(null);\n\n const recorder = useAudioRecorder(RECORDING_OPTIONS);\n const recorderState = useAudioRecorderState(recorder, 100);\n\n useEffect(() => {\n if (!visible) return;\n let cancelled = false;\n\n requestRecordingPermissionsAsync().then(async ({ granted }) => {\n if (cancelled) return;\n if (!granted) {\n setPermissionError('Microphone access is required to record.');\n return;\n }\n /**\n * The audio session defaults to `allowsRecording: false` — record()\n * rejects with RecordingDisabledException until this is set, even\n * with the OS permission already granted. `playsInSilentMode` keeps\n * any background playback and the mic recording both alive together\n * rather than one silencing the other.\n */\n await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });\n });\n\n return () => {\n cancelled = true;\n };\n }, [visible]);\n\n /**\n * Reset to a clean slate every time the sheet opens, computed during\n * render (not an effect) since this component stays mounted across\n * open/close rather than remounting.\n */\n const [wasVisible, setWasVisible] = useState(visible);\n if (visible !== wasVisible) {\n setWasVisible(visible);\n if (visible) {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n }\n }\n\n // Don't start playing anything until the user actually presses record.\n useEffect(() => {\n if (!visible) return;\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n }, [visible]);\n\n async function stopRecording() {\n setFrozenTime(progress?.currentTime ?? 0);\n await recorder.stop();\n progress?.onPlaybackRequest('pause');\n setRecordedUri(recorder.uri);\n setRecordedDurationSeconds(recorderState.durationMillis / 1000);\n }\n\n async function handleToggleRecording() {\n if (recorderState.isRecording) {\n await stopRecording();\n return;\n }\n\n setRecordedUri(null);\n setFrozenTime(null);\n /**\n * Fully pause/rewind before asking AVAudioRecorder to prepare — starting\n * playback and preparing the recorder in the same tick can leave the two\n * contending for the audio session on-device (\"Failed to prepare\n * recorder\"). Only once the recorder is confirmed ready do both\n * playback and recording start together.\n */\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n /**\n * Re-passing the preset (instead of calling prepareToRecordAsync() bare)\n * forces the native side to build a brand-new AVAudioRecorder at a fresh\n * file path every take. With no options, prepare() reuses the same\n * recorder instance from the previous take at its original file — after\n * a take had already been stopped once, re-preparing that same instance\n * produced a file with no audio data on the second take.\n */\n await recorder.prepareToRecordAsync(RECORDING_OPTIONS);\n recorder.record();\n progress?.onPlaybackRequest('play');\n }\n\n /**\n * A fixed-length recording context (e.g. narrating over a video) auto-stops\n * the take once that context reaches its end, capping it at exactly one\n * pass instead of freezing progress without ever stopping the recording.\n */\n useEffect(() => {\n if (!visible || !progress || !recorderState.isRecording) return;\n if (progress.totalDuration > 0 && progress.currentTime >= progress.totalDuration - 0.05) {\n queueMicrotask(stopRecording);\n }\n }, [visible, recorderState.isRecording, progress?.currentTime, progress?.totalDuration]);\n\n function handleDiscard() {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n progress?.onPlaybackRequest('restart');\n progress?.onPlaybackRequest('play');\n }\n\n function handleDone() {\n if (!recordedUri) return;\n onConfirm({ uri: recordedUri, durationSeconds: recordedDurationSeconds });\n }\n\n return (\n <Container visible={visible} onRequestClose={onCancel}>\n {backgroundContent}\n <View className={styles.sheet()}>\n <Text className={styles.title()}>{title}</Text>\n\n {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}\n\n {progress ? (\n <RecordingIndicator\n mode=\"filmstrip\"\n uri={progress.videoUri}\n durationSeconds={progress.totalDuration}\n currentTime={frozenTime ?? progress.currentTime}\n width={width}\n />\n ) : (\n <RecordingIndicator mode=\"waveform\" metering={recorderState.metering} isRecording={recorderState.isRecording} width={width} />\n )}\n\n <View className={styles.controlsRow()}>\n {recordedUri ? (\n <>\n <View className={styles.controlSpacer()} />\n <RecordButton isRecording={false} disabled onPress={handleToggleRecording} />\n <TouchableOpacity className={styles.discardButton()} onPress={handleDiscard}>\n <Text className={styles.discardButtonText()}>✕</Text>\n </TouchableOpacity>\n </>\n ) : (\n <RecordButton isRecording={recorderState.isRecording} disabled={!!permissionError} onPress={handleToggleRecording} />\n )}\n </View>\n\n <View className={styles.bottomBar()}>\n <TouchableOpacity onPress={onCancel}>\n <Text className={styles.cancelText()}>Cancel</Text>\n </TouchableOpacity>\n <TouchableOpacity className={styles.doneButton({ disabled: !recordedUri })} onPress={handleDone} disabled={!recordedUri}>\n <Text className={styles.doneButtonText()}>Done</Text>\n </TouchableOpacity>\n </View>\n </View>\n </Container>\n );\n}\n\nconst ICON_COLOR = '#ffffff';\n\nfunction RecordButton(props: { isRecording: boolean; disabled: boolean; onPress: () => void }) {\n const { isRecording, disabled, onPress } = props;\n return (\n <TouchableOpacity className={styles.recordButtonRing({ disabled })} onPress={onPress} disabled={disabled}>\n {isRecording ? (\n <View className={styles.recordCircle()}>\n <Square size={20} color={ICON_COLOR} fill={ICON_COLOR} />\n </View>\n ) : (\n <View className={styles.recordCircle({ disabled })}>\n <Mic size={26} color={ICON_COLOR} />\n </View>\n )}\n </TouchableOpacity>\n );\n}\n"]}
1
+ {"version":3,"file":"VoiceRecorder.js","sourceRoot":"","sources":["../../src/ui/VoiceRecorder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync, useAudioRecorder, useAudioRecorderState } from 'expo-audio';\nimport { Mic, Square } from 'lucide-react-native';\nimport type { ComponentType, ReactNode } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Modal, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport { RecordingIndicator } from './RecordingIndicator';\n\nconst RECORDING_OPTIONS = { ...RecordingPresets.HIGH_QUALITY, isMeteringEnabled: true };\n\nconst styles = {\n backdrop: tv({ base: 'flex-1 justify-end bg-black/45' }),\n sheet: tv({\n base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',\n variants: {\n // For a caller embedding this inside their own sheet component (which\n // already provides its own background, rounded top corners, and\n // vertical spacing) — painting both stacks two slightly different\n // surfaces with a visible seam between them (this component's own\n // white/dark box on top of the outer sheet's own themed background).\n // `bare` drops the background/rounding/vertical padding this\n // component would otherwise add on top of the outer sheet's own, but\n // keeps horizontal padding — the outer sheet only supplies its own\n // top/bottom inset (e.g. handle spacing, safe-area), not left/right,\n // so this content still needs its own side margins from the sheet's\n // edges.\n bare: { true: 'gap-4 bg-transparent px-5 py-0' },\n },\n }),\n title: tv({ base: 'text-base font-semibold text-video-editor-text' }),\n errorText: tv({ base: 'text-center text-[13px] text-video-editor-text-muted' }),\n controlsRow: tv({ base: 'flex-row items-center justify-center gap-6 py-2' }),\n controlSpacer: tv({ base: 'w-10' }),\n recordButtonRing: tv({\n base: 'h-[76px] w-[76px] items-center justify-center rounded-full border-4 border-video-editor-border',\n variants: { disabled: { true: 'opacity-50' } },\n }),\n recordCircle: tv({\n base: 'h-[60px] w-[60px] items-center justify-center rounded-full bg-video-editor-accent',\n variants: { disabled: { true: 'bg-video-editor-bar-unplayed' } },\n }),\n discardButton: tv({\n base: 'h-10 w-10 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-video-editor-surface',\n }),\n discardButtonText: tv({ base: 'text-base text-video-editor-text' }),\n bottomBar: tv({ base: 'flex-row items-center justify-between' }),\n cancelText: tv({ base: 'text-sm text-video-editor-text' }),\n doneButton: tv({\n base: 'h-[38px] items-center justify-center rounded-full bg-video-editor-accent px-5',\n variants: { disabled: { true: 'opacity-40' } },\n }),\n doneButtonText: tv({ base: 'text-sm font-semibold text-white' }),\n};\n\nexport type VoiceRecorderResult = {\n uri: string;\n durationSeconds: number;\n};\n\n/**\n * The container a `VoiceRecorder` presents in — defaults to a plain React\n * Native `Modal` rendered as a bottom sheet. Pass a real bottom-sheet\n * component (e.g. `@gorhom/bottom-sheet`'s `BottomSheetModal`, or a wrapper\n * like `@niibase/bottom-sheet-manager`'s `BottomSheet`) to get native sheet\n * behavior (drag-to-dismiss, snap points) instead — this package has no\n * opinion on which sheet library you use, and doesn't depend on one.\n */\nexport type VoiceRecorderContainer = ComponentType<{\n visible: boolean;\n onRequestClose: () => void;\n children: ReactNode;\n}>;\n\nfunction DefaultContainer(props: { visible: boolean; onRequestClose: () => void; children: ReactNode }) {\n return (\n <Modal visible={props.visible} transparent animationType=\"slide\" onRequestClose={props.onRequestClose}>\n <View className={styles.backdrop()}>{props.children}</View>\n </Modal>\n );\n}\n\n/**\n * A record/stop/discard/done voice recorder, presented as a bottom sheet.\n * Renders on top of whatever the caller passes as `backgroundContent` (e.g.\n * an editor's own video preview, kept alive and playing behind the sheet) —\n * or nothing, for a recorder with no visual context (e.g. a chat app).\n *\n * `progress`, if provided, drives a `RecordingIndicator` in `'filmstrip'`\n * mode (a video's own frames with a moving playhead) so the user can see a\n * fixed-length recording's end coming. Without it, the indicator falls back\n * to a live mic-amplitude waveform — no video required.\n */\nexport function VoiceRecorder(props: {\n visible: boolean;\n onCancel: () => void;\n onConfirm: (result: VoiceRecorderResult) => void;\n title?: string;\n width: number;\n backgroundContent?: ReactNode;\n /** Ties the recording indicator to a video's own playhead (filmstrip\n * mode) and auto-stops the take once the video reaches its end. Omit for\n * a plain live-amplitude waveform with no fixed end. */\n progress?: {\n videoUri: string | null;\n currentTime: number;\n totalDuration: number;\n /** Called every render while recording so the caller can drive its own\n * video player in lockstep (play on record, pause/rewind on\n * stop/discard) — this component has no video player of its own. */\n onPlaybackRequest: (action: 'play' | 'pause' | 'restart') => void;\n };\n /** Sheet container to present in — defaults to a plain `Modal`. */\n Container?: VoiceRecorderContainer;\n /**\n * Set when embedding this inside a caller-provided sheet component that\n * already supplies its own background, padding, and rounded top corners\n * (e.g. via a custom `Container`) — drops this component's own matching\n * styling so the two don't stack into two slightly different, visibly\n * seamed surfaces. Leave unset for the default `Modal` presentation.\n */\n bare?: boolean;\n}) {\n const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false } = props;\n const [permissionError, setPermissionError] = useState<string | null>(null);\n const [recordedUri, setRecordedUri] = useState<string | null>(null);\n const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);\n /**\n * Where the filmstrip's played-overlay should freeze once a take is\n * stopped. The live `progress.currentTime` keeps moving after that — a\n * caller's own trim-preview effect may rewind its player once it reaches\n * the end — so the overlay needs its own frozen snapshot instead of\n * reading the live position, to stay put where the recording actually\n * ended.\n */\n const [frozenTime, setFrozenTime] = useState<number | null>(null);\n\n const recorder = useAudioRecorder(RECORDING_OPTIONS);\n const recorderState = useAudioRecorderState(recorder, 100);\n\n useEffect(() => {\n if (!visible) return;\n let cancelled = false;\n\n requestRecordingPermissionsAsync().then(async ({ granted }) => {\n if (cancelled) return;\n if (!granted) {\n setPermissionError('Microphone access is required to record.');\n return;\n }\n /**\n * The audio session defaults to `allowsRecording: false` — record()\n * rejects with RecordingDisabledException until this is set, even\n * with the OS permission already granted. `playsInSilentMode` keeps\n * any background playback and the mic recording both alive together\n * rather than one silencing the other.\n */\n await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });\n });\n\n return () => {\n cancelled = true;\n };\n }, [visible]);\n\n /**\n * Reset to a clean slate every time the sheet opens, computed during\n * render (not an effect) since this component stays mounted across\n * open/close rather than remounting.\n */\n const [wasVisible, setWasVisible] = useState(visible);\n if (visible !== wasVisible) {\n setWasVisible(visible);\n if (visible) {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n }\n }\n\n // Don't start playing anything until the user actually presses record.\n useEffect(() => {\n if (!visible) return;\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n }, [visible]);\n\n async function stopRecording() {\n setFrozenTime(progress?.currentTime ?? 0);\n await recorder.stop();\n progress?.onPlaybackRequest('pause');\n setRecordedUri(recorder.uri);\n setRecordedDurationSeconds(recorderState.durationMillis / 1000);\n }\n\n async function handleToggleRecording() {\n if (recorderState.isRecording) {\n await stopRecording();\n return;\n }\n\n setRecordedUri(null);\n setFrozenTime(null);\n /**\n * Fully pause/rewind before asking AVAudioRecorder to prepare — starting\n * playback and preparing the recorder in the same tick can leave the two\n * contending for the audio session on-device (\"Failed to prepare\n * recorder\"). Only once the recorder is confirmed ready do both\n * playback and recording start together.\n */\n progress?.onPlaybackRequest('pause');\n progress?.onPlaybackRequest('restart');\n /**\n * Re-passing the preset (instead of calling prepareToRecordAsync() bare)\n * forces the native side to build a brand-new AVAudioRecorder at a fresh\n * file path every take. With no options, prepare() reuses the same\n * recorder instance from the previous take at its original file — after\n * a take had already been stopped once, re-preparing that same instance\n * produced a file with no audio data on the second take.\n */\n await recorder.prepareToRecordAsync(RECORDING_OPTIONS);\n recorder.record();\n progress?.onPlaybackRequest('play');\n }\n\n /**\n * A fixed-length recording context (e.g. narrating over a video) auto-stops\n * the take once that context reaches its end, capping it at exactly one\n * pass instead of freezing progress without ever stopping the recording.\n */\n useEffect(() => {\n if (!visible || !progress || !recorderState.isRecording) return;\n if (progress.totalDuration > 0 && progress.currentTime >= progress.totalDuration - 0.05) {\n queueMicrotask(stopRecording);\n }\n }, [visible, recorderState.isRecording, progress?.currentTime, progress?.totalDuration]);\n\n function handleDiscard() {\n setRecordedUri(null);\n setRecordedDurationSeconds(0);\n setFrozenTime(null);\n progress?.onPlaybackRequest('restart');\n progress?.onPlaybackRequest('play');\n }\n\n function handleDone() {\n if (!recordedUri) return;\n onConfirm({ uri: recordedUri, durationSeconds: recordedDurationSeconds });\n }\n\n return (\n <Container visible={visible} onRequestClose={onCancel}>\n {backgroundContent}\n <View className={styles.sheet({ bare })}>\n <Text className={styles.title()}>{title}</Text>\n\n {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}\n\n {progress ? (\n <RecordingIndicator\n mode=\"filmstrip\"\n uri={progress.videoUri}\n durationSeconds={progress.totalDuration}\n currentTime={frozenTime ?? progress.currentTime}\n width={width}\n />\n ) : (\n <RecordingIndicator mode=\"waveform\" metering={recorderState.metering} isRecording={recorderState.isRecording} width={width} />\n )}\n\n <View className={styles.controlsRow()}>\n {recordedUri ? (\n <>\n <View className={styles.controlSpacer()} />\n <RecordButton isRecording={false} disabled onPress={handleToggleRecording} />\n <TouchableOpacity className={styles.discardButton()} onPress={handleDiscard}>\n <Text className={styles.discardButtonText()}>✕</Text>\n </TouchableOpacity>\n </>\n ) : (\n <RecordButton isRecording={recorderState.isRecording} disabled={!!permissionError} onPress={handleToggleRecording} />\n )}\n </View>\n\n <View className={styles.bottomBar()}>\n <TouchableOpacity onPress={onCancel}>\n <Text className={styles.cancelText()}>Cancel</Text>\n </TouchableOpacity>\n <TouchableOpacity className={styles.doneButton({ disabled: !recordedUri })} onPress={handleDone} disabled={!recordedUri}>\n <Text className={styles.doneButtonText()}>Done</Text>\n </TouchableOpacity>\n </View>\n </View>\n </Container>\n );\n}\n\nconst ICON_COLOR = '#ffffff';\n\nfunction RecordButton(props: { isRecording: boolean; disabled: boolean; onPress: () => void }) {\n const { isRecording, disabled, onPress } = props;\n return (\n <TouchableOpacity className={styles.recordButtonRing({ disabled })} onPress={onPress} disabled={disabled}>\n {isRecording ? (\n <View className={styles.recordCircle()}>\n <Square size={20} color={ICON_COLOR} fill={ICON_COLOR} />\n </View>\n ) : (\n <View className={styles.recordCircle({ disabled })}>\n <Mic size={26} color={ICON_COLOR} />\n </View>\n )}\n </TouchableOpacity>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-pro-video-editor",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Native video composition (trim, filters, text/image overlays, audio mixing) for Expo/React Native, adapted from pro_video_editor's AVFoundation and Media3 Transformer implementation.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -13,6 +13,20 @@ const styles = {
13
13
  backdrop: tv({ base: 'flex-1 justify-end bg-black/45' }),
14
14
  sheet: tv({
15
15
  base: 'gap-4 rounded-t-2xl bg-white p-5 dark:bg-video-editor-surface',
16
+ variants: {
17
+ // For a caller embedding this inside their own sheet component (which
18
+ // already provides its own background, rounded top corners, and
19
+ // vertical spacing) — painting both stacks two slightly different
20
+ // surfaces with a visible seam between them (this component's own
21
+ // white/dark box on top of the outer sheet's own themed background).
22
+ // `bare` drops the background/rounding/vertical padding this
23
+ // component would otherwise add on top of the outer sheet's own, but
24
+ // keeps horizontal padding — the outer sheet only supplies its own
25
+ // top/bottom inset (e.g. handle spacing, safe-area), not left/right,
26
+ // so this content still needs its own side margins from the sheet's
27
+ // edges.
28
+ bare: { true: 'gap-4 bg-transparent px-5 py-0' },
29
+ },
16
30
  }),
17
31
  title: tv({ base: 'text-base font-semibold text-video-editor-text' }),
18
32
  errorText: tv({ base: 'text-center text-[13px] text-video-editor-text-muted' }),
@@ -98,8 +112,16 @@ export function VoiceRecorder(props: {
98
112
  };
99
113
  /** Sheet container to present in — defaults to a plain `Modal`. */
100
114
  Container?: VoiceRecorderContainer;
115
+ /**
116
+ * Set when embedding this inside a caller-provided sheet component that
117
+ * already supplies its own background, padding, and rounded top corners
118
+ * (e.g. via a custom `Container`) — drops this component's own matching
119
+ * styling so the two don't stack into two slightly different, visibly
120
+ * seamed surfaces. Leave unset for the default `Modal` presentation.
121
+ */
122
+ bare?: boolean;
101
123
  }) {
102
- const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer } = props;
124
+ const { visible, onCancel, onConfirm, title = 'Voice-over', width, backgroundContent, progress, Container = DefaultContainer, bare = false } = props;
103
125
  const [permissionError, setPermissionError] = useState<string | null>(null);
104
126
  const [recordedUri, setRecordedUri] = useState<string | null>(null);
105
127
  const [recordedDurationSeconds, setRecordedDurationSeconds] = useState(0);
@@ -229,7 +251,7 @@ export function VoiceRecorder(props: {
229
251
  return (
230
252
  <Container visible={visible} onRequestClose={onCancel}>
231
253
  {backgroundContent}
232
- <View className={styles.sheet()}>
254
+ <View className={styles.sheet({ bare })}>
233
255
  <Text className={styles.title()}>{title}</Text>
234
256
 
235
257
  {permissionError ? <Text className={styles.errorText()}>{permissionError}</Text> : null}