@extends('layout') @section('title') <title>Scan QR</title> @endsection @section('content') <section class="container py-4"> <h1 class="h4 mb-2">Scan Checkpoint QR</h1> <p class="text-muted mb-3"> Point your camera at the QR on the sign. Voice guidance will play automatically after we update your location. </p> <div id="reader" style="width:100%; max-width:520px; aspect-ratio: 1 / 1; background:#f8f9fa; border-radius:12px; position: relative; overflow: hidden;"></div> <div id="scan-status" class="small text-muted mt-2"></div> <div class="mt-3 d-flex gap-2"> <a href="{{ url()->previous() ?: route('map.show','floor1') }}" class="btn btn-outline-secondary">Back</a> </div> </section> <script src="{{ asset('vendor/html5-qrcode/html5-qrcode.min.js') }}"></script> <script> (function() { const statusEl = document.getElementById('scan-status'); const readerId = 'reader'; const LS_KEY_CAMERA = 'qr_preferred_camera_id'; function setStatus(msg, isErr=false) { statusEl.textContent = msg || ''; statusEl.classList.toggle('text-danger', !!isErr); } // Secure-origin check (required by getUserMedia) const isSecure = location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; if (!isSecure) { setStatus('Camera requires HTTPS or http://localhost. Open this as http://localhost:8000 or use HTTPS (Valet/ngrok).', true); return; } if (!navigator.mediaDevices?.getUserMedia) { setStatus('This browser does not support camera access (getUserMedia). Try a modern browser.', true); return; } // Redirect helper function go(url) { sessionStorage.setItem('map_voice_unlocked', '1'); localStorage.setItem('map_voice_enabled', '1'); try { const u = new URL(url, window.location.origin); if (!u.searchParams.has('s')) u.searchParams.set('s','1'); window.location.href = u.toString(); } catch { window.location.href = url; } } // Decode handler function onScanSuccess(text) { // Stop the scanner once we got a code (prevents double redirects) try { html5Scanner?.clear(); } catch {} try { html5Instance?.stop().then(()=>{}); } catch {} // Full URL or slug support try { const u = new URL(text); go(u.toString()); return; } catch {} let path = text.trim(); if (/^https?:/i.test(path)) { go(path); return; } const [slugPart, queryPart] = path.split('?'); let url = new URL("{{ route('nav.qr', ['slug'=>'__SLUG__']) }}".replace('__SLUG__','')); url.pathname += encodeURIComponent(slugPart); if (queryPart) { for (const kv of queryPart.split('&')) { const [k,v] = kv.split('='); if (k) url.searchParams.set(k, v ?? ''); } } url.searchParams.set('s','1'); go(url.toString()); } function onScanFailure(_err) { /* per-frame decode errors are normal */ } let html5Scanner = null; // Html5QrcodeScanner UI let html5Instance = null; // Html5Qrcode direct instance (for cameraId fast start) // Overlay button shown only when we NEED a user gesture function showStartOverlay(startFn) { const host = document.getElementById(readerId); const overlay = document.createElement('button'); overlay.type = 'button'; overlay.className = 'btn btn-primary'; overlay.textContent = 'Start scanner'; Object.assign(overlay.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.1rem', border: 'none', borderRadius: getComputedStyle(host).borderRadius || '12px', background: 'rgba(0,123,255,0.95)', color: '#fff' }); overlay.addEventListener('click', async () => { overlay.remove(); await startFn(); }, { once: true }); host.appendChild(overlay); } async function startScannerUi() { setStatus('Starting scanner…'); html5Scanner = new Html5QrcodeScanner(readerId, { fps: 10, qrbox: 300, rememberLastUsedCamera: true }, false); html5Scanner.render(onScanSuccess, onScanFailure); setTimeout(() => { if (!document.querySelector('#' + readerId + ' video')) { setStatus('If you don’t see the camera prompt, allow camera access in the browser/site settings and reload.', true); } else { setStatus(''); } }, 1200); } async function startScannerFast(cameraId) { // Start directly with a specific deviceId (fastest path, no UI chrome) setStatus('Starting camera…'); html5Instance = new Html5Qrcode(readerId); try { await html5Instance.start( cameraId, { fps: 10, qrbox: 300, aspectRatio: 1.0 }, onScanSuccess, onScanFailure ); setStatus(''); } catch (e) { // Fallback to UI if deviceId fails (device unplugged/renamed) try { await html5Instance.clear(); } catch {} html5Instance = null; await startScannerUi(); } } async function pickPreferredOrEnvAndStart() { // If we stored a cameraId, try that first for instant start const savedId = localStorage.getItem(LS_KEY_CAMERA); try { const cameras = await Html5Qrcode.getCameras(); if (cameras?.length) { // Prefer saved device; else prefer environment/back const env = cameras.find(c => /back|rear|environment/i.test(c.label)) || cameras[0]; const chosen = cameras.find(c => c.id === savedId) || env; localStorage.setItem(LS_KEY_CAMERA, chosen.id); // store for next time return startScannerFast(chosen.id); } } catch (_) { // If listing cameras fails (permissions), fall back to UI (it will prompt) } return startScannerUi(); } // After user uses the UI once, capture the deviceId and remember it window.addEventListener('cameraSelection', async (ev) => { // Some browsers dispatch a devicechange; html5-qrcode doesn’t emit a public event. // We’ll also try to read the active device after UI starts: try { const cams = await Html5Qrcode.getCameras(); const env = cams.find(c => /back|rear|environment/i.test(c.label)) || cams[0]; if (env) localStorage.setItem(LS_KEY_CAMERA, env.id); } catch {} }); // === Permission strategy === (async () => { // If permission already granted → auto-start with preferred/env camera (no prompt) try { const perm = await (navigator.permissions?.query({ name: 'camera' }) ?? Promise.reject()); if (perm.state === 'granted') { pickPreferredOrEnvAndStart(); return; } if (perm.state === 'prompt') { // We need a user gesture; show one big button showStartOverlay(() => startScannerUi()); setStatus('Tap “Start scanner” to allow camera.'); return; } // denied setStatus('Camera permission is blocked. Enable it in your browser/site settings, then reload.', true); return; } catch { // Permissions API not available (iOS older Safari). Try fast path; if it needs a gesture, show overlay. try { await pickPreferredOrEnvAndStart(); } catch { showStartOverlay(() => startScannerUi()); setStatus('Tap “Start scanner” to allow camera.'); } } })(); })(); </script> <style> /* Make the reader container responsive while keeping max-width */ #reader { display: block; width: 100%; max-width: 520px; aspect-ratio: 1 / 1; background: #f8f9fa; border-radius: 12px; position: relative; overflow: hidden; } /* Force the injected video / canvas to fully cover the reader */ #reader video, #reader canvas, #reader .html5-qrcode-video, #reader .html5-qrcode-canvas { width: 100% !important; height: 100% !important; object-fit: cover !important; } /* Some internal wrappers the library creates — ensure they don't limit size */ #reader .html5-qrcode-placeholder, #reader .html5-qrcode-root, #reader .html5-qrcode-region { width: 100% !important; height: 100% !important; } /* Keep overlay/start button full size and visible on small screens */ #reader > button, #reader .btn { width: 100%; height: 100%; font-size: 1.05rem; border-radius: 12px; } /* Small-screen tweak */ @media (max-width: 576px) { #reader .btn { font-size: 1rem; } } </style> @endsection