11import { Button } from "@cap/ui-solid" ;
22import { createEventListener } from "@solid-primitives/event-listener" ;
33import { createElementSize } from "@solid-primitives/resize-observer" ;
4+ import { makePersisted } from "@solid-primitives/storage" ;
45import { useSearchParams } from "@solidjs/router" ;
56import { createMutation , useQuery } from "@tanstack/solid-query" ;
67import { invoke } from "@tauri-apps/api/core" ;
@@ -32,6 +33,19 @@ import {
3233} from "solid-js" ;
3334import { createStore , reconcile } from "solid-js/store" ;
3435import toast from "solid-toast" ;
36+ import {
37+ CAMERA_DEFAULT_SIZE ,
38+ CAMERA_PRESET_LARGE ,
39+ CAMERA_WINDOW_STATE_STORAGE_KEY ,
40+ CameraPreviewToolbar ,
41+ CameraResizeHandles ,
42+ type CameraWindowState ,
43+ cameraBorderRadius ,
44+ cameraToolbarScale ,
45+ clampCameraSize ,
46+ getDefaultCameraWindowState ,
47+ normalizeBackgroundBlurMode ,
48+ } from "~/components/CameraPreviewChrome" ;
3549import {
3650 CROP_ZERO ,
3751 type CropBounds ,
@@ -315,10 +329,8 @@ function Inner() {
315329 Record using only your camera and microphone
316330 </ span >
317331 </ div >
318- < div class = "w-full max-w-[480px] px-6 mb-4" >
319- < div class = "w-full aspect-video rounded-2xl border border-gray-6 bg-black overflow-hidden" >
320- < CameraPreviewInline />
321- </ div >
332+ < div class = "flex justify-center w-full px-6 mb-4" >
333+ < CameraPreviewInline />
322334 </ div >
323335 < RecordingControls
324336 target = { { variant : "cameraOnly" } as ScreenCaptureTarget }
@@ -1169,10 +1181,18 @@ const WS_STALL_TIMEOUT_MS = 2000;
11691181
11701182function CameraPreviewInline ( ) {
11711183 const { rawOptions } = useRecordingOptions ( ) ;
1184+ const [ state , setState ] = makePersisted (
1185+ createStore < CameraWindowState > ( getDefaultCameraWindowState ( ) ) ,
1186+ { name : CAMERA_WINDOW_STATE_STORAGE_KEY } ,
1187+ ) ;
11721188 const [ frame , setFrame ] = createSignal < ImageData | null > ( null ) ;
11731189 const [ connectionFailed , setConnectionFailed ] = createSignal ( false ) ;
1190+ const [ chromeVisible , setChromeVisible ] = createSignal ( false ) ;
1191+ const [ viewportSize , setViewportSize ] = createSignal ( {
1192+ width : window . innerWidth ,
1193+ height : window . innerHeight ,
1194+ } ) ;
11741195 let canvasRef : HTMLCanvasElement | undefined ;
1175- let containerRef : HTMLDivElement | undefined ;
11761196 let ws : WebSocket | undefined ;
11771197 let retryCount = 0 ;
11781198 let reconnectTimeoutId : ReturnType < typeof setTimeout > | undefined ;
@@ -1186,6 +1206,36 @@ function CameraPreviewInline() {
11861206 const cameraWsPort = window . __CAP__ ?. cameraWsPort ;
11871207 const hasCameraSelected = ( ) => rawOptions . cameraID !== null ;
11881208
1209+ createEventListener ( window , "resize" , ( ) => {
1210+ setViewportSize ( {
1211+ width : window . innerWidth ,
1212+ height : window . innerHeight ,
1213+ } ) ;
1214+ } ) ;
1215+
1216+ createEffect ( ( ) => {
1217+ let currentSize = state . size as number | string ;
1218+ if ( typeof currentSize !== "number" || Number . isNaN ( currentSize ) ) {
1219+ currentSize =
1220+ currentSize === "lg" ? CAMERA_PRESET_LARGE : CAMERA_DEFAULT_SIZE ;
1221+ setState ( "size" , currentSize ) ;
1222+ return ;
1223+ }
1224+
1225+ const clampedSize = clampCameraSize ( currentSize ) ;
1226+ if ( clampedSize !== currentSize ) {
1227+ setState ( "size" , clampedSize ) ;
1228+ return ;
1229+ }
1230+
1231+ commands . setCameraPreviewState ( {
1232+ size : state . size ,
1233+ shape : state . shape ,
1234+ mirrored : state . mirrored ,
1235+ background_blur : normalizeBackgroundBlurMode ( state . backgroundBlur ) ,
1236+ } ) ;
1237+ } ) ;
1238+
11891239 const getReusableFrame = ( width : number , height : number ) => {
11901240 if (
11911241 ! reusableFrame ||
@@ -1434,44 +1484,39 @@ function CameraPreviewInline() {
14341484 ws ?. close ( ) ;
14351485 } ) ;
14361486
1437- const [ containerSize , setContainerSize ] = createSignal < {
1438- width : number ;
1439- height : number ;
1440- } | null > ( null ) ;
1441-
1442- onMount ( ( ) => {
1443- if ( ! containerRef ) return ;
1444- const observer = new ResizeObserver ( ( ) => {
1445- if ( ! containerRef ) return ;
1446- const rect = containerRef . getBoundingClientRect ( ) ;
1447- setContainerSize ( { width : rect . width , height : rect . height } ) ;
1448- } ) ;
1449- observer . observe ( containerRef ) ;
1450- onCleanup ( ( ) => observer . disconnect ( ) ) ;
1451- } ) ;
1452-
1453- const canvasStyle = ( ) => {
1487+ const previewDimensions = ( ) => {
14541488 const f = frame ( ) ;
1455- const cs = containerSize ( ) ;
1456- if ( ! f || ! cs || cs . width === 0 || cs . height === 0 ) return { } ;
1489+ const aspect = f ? f . width / f . height : 16 / 9 ;
1490+ const size = clampCameraSize ( state . size ) ;
1491+ const width = state . shape === "full" && aspect >= 1 ? size * aspect : size ;
1492+ const height =
1493+ state . shape === "full" ? ( aspect >= 1 ? size : size / aspect ) : size ;
1494+ const viewport = viewportSize ( ) ;
1495+ const maxWidth = Math . max ( 160 , viewport . width - 48 ) ;
1496+ const maxHeight = Math . max ( 160 , viewport . height - 320 ) ;
1497+ const scale = Math . min ( 1 , maxWidth / width , maxHeight / height ) ;
14571498
1458- const frameAspect = f . width / f . height ;
1459- const containerAspect = cs . width / cs . height ;
1460-
1461- let displayWidth : number ;
1462- let displayHeight : number ;
1499+ return {
1500+ height : Math . round ( height * scale ) ,
1501+ width : Math . round ( width * scale ) ,
1502+ } ;
1503+ } ;
14631504
1464- if ( frameAspect > containerAspect ) {
1465- displayWidth = cs . width ;
1466- displayHeight = cs . width / frameAspect ;
1467- } else {
1468- displayHeight = cs . height ;
1469- displayWidth = cs . height * frameAspect ;
1470- }
1505+ const previewFrameStyle = ( ) => {
1506+ const dimensions = previewDimensions ( ) ;
1507+ return {
1508+ "border-radius" : cameraBorderRadius ( state ) ,
1509+ height : `${ dimensions . height } px` ,
1510+ width : `${ dimensions . width } px` ,
1511+ } ;
1512+ } ;
14711513
1514+ const canvasStyle = ( ) => {
14721515 return {
1473- width : `${ Math . round ( displayWidth ) } px` ,
1474- height : `${ Math . round ( displayHeight ) } px` ,
1516+ height : "100%" ,
1517+ "object-fit" : "cover" as const ,
1518+ transform : state . mirrored ? "scaleX(-1)" : "scaleX(1)" ,
1519+ width : "100%" ,
14751520 } ;
14761521 } ;
14771522
@@ -1496,41 +1541,68 @@ function CameraPreviewInline() {
14961541
14971542 return (
14981543 < div
1499- ref = { containerRef }
1500- class = "flex items-center justify-center w-full h-full bg-black"
1544+ class = "flex flex-col items-center max-w-full"
1545+ onPointerMove = { ( ) => setChromeVisible ( true ) }
1546+ onPointerLeave = { ( ) => setChromeVisible ( false ) }
1547+ onPointerCancel = { ( ) => setChromeVisible ( false ) }
15011548 >
1502- < Show
1503- when = { hasCameraSelected ( ) }
1504- fallback = {
1505- < div class = "flex flex-col items-center gap-2 text-center px-4" >
1506- < IconCapCamera class = "size-8 text-gray-9 mb-2" />
1507- < div class = "text-sm text-gray-11" > Please select a camera</ div >
1508- </ div >
1509- }
1510- >
1511- < Show
1512- when = { ! connectionFailed ( ) }
1513- fallback = {
1514- < div class = "flex flex-col items-center gap-2 text-center px-4" >
1515- < div class = "text-sm text-red-400" > Camera connection failed</ div >
1516- < button
1517- type = "button"
1518- onClick = { handleRetryConnection }
1519- class = "text-xs text-blue-400 hover:text-blue-300 underline"
1520- >
1521- Try again
1522- </ button >
1523- </ div >
1524- }
1549+ < div class = "h-14 flex items-center justify-center" >
1550+ < CameraPreviewToolbar
1551+ state = { state }
1552+ setState = { setState }
1553+ visible = { chromeVisible ( ) }
1554+ scale = { cameraToolbarScale ( state . size ) }
1555+ />
1556+ </ div >
1557+ < div class = "relative shadow-lg" style = { previewFrameStyle ( ) } >
1558+ < div
1559+ class = "flex items-center justify-center w-full h-full overflow-hidden border border-gray-6 bg-black text-gray-11"
1560+ style = { { "border-radius" : "inherit" } }
15251561 >
15261562 < Show
1527- when = { frame ( ) }
1528- fallback = { < div class = "text-sm text-gray-11" > Loading camera...</ div > }
1563+ when = { hasCameraSelected ( ) }
1564+ fallback = {
1565+ < div class = "flex flex-col items-center gap-2 text-center px-4" >
1566+ < IconCapCamera class = "size-8 text-gray-9 mb-2" />
1567+ < div class = "text-sm text-gray-11" > Please select a camera</ div >
1568+ </ div >
1569+ }
15291570 >
1530- < canvas ref = { canvasRef } style = { canvasStyle ( ) } />
1571+ < Show
1572+ when = { ! connectionFailed ( ) }
1573+ fallback = {
1574+ < div class = "flex flex-col items-center gap-2 text-center px-4" >
1575+ < div class = "text-sm text-red-400" >
1576+ Camera connection failed
1577+ </ div >
1578+ < button
1579+ type = "button"
1580+ onClick = { handleRetryConnection }
1581+ class = "text-xs text-blue-400 hover:text-blue-300 underline"
1582+ >
1583+ Try again
1584+ </ button >
1585+ </ div >
1586+ }
1587+ >
1588+ < Show
1589+ when = { frame ( ) }
1590+ fallback = {
1591+ < div class = "text-sm text-gray-11" > Loading camera...</ div >
1592+ }
1593+ >
1594+ < canvas ref = { canvasRef } style = { canvasStyle ( ) } />
1595+ </ Show >
1596+ </ Show >
15311597 </ Show >
1532- </ Show >
1533- </ Show >
1598+ </ div >
1599+ < CameraResizeHandles
1600+ state = { state }
1601+ setState = { setState }
1602+ toolbarHeight = { 0 }
1603+ visible = { chromeVisible ( ) }
1604+ />
1605+ </ div >
15341606 </ div >
15351607 ) ;
15361608}
0 commit comments