import { useEffect, useRef, useState } from 'react' import { createBarcodeDecoder, openCameraStream, stopCameraStream, vibrateOnScan, } from '@/utils/barcode' import { isCameraAvailable, isCameraBlockedByStandaloneIos } from '@/utils/device' export type BarcodeReaderStatus = | 'starting' | 'scanning' | 'denied' | 'insecure' | 'standaloneIos' | 'error' /** Kare çözme aralığı. Her karede çözmek mobilde pili hızla tüketir. */ const DECODE_INTERVAL_MS = 180 /** Aynı kodun arka arkaya birden çok kez okunmasını engelleyen pencere. */ const DUPLICATE_WINDOW_MS = 1500 /** * Kamerayı açar, kareleri çözer ve okunan kodu bildirir. * * `onDetect` her okumada yeniden bağlanabildiği için ref üzerinden tutulur; * aksi hâlde her render kamerayı kapatıp yeniden açardı. */ export const useBarcodeReader = ({ active, onDetect, }: { active: boolean onDetect: (code: string) => void }) => { const videoRef = useRef(null) const onDetectRef = useRef(onDetect) const [status, setStatus] = useState('starting') onDetectRef.current = onDetect useEffect(() => { if (!active) return if (isCameraBlockedByStandaloneIos()) { setStatus('standaloneIos') return } if (!isCameraAvailable()) { setStatus('insecure') return } const video = videoRef.current let stream: MediaStream | undefined let timer: number | undefined let cancelled = false let lastCode = '' let lastCodeAt = 0 const start = async () => { setStatus('starting') try { const [cameraStream, decode] = await Promise.all([ openCameraStream(), createBarcodeDecoder(), ]) if (cancelled) { stopCameraStream(cameraStream) return } if (!video) return stream = cameraStream video.srcObject = cameraStream await video.play() setStatus('scanning') const tick = async () => { if (cancelled) return try { const code = await decode(video) const now = Date.now() if (code && (code !== lastCode || now - lastCodeAt > DUPLICATE_WINDOW_MS)) { lastCode = code lastCodeAt = now vibrateOnScan() onDetectRef.current(code) } } catch { // Tek bir karenin çözülememesi tarama döngüsünü durdurmaz. } if (!cancelled) { timer = window.setTimeout(tick, DECODE_INTERVAL_MS) } } tick() } catch (error) { if (cancelled) return const name = (error as DOMException)?.name setStatus(name === 'NotAllowedError' || name === 'SecurityError' ? 'denied' : 'error') } } start() return () => { cancelled = true if (timer) window.clearTimeout(timer) stopCameraStream(stream) if (video) video.srcObject = null } }, [active]) return { videoRef, status } }