From f800bb8dae99148b7107deffdc87cf300ccad5c2 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Tue, 10 Mar 2026 17:04:58 -0700 Subject: [PATCH] CameraCaptureModal: open with options --- .../useAttachmentSourceHandlers.ts | 12 +++++--- .../components/camera/CameraCaptureModal.tsx | 30 +++++++++++++++---- .../camera/useCameraCaptureDialog.tsx | 13 +++++++- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/common/attachment-drafts/attachment-sources/useAttachmentSourceHandlers.ts b/src/common/attachment-drafts/attachment-sources/useAttachmentSourceHandlers.ts index 67dd38122..7902fa5a7 100644 --- a/src/common/attachment-drafts/attachment-sources/useAttachmentSourceHandlers.ts +++ b/src/common/attachment-drafts/attachment-sources/useAttachmentSourceHandlers.ts @@ -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; +type _HandleCameraOpen = (options?: CameraCaptureDialogOptions) => Promise; 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 diff --git a/src/common/components/camera/CameraCaptureModal.tsx b/src/common/components/camera/CameraCaptureModal.tsx index ab4f97609..7cfbc9610 100644 --- a/src/common/components/camera/CameraCaptureModal.tsx +++ b/src/common/components/camera/CameraCaptureModal.tsx @@ -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['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( + , + ); + 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} > , ); return btns; - }, [allowLiveFeed, allowMultiCapture, cameraIdx, capturedCount, handleCloseModal, handleStartLiveFeedClicked, handleVideoAddClicked, handleVideoSnapClicked, isAddButtonDisabled]); + }, [allowLiveFeed, allowMultiCapture, liveFeedOnly, cameraIdx, capturedCount, handleCloseModal, handleStartLiveFeedClicked, handleVideoAddClicked, handleVideoSnapClicked, isAddButtonDisabled]); return ( diff --git a/src/common/components/camera/useCameraCaptureDialog.tsx b/src/common/components/camera/useCameraCaptureDialog.tsx index c555e32dd..0882f9233 100644 --- a/src/common/components/camera/useCameraCaptureDialog.tsx +++ b/src/common/components/camera/useCameraCaptureDialog.tsx @@ -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 => + const openCameraCapture = React.useCallback((options?: CameraCaptureDialogOptions): Promise => showPromisedOverlay('camera-capture', { rejectWithValue: null }, ({ onResolve }) => ( )), [showPromisedOverlay]);