loko/streetup/mobile/templates/mobile/qr_scanner.html
2026-07-22 14:48:40 +02:00

243 lines
7.6 KiB
HTML

{% extends 'mobile/mobile_base.html' %}
{% load static %}
{% load i18n %}
{% block title %}{% translate "Scanner QR" %}{% endblock title %}
{% block upleft %}
<a href="javascript:history.back()" class="nav-link" style="color: white;">
<i class="bi bi-arrow-left fs-3"></i>
</a>
{% endblock upleft %}
{% block extra_head %}
<style>
#scanner-container {
position: relative;
width: 100%;
max-width: 400px;
margin: 0 auto;
}
#video {
width: 100%;
border-radius: 1rem;
background: #000;
display: block;
}
#canvas { display: none; }
#scan-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
aspect-ratio: 1;
border: 3px solid rgba(255,255,255,0.85);
border-radius: 0.75rem;
box-shadow: 0 0 0 9999px rgba(0,0,0,0.35);
pointer-events: none;
}
#scan-line {
position: absolute;
left: 5%;
right: 5%;
height: 2px;
background: linear-gradient(to right, transparent, #2f3e8b, transparent);
animation: scanline 2s linear infinite;
}
@keyframes scanline {
0% { top: 8%; }
100% { top: 92%; }
}
#status-bar {
text-align: center;
padding: 0.75rem 0 0.5rem;
font-size: 0.9rem;
color: #6c757d;
min-height: 2.5rem;
}
#result-card {
display: none;
border-radius: 0.75rem;
border: 1px solid #dee2e6;
padding: 1rem;
margin-top: 1rem;
background: #fff;
box-shadow: 0 2px 8px rgba(47,62,139,0.08);
text-align: center;
}
#error-card {
display: none;
border-radius: 0.75rem;
border: 1px solid #f5c2c7;
padding: 1rem;
margin-top: 1rem;
background: #fff3f4;
text-align: center;
color: #842029;
}
#start-btn, #restart-btn {
display: block;
width: 100%;
max-width: 400px;
margin: 1rem auto 0;
}
</style>
{% endblock extra_head %}
{% block content %}
<div class="text-center mb-3">
<p class="text-muted" style="font-size:0.9rem;">
{% translate "Pointez la caméra vers un QR code StreetUp pour être redirigé automatiquement." %}
</p>
</div>
<div id="scanner-container">
<video id="video" autoplay playsinline muted></video>
<canvas id="canvas"></canvas>
<div id="scan-overlay"><div id="scan-line"></div></div>
</div>
<div id="status-bar">{% translate "Initialisation de la caméra…" %}</div>
<div id="result-card">
<i class="bi bi-check-circle-fill text-success fs-3 d-block mb-2"></i>
<div id="result-text" class="fw-semibold mb-3"></div>
<a id="result-link" class="btn btn-primary w-100">{% translate "Ouvrir" %}</a>
</div>
<div id="error-card">
<i class="bi bi-exclamation-triangle-fill fs-3 d-block mb-2"></i>
<div id="error-text" class="mb-3"></div>
<button id="restart-btn-err" class="btn btn-outline-secondary btn-sm">{% translate "Réessayer" %}</button>
</div>
<button id="start-btn" class="btn btn-outline-secondary" style="display:none;">
<i class="bi bi-camera me-1"></i>{% translate "Démarrer la caméra" %}
</button>
<!-- jsQR via CDN -->
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script>
(function () {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status-bar');
const resultCard = document.getElementById('result-card');
const resultText = document.getElementById('result-text');
const resultLink = document.getElementById('result-link');
const errorCard = document.getElementById('error-card');
const errorText = document.getElementById('error-text');
const startBtn = document.getElementById('start-btn');
const restartErrBtn = document.getElementById('restart-btn-err');
const RESOLVE_URL = "{% url 'mobile:qr_resolve' %}";
let scanning = false;
let rafId = null;
function setStatus(msg) { statusEl.textContent = msg; }
async function startCamera() {
startBtn.style.display = 'none';
resultCard.style.display = 'none';
errorCard.style.display = 'none';
setStatus("{% translate 'Initialisation de la caméra…' %}");
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }
});
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
scanning = true;
setStatus("{% translate 'Pointez vers un QR code…' %}");
tick();
};
} catch (err) {
setStatus('');
startBtn.style.display = 'block';
console.error('Camera error:', err);
}
}
function tick() {
if (!scanning) return;
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert'
});
if (code) {
scanning = false;
cancelAnimationFrame(rafId);
handleCode(code.data);
return;
}
}
rafId = requestAnimationFrame(tick);
}
function stopCamera() {
scanning = false;
if (video.srcObject) {
video.srcObject.getTracks().forEach(t => t.stop());
video.srcObject = null;
}
}
async function handleCode(data) {
setStatus("{% translate 'QR Code détecté, résolution…' %}");
try {
const resp = await fetch(RESOLVE_URL + '?code=' + encodeURIComponent(data), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const json = await resp.json();
if (json.url) {
stopCamera();
resultText.textContent = json.label || data;
resultLink.href = json.url;
resultCard.style.display = 'block';
setStatus('');
// Redirection automatique après 1.2s
setTimeout(() => { window.location.href = json.url; }, 1200);
} else {
showError(json.error || "{% translate 'QR Code non reconnu.' %}");
}
} catch (e) {
showError("{% translate 'Erreur lors de la résolution.' %}");
}
}
function showError(msg) {
errorText.textContent = msg;
errorCard.style.display = 'block';
setStatus('');
// Reprendre le scan après 2.5s
setTimeout(() => {
errorCard.style.display = 'none';
scanning = true;
setStatus("{% translate 'Pointez vers un QR code…' %}");
tick();
}, 2500);
}
restartErrBtn.addEventListener('click', () => {
errorCard.style.display = 'none';
scanning = true;
setStatus("{% translate 'Pointez vers un QR code…' %}");
tick();
});
startBtn.addEventListener('click', startCamera);
// Démarrage automatique
startCamera();
})();
</script>
{% endblock content %}