photo_capture_input.tsx (3279B)
1 import { useComputed, useSignal } from "@preact/signals"; 2 import { useEffect, useRef } from "preact/hooks"; 3 import { ComponentChildren, JSX } from "preact"; 4 5 export type PhotoCaptureInputProps = { 6 capture?: unknown; 7 retry?: unknown; 8 send?: unknown; 9 children: ComponentChildren; 10 camera: "user" | "environment"; 11 }; 12 13 export function PhotoCaptureInput(props: PhotoCaptureInputProps) { 14 const image = useSignal<string | undefined>(undefined); 15 const videoRef = useRef<HTMLVideoElement>(null); 16 const canvasRef = useRef<HTMLCanvasElement>(null); 17 const unauthorized = useSignal(true); 18 const capturedShow = useComputed(() => 19 image.value !== undefined ? undefined : "display:none;" 20 ); 21 const capturedHide = useComputed(() => 22 image.value !== undefined ? "display:none;" : undefined 23 ); 24 25 useEffect(() => { 26 if (videoRef.current && canvasRef.current) { 27 navigator.mediaDevices.getUserMedia({ 28 video: { facingMode: props.camera }, 29 audio: false, 30 }) 31 .then((stream) => { 32 unauthorized.value = false; 33 videoRef.current!.srcObject = stream; 34 }) 35 .catch(() => { 36 unauthorized.value = true; 37 }); 38 } 39 }, [videoRef.current, canvasRef.current]); 40 41 const capture = () => { 42 if (!videoRef.current || !canvasRef.current!) return; 43 const rect = videoRef.current.getBoundingClientRect(); 44 canvasRef.current.width = rect.width; 45 canvasRef.current.height = rect.height; 46 const context = canvasRef.current.getContext("2d")!; 47 context.drawImage(videoRef.current, 0, 0, rect.width, rect.height); 48 image.value = String(canvasRef.current.toDataURL("image/png")); 49 }; 50 51 const retry = () => { 52 image.value = undefined; 53 }; 54 55 const submit = (event: JSX.TargetedMouseEvent<HTMLButtonElement>) => { 56 event.currentTarget.form?.submit(); 57 }; 58 59 return ( 60 <fieldset> 61 <section aria-describedby="legend"> 62 <input type="hidden" name="picture" value={image} /> 63 <video 64 autoPlay 65 playsinline 66 ref={videoRef} 67 style={useComputed(() => 68 image.value !== undefined 69 ? "width:100%; display:none;" 70 : "width:100%;" 71 )} 72 /> 73 <canvas 74 ref={canvasRef} 75 style={useComputed(() => 76 image.value === undefined ? "display: none" : "" 77 )} 78 /> 79 <small>{props.children}</small> 80 </section> 81 <div role="group"> 82 <button 83 type="button" 84 className="secondary" 85 style={capturedShow} 86 onClick={debounce(retry)} 87 > 88 {props.retry ?? "Retry"} 89 </button> 90 <button 91 type="button" 92 disabled={unauthorized} 93 style={capturedHide} 94 onClick={debounce(capture)} 95 > 96 {props.capture ?? "Capture"} 97 </button> 98 <button 99 type="submit" 100 style={capturedShow} 101 onClick={submit} 102 > 103 {props.send ?? "Send"} 104 </button> 105 </div> 106 </fieldset> 107 ); 108 } 109 110 function debounce<A extends unknown[]>(fn: (...args: A) => unknown) { 111 let timer: number; 112 return (...args: A) => { 113 clearTimeout(timer); 114 timer = setTimeout(fn, 300, ...args); 115 }; 116 }