import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Camera, X } from 'lucide-react' import { captureVideoFrame, preferNativeCameraPicker } from '../utils/captureVideoFrame.js' interface LiveCameraCaptureProps { open: boolean busy?: boolean caption?: string onCaptionChange?: (value: string) => void onClose: () => void onCapture: (blob: Blob) => void } type Phase = 'live' | 'preview' | 'native' export default function LiveCameraCapture({ open, busy = false, caption = '', onCaptionChange, onClose, onCapture }: LiveCameraCaptureProps) { const { t } = useTranslation() const videoRef = useRef(null) const fileInputRef = useRef(null) const streamRef = useRef(null) const previewUrlRef = useRef(null) const [cameraError, setCameraError] = useState(null) const [ready, setReady] = useState(false) const [capturing, setCapturing] = useState(false) const [phase, setPhase] = useState(() => (preferNativeCameraPicker() ? 'native' : 'live')) const [previewUrl, setPreviewUrl] = useState(null) const [previewBlob, setPreviewBlob] = useState(null) const [streamGeneration, setStreamGeneration] = useState(0) const clearPreview = useCallback(() => { if (previewUrlRef.current) { URL.revokeObjectURL(previewUrlRef.current) previewUrlRef.current = null } setPreviewUrl(null) setPreviewBlob(null) }, []) const stopStream = useCallback(() => { for (const track of streamRef.current?.getTracks() ?? []) { track.stop() } streamRef.current = null if (videoRef.current) { videoRef.current.srcObject = null } setReady(false) }, []) const enterPreview = useCallback((blob: Blob) => { stopStream() clearPreview() const url = URL.createObjectURL(blob) previewUrlRef.current = url setPreviewBlob(blob) setPreviewUrl(url) setPhase('preview') }, [stopStream, clearPreview]) const resetToLive = useCallback(() => { clearPreview() setCameraError(null) setCapturing(false) if (preferNativeCameraPicker()) { setPhase('native') } else { setPhase('live') setStreamGeneration((n) => n + 1) } }, [clearPreview]) useEffect(() => { if (!open) { stopStream() clearPreview() setCameraError(null) setCapturing(false) setPhase(preferNativeCameraPicker() ? 'native' : 'live') return } setPhase(preferNativeCameraPicker() ? 'native' : 'live') clearPreview() }, [open, stopStream, clearPreview]) useEffect(() => { if (!open || phase !== 'live') { stopStream() return } let cancelled = false const start = async () => { setCameraError(null) setReady(false) if (!navigator.mediaDevices?.getUserMedia) { setCameraError(t('logs.live_photo_camera_unavailable')) return } try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }) if (cancelled) { for (const track of stream.getTracks()) track.stop() return } streamRef.current = stream const video = videoRef.current if (!video) return const markReady = () => { if (cancelled) return if (video.videoWidth > 0 && video.videoHeight > 0) { setReady(true) } } video.onloadedmetadata = markReady video.srcObject = stream await video.play() markReady() } catch (err) { console.error('Camera access failed:', err) if (!cancelled) { setCameraError(t('logs.live_photo_camera_denied')) } } } void start() return () => { cancelled = true stopStream() } }, [open, phase, streamGeneration, stopStream, t]) const handleCapture = async () => { const video = videoRef.current if (!video || !ready || busy || capturing) return setCapturing(true) setCameraError(null) try { const blob = await captureVideoFrame(video) enterPreview(blob) } catch (err) { console.error('Live camera capture failed:', err) setCameraError(t('logs.live_photo_capture_failed')) } finally { setCapturing(false) } } const handleNativeFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] e.target.value = '' if (!file || busy) return setCameraError(null) try { enterPreview(file) } catch (err) { console.error('Live camera file pick failed:', err) setCameraError(t('logs.live_photo_capture_failed')) } } const handleSave = () => { if (!previewBlob || busy) return onCapture(previewBlob) } const handleRetake = () => { if (busy) return resetToLive() } const openNativePicker = () => { if (busy) return fileInputRef.current?.click() } if (!open) return null const showPreview = phase === 'preview' && previewUrl return (
{ if (e.target === e.currentTarget && !busy) onClose() }} >
e.stopPropagation()}>

{t('logs.live_photo_btn')}

void handleNativeFile(e)} /> {cameraError && (

{cameraError}

)} {showPreview ? (
) : phase === 'native' ? (

{t('logs.live_photo_native_hint')}

) : cameraError && !ready ? null : (
)} {onCaptionChange && (
onCaptionChange(e.target.value)} disabled={busy} />
)}
{showPreview ? ( <> ) : phase === 'native' ? null : ( )}
) }