CameraCaptureModal: open with options

This commit is contained in:
Enrico Ros
2026-03-10 17:04:58 -07:00
parent 18862c0ff4
commit f800bb8dae
3 changed files with 44 additions and 11 deletions
@@ -1,6 +1,7 @@
import * as React from 'react';
import type { FileWithHandle } from 'browser-fs-access';
import type { CameraCaptureDialogOptions } from '~/common/components/camera/useCameraCaptureDialog';
import type { CameraLiveStream } from '~/common/components/camera/useCameraCapture';
import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore';
import { useCameraCaptureDialog } from '~/common/components/camera/useCameraCaptureDialog';
@@ -12,7 +13,7 @@ import { useWebAttachmentModal } from './useWebAttachmentModal';
// Focused hooks that bridge `useAttachmentDrafts` return values to UI callback shapes.
// Each hook wraps one attachment source. Consumers compose only what they need.
type _HandleCameraOpen = () => Promise<void>;
type _HandleCameraOpen = (options?: CameraCaptureDialogOptions) => Promise<void>;
type _HandleFiles = (files: FileWithHandle[], errorMessage: string | null) => void;
type _HandlePasteIntercept = (event: React.ClipboardEvent) => void;
type _HandleScreenCapture = (file: File) => void;
@@ -24,15 +25,18 @@ type _HandleWebLinks = (links: { url: string }[]) => void;
*/
export function useAttachHandler_CameraOpen(
attachAppendFile: AttachmentDraftsApi['attachAppendFile'],
handleLiveStream?: (stream: CameraLiveStream) => void
handleLiveStream?: (stream: CameraLiveStream) => void,
): _HandleCameraOpen {
// external state
const { openCameraCapture } = useCameraCaptureDialog(); // -> showPromisedOverlay
return React.useCallback<_HandleCameraOpen>(async () => {
return React.useCallback(async (optionsOrEvent?: CameraCaptureDialogOptions | React.SyntheticEvent) => {
const result = await openCameraCapture({ allowMultiCapture: true, allowLiveFeed: !!handleLiveStream });
// guard: onClick handlers pass the event as first arg
const options = optionsOrEvent && 'nativeEvent' in optionsOrEvent ? undefined : optionsOrEvent;
const result = await openCameraCapture({ allowMultiCapture: true, allowLiveFeed: !!handleLiveStream, ...options });
if (!result) return; // user dismissed the dialog without capturing anything
// append all captured images
@@ -1,7 +1,7 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Button, ButtonGroup, IconButton, Modal, ModalClose, ModalSlotsAndSlotProps, Option, Select, SelectSlotsAndSlotProps, Sheet, Tooltip, Typography, } from '@mui/joy';
import { Box, Button, ButtonGroup, IconButton, Modal, ModalClose, ModalSlotsAndSlotProps, Option, Select, SelectSlotsAndSlotProps, Sheet, Tooltip, Typography } from '@mui/joy';
import AddRoundedIcon from '@mui/icons-material/AddRounded';
import CameraEnhanceIcon from '@mui/icons-material/CameraEnhance';
import CameraFrontIcon from '@mui/icons-material/CameraFront';
@@ -40,7 +40,7 @@ const _modalSlotProps: ModalSlotsAndSlotProps['slotProps'] = {
const _selectSlotProps: SelectSlotsAndSlotProps<false>['slotProps'] = {
listbox: {
size: 'md'
size: 'md',
},
} as const;
@@ -124,7 +124,14 @@ const _styles = {
},
},
recButton: {
recButtonLarge: {
pl: 3.25,
pr: 4.5,
py: 1.5,
minWidth: { md: 200 },
},
recButtonRight: {
pl: 2,
pr: 2.5,
},
@@ -135,6 +142,7 @@ const _styles = {
export function CameraCaptureModal(props: {
allowMultiCapture?: boolean;
allowLiveFeed?: boolean;
liveFeedOnly?: boolean;
// allowOcr?: boolean;
onDone: (result: CameraCaptureResult | null) => void;
}) {
@@ -160,7 +168,7 @@ export function CameraCaptureModal(props: {
// derived state
const { allowMultiCapture, allowLiveFeed, onDone } = props;
const { allowMultiCapture, allowLiveFeed, liveFeedOnly, onDone } = props;
// single exit point: gather results and close (stream cleanup happens via effect on unmount)
@@ -306,6 +314,16 @@ export function CameraCaptureModal(props: {
const cameraButtons = React.useMemo(() => {
const btns: React.ReactNode[] = [];
// Live-feed-only mode: single prominent red Record button
if (liveFeedOnly) {
btns.push(
<Button key='rec' size='lg' color='danger' disabled={cameraIdx === -1} onClick={handleStartLiveFeedClicked} endDecorator={<FiberManualRecordIcon />} sx={_styles.recButtonLarge}>
Record
</Button>,
);
return btns;
}
// After first capture: [wide +] [Done (N)] - no confusing Capture
if (capturedCount > 0) {
btns.push(
@@ -343,14 +361,14 @@ export function CameraCaptureModal(props: {
color='danger'
disabled={cameraIdx === -1}
onClick={handleStartLiveFeedClicked}
sx={_styles.recButton}
sx={_styles.recButtonRight}
>
<FiberManualRecordIcon />
</IconButton>
</Tooltip>,
);
return btns;
}, [allowLiveFeed, allowMultiCapture, cameraIdx, capturedCount, handleCloseModal, handleStartLiveFeedClicked, handleVideoAddClicked, handleVideoSnapClicked, isAddButtonDisabled]);
}, [allowLiveFeed, allowMultiCapture, liveFeedOnly, cameraIdx, capturedCount, handleCloseModal, handleStartLiveFeedClicked, handleVideoAddClicked, handleVideoSnapClicked, isAddButtonDisabled]);
return (
@@ -6,6 +6,16 @@ import type { CameraCaptureResult } from './useCameraCapture';
import { CameraCaptureModal } from './CameraCaptureModal';
export type CameraCaptureDialogOptions = {
/** show [+] button to queue multiple captures */
allowMultiCapture?: boolean;
/** show small Record button alongside Capture */
allowLiveFeed?: boolean;
/** only show a prominent Record button (no Capture/Add) */
liveFeedOnly?: boolean;
};
/**
* Returns a function to open the camera overlay dialog.
* Resolves with null if dismissed empty, or CameraCaptureResult (images + optional live stream).
@@ -15,11 +25,12 @@ export function useCameraCaptureDialog() {
// external state
const { showPromisedOverlay } = useOverlayComponents();
const openCameraCapture = React.useCallback((options?: { allowMultiCapture?: boolean; allowLiveFeed?: boolean }): Promise<CameraCaptureResult | null> =>
const openCameraCapture = React.useCallback((options?: CameraCaptureDialogOptions): Promise<CameraCaptureResult | null> =>
showPromisedOverlay<CameraCaptureResult | null>('camera-capture', { rejectWithValue: null }, ({ onResolve }) => (
<CameraCaptureModal
allowMultiCapture={options?.allowMultiCapture}
allowLiveFeed={options?.allowLiveFeed}
liveFeedOnly={options?.liveFeedOnly}
onDone={onResolve}
/>
)), [showPromisedOverlay]);