243 lines
7.6 KiB
HTML
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 %}
|