// Simple QR Scanner Component using only Html5Qrcode const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => { const videoRef = React.useRef(null); const qrScannerRef = React.useRef(null); const [error, setError] = React.useState(null); const [isScanning, setIsScanning] = React.useState(false); const [progress, setProgress] = React.useState({ id: null, seq: 0, total: 0 }); const [showFocusHint, setShowFocusHint] = React.useState(false); const [manualMode, setManualMode] = React.useState(false); const [scannedParts, setScannedParts] = React.useState(new Set()); const [currentQRId, setCurrentQRId] = React.useState(null); React.useEffect(() => { if (isVisible) { startScanner(); } else { stopScanner(); } return () => { stopScanner(); }; }, [isVisible]); React.useEffect(() => { const onProgress = (e) => { const { id, seq, total } = e.detail || {}; if (!id || !total) return; setProgress({ id, seq, total }); // Обновляем ID текущего QR кода if (id !== currentQRId) { setCurrentQRId(id); setScannedParts(new Set()); // Сбрасываем сканированные части для нового ID } // Добавляем отсканированную часть setScannedParts(prev => new Set([...prev, seq])); }; const onComplete = () => { // Close scanner once app signals completion if (!continuous) return; try { stopScanner(); } catch {} }; document.addEventListener('qr-scan-progress', onProgress, { passive: true }); document.addEventListener('qr-scan-complete', onComplete, { passive: true }); return () => { document.removeEventListener('qr-scan-progress', onProgress, { passive: true }); document.removeEventListener('qr-scan-complete', onComplete, { passive: true }); }; }, [currentQRId]); // Функция для tap-to-focus const handleTapToFocus = (event, html5Qrcode) => { try { // Показываем подсказку о фокусировке setShowFocusHint(true); setTimeout(() => setShowFocusHint(false), 2000); // Получаем координаты клика относительно видео элемента const rect = event.target.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // Нормализуем координаты (0-1) const normalizedX = x / rect.width; const normalizedY = y / rect.height; console.log('Tap to focus at:', { x, y, normalizedX, normalizedY }); // Попытка программной фокусировки (если поддерживается браузером) if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { // Это может не работать во всех браузерах, но попробуем console.log('Attempting programmatic focus...'); } } catch (error) { console.warn('Tap to focus error:', error); } }; // Функции для ручного управления const toggleManualMode = () => { setManualMode(!manualMode); if (!manualMode) { // При включении ручного режима останавливаем автопрокрутку console.log('Manual mode enabled - auto-scroll disabled'); } else { // При выключении ручного режима возобновляем автопрокрутку console.log('Manual mode disabled - auto-scroll enabled'); } }; const resetProgress = () => { setScannedParts(new Set()); setCurrentQRId(null); setProgress({ id: null, seq: 0, total: 0 }); }; const startScanner = async () => { try { console.log('Starting QR scanner...'); setError(null); setIsScanning(true); // Allow camera on HTTP as well; rely on browser permission prompts // Check if Html5Qrcode is available if (!window.Html5Qrcode) { setError('QR scanner library not loaded'); setIsScanning(false); return; } // Get available cameras first console.log('Getting available cameras...'); const cameras = await window.Html5Qrcode.getCameras(); console.log('Available cameras:', cameras); if (!cameras || cameras.length === 0) { setError('No cameras found on this device'); setIsScanning(false); return; } // Clear any existing scanner if (qrScannerRef.current) { try { qrScannerRef.current.stop(); } catch (e) { console.log('Stopping previous scanner:', e.message); } } // Create video element if it doesn't exist if (!videoRef.current) { console.log('Video element not found'); setError('Video element not found'); setIsScanning(false); return; } console.log('Video element found:', videoRef.current); console.log('Video element ID:', videoRef.current.id); // Create Html5Qrcode instance console.log('Creating Html5Qrcode instance...'); const html5Qrcode = new window.Html5Qrcode(videoRef.current.id || 'qr-reader'); // Find back camera (environment facing) let cameraId = cameras[0].id; // Default to first camera let selectedCamera = cameras[0]; // Look for back camera for (const camera of cameras) { if (camera.label.toLowerCase().includes('back') || camera.label.toLowerCase().includes('rear') || camera.label.toLowerCase().includes('environment')) { cameraId = camera.id; selectedCamera = camera; break; } } console.log('Available cameras:'); cameras.forEach((cam, index) => { console.log(`${index + 1}. ${cam.label} (${cam.id})`); }); console.log('Selected camera:', selectedCamera.label, 'ID:', cameraId); // Start camera console.log('Starting camera with Html5Qrcode...'); const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024); const qrboxSize = isDesktop ? 560 : 360; await html5Qrcode.start( cameraId, // Use specific camera ID { fps: /iPhone|iPad|iPod/i.test(navigator.userAgent) ? 2 : 3, qrbox: { width: qrboxSize, height: qrboxSize }, // Улучшенные настройки для мобильных устройств aspectRatio: 1.0, videoConstraints: { focusMode: "continuous", // Непрерывная автофокусировка exposureMode: "continuous", // Непрерывная экспозиция whiteBalanceMode: "continuous", // Непрерывный баланс белого torch: false, // Вспышка выключена по умолчанию facingMode: "environment" // Используем заднюю камеру } }, (decodedText, decodedResult) => { console.log('QR Code detected:', decodedText); try { const res = onScan(decodedText); const handleResult = (val) => { const shouldClose = val === true || !continuous; if (shouldClose) { stopScanner(); } }; if (res && typeof res.then === 'function') { res.then(handleResult).catch((e) => { console.warn('onScan async handler error:', e); if (!continuous) stopScanner(); }); } else { handleResult(res); } } catch (e) { console.warn('onScan handler threw:', e); if (!continuous) { stopScanner(); } } }, (error) => { // Ignore decode errors, they're normal during scanning console.log('QR decode error:', error); } ); // Store scanner reference qrScannerRef.current = html5Qrcode; console.log('QR scanner started successfully'); // Добавляем обработчик tap-to-focus для мобильных устройств if (videoRef.current) { videoRef.current.addEventListener('click', (event) => { handleTapToFocus(event, html5Qrcode); }); } } catch (err) { console.error('Error starting QR scanner:', err); let errorMessage = 'Failed to start camera'; if (err.name === 'NotAllowedError') { errorMessage = 'Camera access denied. Please allow camera access and try again.'; } else if (err.name === 'NotFoundError') { errorMessage = 'No camera found on this device.'; } else if (err.name === 'NotSupportedError') { errorMessage = 'Camera not supported on this device.'; } else if (err.name === 'NotReadableError') { errorMessage = 'Camera is already in use by another application.'; } else if (err.message) { errorMessage = err.message; } setError(errorMessage); setIsScanning(false); } }; const stopScanner = () => { if (qrScannerRef.current) { try { qrScannerRef.current.stop().then(() => { console.log('QR scanner stopped'); }).catch((err) => { console.log('Error stopping scanner:', err); }); } catch (err) { console.log('Error stopping scanner:', err); } qrScannerRef.current = null; } setIsScanning(false); try { // iOS Safari workaround: small delay before closing modal to release camera if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { setTimeout(() => { // no-op; allow camera to settle }, 150); } } catch {} }; const handleClose = () => { stopScanner(); onClose(); }; if (!isVisible) { return null; } return React.createElement('div', { className: "fixed inset-0 bg-black/80 flex items-center justify-center z-50" }, [ React.createElement('div', { key: 'scanner-modal', className: "bg-gray-800 rounded-lg p-6 w-full mx-4 max-w-2xl" }, [ React.createElement('div', { key: 'scanner-header', className: "flex items-center justify-between mb-4" }, [ React.createElement('h3', { key: 'title', className: "text-lg font-medium text-white" }, 'Scan QR Code'), React.createElement('button', { key: 'close-btn', onClick: handleClose, className: "text-gray-400 hover:text-white transition-colors" }, [ React.createElement('i', { className: 'fas fa-times text-xl' }) ]) ]), // Индикатор прогресса сканирования progress.total > 1 && React.createElement('div', { key: 'progress-indicator', className: "mb-4 p-3 bg-gray-800/50 border border-gray-600/30 rounded-lg" }, [ React.createElement('div', { key: 'progress-header', className: "flex items-center justify-between mb-2" }, [ React.createElement('span', { key: 'progress-title', className: "text-sm text-gray-300" }, `QR ID: ${currentQRId ? currentQRId.substring(0, 8) + '...' : 'N/A'}`), React.createElement('span', { key: 'progress-count', className: "text-sm text-blue-400" }, `${scannedParts.size}/${progress.total} scanned`) ]), React.createElement('div', { key: 'progress-numbers', className: "flex flex-wrap gap-1" }, Array.from({ length: progress.total }, (_, i) => { const partNumber = i + 1; const isScanned = scannedParts.has(partNumber); return React.createElement('div', { key: `part-${partNumber}`, className: `w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-colors ${ isScanned ? 'bg-green-500 text-white' : 'bg-gray-600 text-gray-300' }` }, partNumber); })) ]), // Панель управления progress.total > 1 && React.createElement('div', { key: 'control-panel', className: "mb-4 flex gap-2" }, [ React.createElement('button', { key: 'manual-toggle', onClick: toggleManualMode, className: `px-3 py-1 rounded text-xs font-medium transition-colors ${ manualMode ? 'bg-blue-500 text-white' : 'bg-gray-600 text-gray-300 hover:bg-gray-500' }` }, manualMode ? 'Manual Mode' : 'Auto Mode'), React.createElement('button', { key: 'reset-progress', onClick: resetProgress, className: "px-3 py-1 bg-red-500/20 text-red-400 border border-red-500/20 rounded text-xs font-medium hover:bg-red-500/30" }, 'Reset'), React.createElement('span', { key: 'mode-hint', className: "text-xs text-gray-400 self-center" }, manualMode ? 'Tap to focus, scan manually' : 'Auto-scrolling enabled') ]), React.createElement('div', { key: 'scanner-content', className: "relative" }, [ React.createElement('div', { key: 'video-container', id: 'qr-reader', ref: videoRef, className: "w-full h-80 md:h-[32rem] bg-gray-700 rounded-lg" }), error && React.createElement('div', { key: 'error', className: "absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg" }, [ React.createElement('div', { key: 'error-content', className: "text-center text-white p-4" }, [ React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle text-2xl mb-2' }), React.createElement('p', { key: 'error-text', className: "text-sm" }, error) ]) ]), !error && !isScanning && React.createElement('div', { key: 'loading', className: "absolute inset-0 flex items-center justify-center bg-gray-700/50 rounded-lg" }, [ React.createElement('div', { key: 'loading-content', className: "text-center text-white" }, [ React.createElement('i', { key: 'loading-icon', className: 'fas fa-spinner fa-spin text-2xl mb-2' }), React.createElement('p', { key: 'loading-text', className: "text-sm" }, 'Starting camera...') ]) ]), !error && isScanning && React.createElement('div', { key: 'scanning-overlay', className: "absolute inset-0 flex items-center justify-center" }, [ React.createElement('div', { key: 'scanning-content', className: "text-center text-white bg-black/50 rounded-lg px-4 py-2" }, [ React.createElement('i', { key: 'scanning-icon', className: 'fas fa-qrcode text-xl mb-1' }), React.createElement('p', { key: 'scanning-text', className: "text-xs" }, progress && progress.total > 1 ? `Frames: ${Math.min(progress.seq, progress.total)}/${progress.total}` : 'Point camera at QR code'), React.createElement('p', { key: 'tap-hint', className: "text-xs text-blue-300 mt-1" }, 'Tap screen to focus') ]) ]), // Подсказка о фокусировке showFocusHint && React.createElement('div', { key: 'focus-hint', className: "absolute top-4 left-1/2 transform -translate-x-1/2 bg-green-500/90 text-white px-3 py-1 rounded-full text-xs font-medium z-10" }, 'Focusing...'), // Bottom overlay kept simple on mobile ]), // Дополнительные подсказки для улучшения сканирования React.createElement('div', { key: 'scanning-tips', className: "mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg" }, [ React.createElement('h4', { key: 'tips-title', className: "text-blue-400 text-sm font-medium mb-2 flex items-center" }, [ React.createElement('i', { key: 'tips-icon', className: 'fas fa-lightbulb mr-2' }), 'Tips for better scanning:' ]), React.createElement('ul', { key: 'tips-list', className: "text-xs text-blue-300 space-y-1" }, [ React.createElement('li', { key: 'tip-1' }, '• Ensure good lighting'), React.createElement('li', { key: 'tip-2' }, '• Hold phone steady'), React.createElement('li', { key: 'tip-3' }, '• Tap screen to focus'), React.createElement('li', { key: 'tip-4' }, '• Keep QR code in frame') ]) ]) ]) ]); }; // Export for use in other files window.QRScanner = QRScanner; console.log('QRScanner component loaded and available on window.QRScanner');