Skip to content

Commit bc167f6

Browse files
XS⚠️ ◾ Added confirm recording button to prevent auto recording after screen select (#697)
added confirm recording buttons to prevent auto recording
1 parent 91044a1 commit bc167f6

File tree

1 file changed

+68
-34
lines changed

1 file changed

+68
-34
lines changed

src/ui/src/components/recording/SourcePickerDialog.tsx

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SCREEN_RECORDING_ERRORS } from "@shared/constants/error-messages";
22
import { Camera } from "lucide-react";
33
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import { toast } from "sonner";
5+
import { cn } from "@/lib/utils";
56
import { formatErrorMessage } from "@/utils";
67
import { CAMERA_ONLY_SOURCE_ID } from "../../constants/recording";
78
import { 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
321348
function 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

Comments
 (0)