Skip to content

Commit 3eeecc4

Browse files
Merge pull request #241 from marcusschiesser/codex/add-multiple-layout-presets-for-video
Add selectable webcam layout presets (Picture in Picture, Vertical Stack)
2 parents 4563641 + 6236d2a commit 3eeecc4

22 files changed

Lines changed: 636 additions & 205 deletions

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22.22.1

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
"private": true,
44
"version": "1.2.0",
55
"type": "module",
6+
"packageManager": "npm@10.9.4",
7+
"engines": {
8+
"node": "22.22.1",
9+
"npm": "10.9.4"
10+
},
611
"scripts": {
712
"dev": "vite",
813
"build": "tsc && vite build && electron-builder",

src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from "react";
22
import { LaunchWindow } from "./components/launch/LaunchWindow";
33
import { SourceSelector } from "./components/launch/SourceSelector";
4+
import { Toaster } from "./components/ui/sonner";
45
import { TooltipProvider } from "./components/ui/tooltip";
56
import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog";
67
import VideoEditor from "./components/video-editor/VideoEditor";
@@ -48,5 +49,10 @@ export default function App() {
4849
}
4950
})();
5051

51-
return <TooltipProvider>{content}</TooltipProvider>;
52+
return (
53+
<TooltipProvider>
54+
{content}
55+
<Toaster theme="dark" className="pointer-events-auto" />
56+
</TooltipProvider>
57+
);
5258
}

src/components/launch/LaunchWindow.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RxDragHandleDots2 } from "react-icons/rx";
1919
import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter";
2020
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
2121
import { useScreenRecorder } from "../../hooks/useScreenRecorder";
22+
import { requestCameraAccess } from "../../lib/requestCameraAccess";
2223
import { formatTimePadded } from "../../utils/timeUtils";
2324
import { AudioLevelMeter } from "../ui/audio-level-meter";
2425
import { Tooltip } from "../ui/tooltip";
@@ -110,6 +111,16 @@ export function LaunchWindow() {
110111
};
111112
}, [recording, recordingStart]);
112113

114+
useEffect(() => {
115+
if (!import.meta.env.DEV) {
116+
return;
117+
}
118+
119+
void requestCameraAccess().catch((error) => {
120+
console.warn("Failed to trigger camera access request during development:", error);
121+
});
122+
}, []);
123+
113124
const [selectedSource, setSelectedSource] = useState("Screen");
114125
const [hasSelectedSource, setHasSelectedSource] = useState(false);
115126

@@ -251,8 +262,8 @@ export function LaunchWindow() {
251262
</button>
252263
<button
253264
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
254-
onClick={() => {
255-
void setWebcamEnabled(!webcamEnabled);
265+
onClick={async () => {
266+
await setWebcamEnabled(!webcamEnabled);
256267
}}
257268
title={webcamEnabled ? "Disable webcam" : "Enable webcam"}
258269
>

src/components/video-editor/SettingsPanel.tsx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,18 @@ import {
2525
AccordionTrigger,
2626
} from "@/components/ui/accordion";
2727
import { Button } from "@/components/ui/button";
28+
import {
29+
Select,
30+
SelectContent,
31+
SelectItem,
32+
SelectTrigger,
33+
SelectValue,
34+
} from "@/components/ui/select";
2835
import { Slider } from "@/components/ui/slider";
2936
import { Switch } from "@/components/ui/switch";
3037
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3138
import { getAssetPath } from "@/lib/assetPath";
39+
import { WEBCAM_LAYOUT_PRESETS } from "@/lib/compositeLayout";
3240
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
3341
import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
3442
import { cn } from "@/lib/utils";
@@ -43,6 +51,7 @@ import type {
4351
CropRegion,
4452
FigureData,
4553
PlaybackSpeed,
54+
WebcamLayoutPreset,
4655
ZoomDepth,
4756
} from "./types";
4857
import { SPEED_OPTIONS } from "./types";
@@ -132,6 +141,9 @@ interface SettingsPanelProps {
132141
selectedSpeedValue?: PlaybackSpeed | null;
133142
onSpeedChange?: (speed: PlaybackSpeed) => void;
134143
onSpeedDelete?: (id: string) => void;
144+
hasWebcam?: boolean;
145+
webcamLayoutPreset?: WebcamLayoutPreset;
146+
onWebcamLayoutPresetChange?: (preset: WebcamLayoutPreset) => void;
135147
}
136148

137149
export default SettingsPanel;
@@ -197,6 +209,9 @@ export function SettingsPanel({
197209
selectedSpeedValue,
198210
onSpeedChange,
199211
onSpeedDelete,
212+
hasWebcam = false,
213+
webcamLayoutPreset = "picture-in-picture",
214+
onWebcamLayoutPresetChange,
200215
}: SettingsPanelProps) {
201216
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
202217
const [customImages, setCustomImages] = useState<string[]>([]);
@@ -567,7 +582,47 @@ export function SettingsPanel({
567582
)}
568583
</div>
569584

570-
<Accordion type="multiple" defaultValue={["effects", "background"]} className="space-y-1">
585+
<Accordion
586+
type="multiple"
587+
defaultValue={hasWebcam ? ["layout", "effects", "background"] : ["effects", "background"]}
588+
className="space-y-1"
589+
>
590+
{hasWebcam && (
591+
<AccordionItem
592+
value="layout"
593+
className="border-white/5 rounded-xl bg-white/[0.02] px-3"
594+
>
595+
<AccordionTrigger className="py-2.5 hover:no-underline">
596+
<div className="flex items-center gap-2">
597+
<Sparkles className="w-4 h-4 text-[#34B27B]" />
598+
<span className="text-xs font-medium">Layout</span>
599+
</div>
600+
</AccordionTrigger>
601+
<AccordionContent className="pb-3">
602+
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
603+
<div className="text-[10px] font-medium text-slate-300 mb-1.5">Preset</div>
604+
<Select
605+
value={webcamLayoutPreset}
606+
onValueChange={(value: WebcamLayoutPreset) =>
607+
onWebcamLayoutPresetChange?.(value)
608+
}
609+
>
610+
<SelectTrigger className="h-8 bg-black/20 border-white/10 text-xs">
611+
<SelectValue placeholder="Select preset" />
612+
</SelectTrigger>
613+
<SelectContent>
614+
{WEBCAM_LAYOUT_PRESETS.map((preset) => (
615+
<SelectItem key={preset.value} value={preset.value} className="text-xs">
616+
{preset.label}
617+
</SelectItem>
618+
))}
619+
</SelectContent>
620+
</Select>
621+
</div>
622+
</AccordionContent>
623+
</AccordionItem>
624+
)}
625+
571626
<AccordionItem value="effects" className="border-white/5 rounded-xl bg-white/[0.02] px-3">
572627
<AccordionTrigger className="py-2.5 hover:no-underline">
573628
<div className="flex items-center gap-2">

src/components/video-editor/VideoEditor.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Span } from "dnd-timeline";
22
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
44
import { toast } from "sonner";
5-
import { Toaster } from "@/components/ui/sonner";
65
import { useShortcuts } from "@/contexts/ShortcutsContext";
76
import { INITIAL_EDITOR_STATE, useEditorHistory } from "@/hooks/useEditorHistory";
87
import {
@@ -76,6 +75,7 @@ export default function VideoEditor() {
7675
borderRadius,
7776
padding,
7877
aspectRatio,
78+
webcamLayoutPreset,
7979
} = editorState;
8080

8181
// ── Non-undoable state
@@ -173,6 +173,7 @@ export default function VideoEditor() {
173173
speedRegions: normalizedEditor.speedRegions,
174174
annotationRegions: normalizedEditor.annotationRegions,
175175
aspectRatio: normalizedEditor.aspectRatio,
176+
webcamLayoutPreset: normalizedEditor.webcamLayoutPreset,
176177
});
177178
setExportQuality(normalizedEditor.exportQuality);
178179
setExportFormat(normalizedEditor.exportFormat);
@@ -240,6 +241,7 @@ export default function VideoEditor() {
240241
speedRegions,
241242
annotationRegions,
242243
aspectRatio,
244+
webcamLayoutPreset,
243245
exportQuality,
244246
exportFormat,
245247
gifFrameRate,
@@ -261,6 +263,7 @@ export default function VideoEditor() {
261263
speedRegions,
262264
annotationRegions,
263265
aspectRatio,
266+
webcamLayoutPreset,
264267
exportQuality,
265268
exportFormat,
266269
gifFrameRate,
@@ -352,6 +355,7 @@ export default function VideoEditor() {
352355
speedRegions,
353356
annotationRegions,
354357
aspectRatio,
358+
webcamLayoutPreset,
355359
exportQuality,
356360
exportFormat,
357361
gifFrameRate,
@@ -404,6 +408,7 @@ export default function VideoEditor() {
404408
speedRegions,
405409
annotationRegions,
406410
aspectRatio,
411+
webcamLayoutPreset,
407412
exportQuality,
408413
exportFormat,
409414
gifFrameRate,
@@ -1021,6 +1026,7 @@ export default function VideoEditor() {
10211026
videoPadding: padding,
10221027
cropRegion,
10231028
annotationRegions,
1029+
webcamLayoutPreset,
10241030
previewWidth,
10251031
previewHeight,
10261032
onProgress: (progress: ExportProgress) => {
@@ -1148,6 +1154,7 @@ export default function VideoEditor() {
11481154
padding,
11491155
cropRegion,
11501156
annotationRegions,
1157+
webcamLayoutPreset,
11511158
previewWidth,
11521159
previewHeight,
11531160
onProgress: (progress: ExportProgress) => {
@@ -1212,6 +1219,7 @@ export default function VideoEditor() {
12121219
annotationRegions,
12131220
isPlaying,
12141221
aspectRatio,
1222+
webcamLayoutPreset,
12151223
exportQuality,
12161224
handleExportSaved,
12171225
],
@@ -1351,6 +1359,7 @@ export default function VideoEditor() {
13511359
ref={videoPlaybackRef}
13521360
videoPath={videoPath || ""}
13531361
webcamVideoPath={webcamVideoPath || undefined}
1362+
webcamLayoutPreset={webcamLayoutPreset}
13541363
onDurationChange={setDuration}
13551364
onTimeUpdate={setCurrentTime}
13561365
currentTime={currentTime}
@@ -1474,6 +1483,9 @@ export default function VideoEditor() {
14741483
cropRegion={cropRegion}
14751484
onCropChange={(r) => pushState({ cropRegion: r })}
14761485
aspectRatio={aspectRatio}
1486+
hasWebcam={Boolean(webcamVideoPath)}
1487+
webcamLayoutPreset={webcamLayoutPreset}
1488+
onWebcamLayoutPresetChange={(preset) => pushState({ webcamLayoutPreset: preset })}
14771489
videoElement={videoPlaybackRef.current?.video || null}
14781490
exportQuality={exportQuality}
14791491
onExportQualityChange={setExportQuality}
@@ -1521,8 +1533,6 @@ export default function VideoEditor() {
15211533
</PanelGroup>
15221534
</div>
15231535

1524-
<Toaster theme="dark" className="pointer-events-auto" />
1525-
15261536
<ExportDialog
15271537
isOpen={showExportDialog}
15281538
onClose={() => setShowExportDialog(false)}

0 commit comments

Comments
 (0)