@@ -2,6 +2,7 @@ import { SCREEN_RECORDING_ERRORS } from "@shared/constants/error-messages";
22import { Camera } from "lucide-react" ;
33import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
44import { toast } from "sonner" ;
5+ import { cn } from "@/lib/utils" ;
56import { formatErrorMessage } from "@/utils" ;
67import { CAMERA_ONLY_SOURCE_ID } from "../../constants/recording" ;
78import { ipcClient } from "../../services/ipc-client" ;
@@ -25,6 +26,7 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
2526 const [ microphoneDevices , setMicrophoneDevices ] = useState < MediaDeviceInfo [ ] > ( [ ] ) ;
2627 const [ selectedCameraId , setSelectedCameraId ] = useState < string | undefined > ( undefined ) ;
2728 const [ selectedMicrophoneId , setSelectedMicrophoneId ] = useState < string | undefined > ( undefined ) ;
29+ const [ selectedSourceId , setSelectedSourceId ] = useState < string | null > ( null ) ;
2830 const cameraPreviewRef = useRef < HTMLVideoElement | null > ( null ) ;
2931 const cameraPreviewStreamRef = useRef < MediaStream | null > ( null ) ;
3032 const stopCameraPreviewStream = useCallback ( ( ) => {
@@ -130,6 +132,7 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
130132 setLoading ( false ) ;
131133 setCameraDevices ( [ ] ) ;
132134 setMicrophoneDevices ( [ ] ) ;
135+ setSelectedSourceId ( null ) ;
133136 setDevicesReady ( false ) ;
134137 stopCameraPreviewStream ( ) ;
135138 } , [ open , fetchSources , fetchDevices , stopCameraPreviewStream ] ) ;
@@ -170,14 +173,38 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
170173
171174 const screens = useMemo ( ( ) => sources . filter ( ( s ) => s . type === "screen" ) , [ sources ] ) ;
172175 // const windows = useMemo(() => sources.filter((s) => s.type === "window"), [sources]);
176+ const canConfirm =
177+ selectedSourceId === CAMERA_ONLY_SOURCE_ID
178+ ? Boolean ( selectedCameraId )
179+ : sources . some ( ( source ) => source . id === selectedSourceId ) ;
180+
181+ const handleConfirm = async ( ) => {
182+ if ( ! selectedSourceId ) return ;
183+ if ( selectedSourceId === CAMERA_ONLY_SOURCE_ID ) {
184+ await window . electronAPI . screenRecording . minimizeMainWindow ( ) ;
185+ onSelect ( CAMERA_ONLY_SOURCE_ID , {
186+ cameraId : selectedCameraId ,
187+ microphoneId : selectedMicrophoneId ,
188+ } ) ;
189+ return ;
190+ }
191+ const selectedSource = sources . find ( ( source ) => source . id === selectedSourceId ) ;
192+ if ( selectedSource && ! selectedSource . isMainWindow ) {
193+ await window . electronAPI . screenRecording . minimizeMainWindow ( ) ;
194+ }
195+ onSelect ( selectedSourceId , {
196+ cameraId : selectedCameraId ,
197+ microphoneId : selectedMicrophoneId ,
198+ } ) ;
199+ } ;
173200
174201 return (
175202 < Dialog open = { open } onOpenChange = { onOpenChange } >
176203 < DialogContent className = "max-w-6xl p-4" >
177204 < DialogHeader >
178205 < DialogTitle > Choose a source to record</ DialogTitle >
179206 < DialogDescription >
180- Select a screen to capture. Hover to preview and click to start recording.
207+ Select a screen to capture, then click Start to begin recording.
181208 </ DialogDescription >
182209 </ DialogHeader >
183210
@@ -275,12 +302,8 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
275302 </ h3 >
276303 < div className = "grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-5" >
277304 < CameraOnlyTile
278- onClick = { ( ) =>
279- onSelect ( CAMERA_ONLY_SOURCE_ID , {
280- cameraId : selectedCameraId ,
281- microphoneId : selectedMicrophoneId ,
282- } )
283- }
305+ isSelected = { selectedSourceId === CAMERA_ONLY_SOURCE_ID }
306+ onClick = { ( ) => setSelectedSourceId ( CAMERA_ONLY_SOURCE_ID ) }
284307 />
285308 </ div >
286309 </ section >
@@ -289,12 +312,8 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
289312 < SourceSection
290313 label = "Screens"
291314 sources = { screens }
292- onSelect = { ( id ) =>
293- onSelect ( id , {
294- cameraId : selectedCameraId ,
295- microphoneId : selectedMicrophoneId ,
296- } )
297- }
315+ selectedSourceId = { selectedSourceId }
316+ onSelect = { setSelectedSourceId }
298317 />
299318
300319 { /**
@@ -313,6 +332,14 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
313332 </ div >
314333 ) }
315334 </ div >
335+ < div className = "flex justify-end gap-2 border-t border-border pt-3" >
336+ < Button type = "button" variant = "secondary" onClick = { ( ) => onOpenChange ( false ) } >
337+ Cancel
338+ </ Button >
339+ < Button type = "button" onClick = { handleConfirm } disabled = { ! canConfirm } >
340+ Start
341+ </ Button >
342+ </ div >
316343 </ DialogContent >
317344 </ Dialog >
318345 ) ;
@@ -321,10 +348,12 @@ export function SourcePickerDialog({ open, onOpenChange, onSelect }: SourcePicke
321348function SourceSection ( {
322349 label,
323350 sources,
351+ selectedSourceId,
324352 onSelect,
325353} : {
326354 label : string ;
327355 sources : ScreenSource [ ] ;
356+ selectedSourceId : string | null ;
328357 onSelect : ( id : string ) => void ;
329358} ) {
330359 if ( sources . length === 0 ) return null ;
@@ -333,31 +362,38 @@ function SourceSection({
333362 < h3 className = "mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400" > { label } </ h3 >
334363 < div className = "grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-5" >
335364 { sources . map ( ( src ) => (
336- < ImageTile key = { src . id } source = { src } onClick = { ( ) => onSelect ( src . id ) } />
365+ < ImageTile
366+ key = { src . id }
367+ source = { src }
368+ isSelected = { selectedSourceId === src . id }
369+ onClick = { ( ) => onSelect ( src . id ) }
370+ />
337371 ) ) }
338372 </ div >
339373 </ section >
340374 ) ;
341375}
342376
343- function ImageTile ( { source, onClick } : { source : ScreenSource ; onClick : ( ) => void } ) {
377+ function ImageTile ( {
378+ source,
379+ isSelected,
380+ onClick,
381+ } : {
382+ source : ScreenSource ;
383+ isSelected : boolean ;
384+ onClick : ( ) => void ;
385+ } ) {
344386 const preview = source . thumbnailDataURL ?? source . appIconDataURL ;
345387
346- const handleClick = async ( ) => {
347- // Only minimize if we're not recording the main app window itself
348- if ( ! source . isMainWindow ) {
349- await window . electronAPI . screenRecording . minimizeMainWindow ( ) ;
350- }
351-
352- onClick ( ) ;
353- } ;
354-
355388 return (
356389 < Button
357390 variant = "ghost"
358- onClick = { handleClick }
391+ onClick = { onClick }
359392 title = { source . name }
360- className = "group relative block aspect-video w-full h-auto overflow-hidden rounded-lg bg-neutral-800 p-0 ring-offset-neutral-900 transition-all hover:ring-2 hover:ring-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 hover:bg-neutral-800"
393+ className = { cn (
394+ "group relative block aspect-video w-full h-auto overflow-hidden rounded-lg bg-neutral-800 p-0 ring-offset-neutral-900 transition-all hover:ring-2 hover:ring-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 hover:bg-neutral-800" ,
395+ isSelected && "ring-2 ring-primary-500" ,
396+ ) }
361397 >
362398 { preview ? (
363399 < img src = { preview } alt = { source . name } className = "h-full w-full object-cover" />
@@ -368,18 +404,16 @@ function ImageTile({ source, onClick }: { source: ScreenSource; onClick: () => v
368404 ) ;
369405}
370406
371- function CameraOnlyTile ( { onClick } : { onClick : ( ) => void } ) {
372- const handleClick = async ( ) => {
373- await window . electronAPI . screenRecording . minimizeMainWindow ( ) ;
374- onClick ( ) ;
375- } ;
376-
407+ function CameraOnlyTile ( { isSelected, onClick } : { isSelected : boolean ; onClick : ( ) => void } ) {
377408 return (
378409 < Button
379410 variant = "ghost"
380- onClick = { handleClick }
411+ onClick = { onClick }
381412 title = "Camera Only - No Screen"
382- className = "group relative block aspect-video w-full h-auto overflow-hidden rounded-lg bg-neutral-800 p-4 ring-offset-neutral-900 transition-all hover:ring-2 hover:ring-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 hover:bg-neutral-800"
413+ className = { cn (
414+ "group relative block aspect-video w-full h-auto overflow-hidden rounded-lg bg-neutral-800 p-4 ring-offset-neutral-900 transition-all hover:ring-2 hover:ring-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 hover:bg-neutral-800" ,
415+ isSelected && "ring-2 ring-primary-500" ,
416+ ) }
383417 >
384418 < div className = "h-full w-full flex flex-col items-center justify-center gap-2 text-neutral-400" >
385419 < Camera className = "h-12 w-12" />
0 commit comments