diff --git a/css/style.css b/css/style.css
index 90e273fc..5158585d 100644
--- a/css/style.css
+++ b/css/style.css
@@ -2468,74 +2468,6 @@ a.ms1-trad-link.btn-aide-trad{
margin:0 0 8px;
}
-.inscr-mobile-qr-mode{
- display:flex;
- flex-wrap:wrap;
- gap:8px 16px;
- margin:0 0 10px;
-}
-
-.inscr-mobile-qr-mode__opt{
- display:inline-flex;
- align-items:center;
- gap:6px;
- margin:0;
- font-size:13px;
- font-weight:600;
- cursor:pointer;
-}
-
-.inscr-mobile-qr-mode__opt input{
- margin:0;
-}
-
-.inscr-mobile-qr-dossier-actions{
- margin-top:10px;
- display:flex;
- flex-wrap:wrap;
- gap:8px;
- align-items:center;
-}
-
-.inscr-mobile-qr-photo-btn{
- cursor:pointer;
- margin:0;
-}
-
-.inscr-mobile-qr-photo-btn input{
- display:none;
-}
-
-.inscr-mobile-qr-dossier-status{
- color:#6f42c1;
- font-weight:600;
-}
-
-.inscr-mobile-qr-dossier-status.is-active::before{
- content:'';
- display:inline-block;
- width:8px;
- height:8px;
- margin-right:6px;
- border-radius:50%;
- background:#6f42c1;
- animation:inscr-mobile-qr-pulse 1.2s ease-in-out infinite;
-}
-
-@keyframes inscr-mobile-qr-pulse{
- 0%,100%{ opacity:1; transform:scale(1); }
- 50%{ opacity:.4; transform:scale(.85); }
-}
-
-.inscr-mobile-qr-ocr-hint{
- margin-top:6px;
-}
-
-.inscr-mobile-bib-row input.inscr-mobile-bib--proposed{
- border-color:#6f42c1;
- box-shadow:0 0 0 2px rgba(111,66,193,.2);
-}
-
.inscr-mobile-qr-reader-wrap{
max-width:100%;
margin:0 auto;
@@ -2549,6 +2481,56 @@ a.ms1-trad-link.btn-aide-trad{
border-radius:4px;
}
+.inscr-mobile-bib-scan-toggle.is-active{
+ background:#6f42c1;
+ border-color:#6f42c1;
+ color:#fff;
+}
+
+.inscr-mobile-bib-scan-panel{
+ margin-top:8px;
+}
+
+.inscr-mobile-bib-scan-status{
+ color:#6f42c1;
+ font-weight:600;
+}
+
+.inscr-mobile-bib-scan-status.is-active::before{
+ content:'';
+ display:inline-block;
+ width:7px;
+ height:7px;
+ margin-right:5px;
+ border-radius:50%;
+ background:#6f42c1;
+ animation:inscr-mobile-qr-pulse 1.2s ease-in-out infinite;
+}
+
+.inscr-mobile-bib-scan-video{
+ overflow:hidden;
+ border-radius:4px;
+ background:#111;
+ max-height:100px;
+}
+
+.inscr-mobile-bib-scan-video video{
+ display:block;
+ width:100%;
+ max-height:100px;
+ object-fit:cover;
+}
+
+.inscr-mobile-bib-row input.inscr-mobile-bib--proposed{
+ border-color:#6f42c1;
+ box-shadow:0 0 0 2px rgba(111,66,193,.2);
+}
+
+@keyframes inscr-mobile-qr-pulse{
+ 0%,100%{ opacity:1; transform:scale(1); }
+ 50%{ opacity:.4; transform:scale(.85); }
+}
+
.inscr-mobile-label{
display:block;
font-size:12px;
diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php
index 4fe369d6..f2bee375 100644
--- a/inc_footer_scripts.php
+++ b/inc_footer_scripts.php
@@ -3555,132 +3555,11 @@ if ($strLangue == 'fr') {
var qrReaderEl = document.getElementById('inscr-mobile-qr-reader');
var qrFeedback = document.getElementById('inscr-mobile-qr-feedback');
- var qrHintParticipant = document.getElementById('inscr-mobile-qr-hint-participant');
- var qrHintDossier = document.getElementById('inscr-mobile-qr-hint-dossier');
- var qrDossierActions = document.getElementById('inscr-mobile-qr-dossier-actions');
- var qrDossierStatus = document.getElementById('inscr-mobile-qr-dossier-status');
- var qrOcrBtn = document.getElementById('inscr-mobile-qr-ocr-btn');
- var qrPhotoInput = document.getElementById('inscr-mobile-qr-photo-input');
- var qrModeInputs = qrPanel.querySelectorAll('input[name="inscr_mobile_qr_mode"]');
var qrCamera = null;
- var dossierStream = null;
- var dossierVideo = null;
var qrStarting = false;
var lastQrRaw = '';
- var lastDossierNum = '';
var qrResolvePending = false;
var qrLibPromise = null;
- var tesseractPromise = null;
- var ocrBusy = false;
- var ocrWorker = null;
- var ocrWorkerPromise = null;
- var ocrIntervalId = null;
- var ocrFirstRun = true;
-
- function getQrMode() {
- var checked = qrPanel.querySelector('input[name="inscr_mobile_qr_mode"]:checked');
- return checked ? checked.value : (qrPanel.getAttribute('data-qr-default-mode') || 'participant');
- }
-
- function updateQrModeUi() {
- var mode = getQrMode();
- var isDossier = mode === 'dossier';
- if (qrHintParticipant) {
- qrHintParticipant.hidden = isDossier;
- }
- if (qrHintDossier) {
- qrHintDossier.hidden = !isDossier;
- }
- if (qrDossierActions) {
- qrDossierActions.hidden = !isDossier;
- }
- if (isDossier) {
- preloadOcrWorker();
- } else {
- stopDossierOcrLoop();
- terminateOcrWorker();
- setDossierStatus('');
- }
- }
-
- function setDossierStatus(message, active) {
- if (!qrDossierStatus) {
- return;
- }
- qrDossierStatus.textContent = message || '';
- qrDossierStatus.hidden = !message;
- qrDossierStatus.classList.toggle('is-active', !!active);
- }
-
- function preloadOcrWorker() {
- if (getQrMode() !== 'dossier') {
- return;
- }
- getOcrWorker().catch(function () {
- /* ignore preload errors */
- });
- }
-
- function getOcrWorker() {
- if (ocrWorker) {
- return Promise.resolve(ocrWorker);
- }
- if (ocrWorkerPromise) {
- return ocrWorkerPromise;
- }
- ocrWorkerPromise = loadTesseract().then(function () {
- return Tesseract.createWorker('eng');
- }).then(function (worker) {
- ocrWorker = worker;
- var params = {tessedit_char_whitelist: '0123456789'};
- if (window.Tesseract && Tesseract.PSM && Tesseract.PSM.SINGLE_BLOCK) {
- params.tessedit_pageseg_mode = Tesseract.PSM.SINGLE_BLOCK;
- }
- return worker.setParameters(params).then(function () {
- return ocrWorker;
- });
- }).catch(function (err) {
- ocrWorkerPromise = null;
- throw err;
- });
- return ocrWorkerPromise;
- }
-
- function terminateOcrWorker() {
- if (ocrWorker) {
- ocrWorker.terminate();
- ocrWorker = null;
- }
- ocrWorkerPromise = null;
- }
-
- function startDossierOcrLoop() {
- stopDossierOcrLoop();
- if (getQrMode() !== 'dossier' || !qrPanel.open) {
- return;
- }
- ocrFirstRun = true;
- setDossierStatus(qrPanel.getAttribute('data-qr-msg-dossier-scanning') || '', true);
- ocrIntervalId = window.setInterval(function () {
- if (getQrMode() !== 'dossier' || !qrPanel.open || ocrBusy) {
- return;
- }
- runOcrCapture(true);
- }, 2500);
- window.setTimeout(function () {
- if (getQrMode() === 'dossier' && qrPanel.open && !ocrBusy) {
- runOcrCapture(true);
- }
- }, 800);
- }
-
- function stopDossierOcrLoop() {
- if (ocrIntervalId) {
- window.clearInterval(ocrIntervalId);
- ocrIntervalId = null;
- }
- ocrFirstRun = true;
- }
function loadHtml5Qrcode() {
if (window.Html5Qrcode) {
@@ -3692,9 +3571,7 @@ if ($strLangue == 'fr') {
qrLibPromise = new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js';
- script.onload = function () {
- resolve();
- };
+ script.onload = function () { resolve(); };
script.onerror = function () {
qrLibPromise = null;
reject(new Error('load'));
@@ -3704,97 +3581,6 @@ if ($strLangue == 'fr') {
return qrLibPromise;
}
- function loadTesseract() {
- if (window.Tesseract) {
- return Promise.resolve();
- }
- if (tesseractPromise) {
- return tesseractPromise;
- }
- tesseractPromise = new Promise(function (resolve, reject) {
- var script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js';
- script.onload = function () {
- resolve();
- };
- script.onerror = function () {
- tesseractPromise = null;
- reject(new Error('load'));
- };
- document.head.appendChild(script);
- });
- return tesseractPromise;
- }
-
- function getScanFormats() {
- if (!window.Html5QrcodeSupportedFormats) {
- return undefined;
- }
- return [Html5QrcodeSupportedFormats.QR_CODE];
- }
-
- function stopDossierCamera() {
- if (dossierStream) {
- dossierStream.getTracks().forEach(function (track) {
- track.stop();
- });
- dossierStream = null;
- }
- if (dossierVideo) {
- dossierVideo.srcObject = null;
- dossierVideo = null;
- }
- if (qrReaderEl) {
- qrReaderEl.innerHTML = '';
- }
- return Promise.resolve();
- }
-
- function stopHtml5QrCamera() {
- if (!qrCamera) {
- return Promise.resolve();
- }
- var instance = qrCamera;
- qrCamera = null;
- return instance.stop().catch(function () {
- return null;
- }).then(function () {
- try {
- instance.clear();
- } catch (e) {
- /* ignore */
- }
- });
- }
-
- function startDossierCamera() {
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- return Promise.reject(new Error('no camera'));
- }
- return stopDossierCamera().then(function () {
- return navigator.mediaDevices.getUserMedia({
- video: {facingMode: 'environment'},
- audio: false
- });
- }).then(function (stream) {
- dossierStream = stream;
- dossierVideo = document.createElement('video');
- dossierVideo.setAttribute('playsinline', 'true');
- dossierVideo.setAttribute('autoplay', 'true');
- dossierVideo.muted = true;
- dossierVideo.style.width = '100%';
- dossierVideo.style.borderRadius = '4px';
- dossierVideo.srcObject = stream;
- qrReaderEl.innerHTML = '';
- qrReaderEl.appendChild(dossierVideo);
- return dossierVideo.play();
- }).then(function () {
- if (getQrMode() === 'dossier' && qrPanel.open) {
- startDossierOcrLoop();
- }
- });
- }
-
function showQrFeedback(type, message) {
if (!qrFeedback) {
return;
@@ -3813,46 +3599,38 @@ if ($strLangue == 'fr') {
}
function stopQrScanner() {
- stopDossierOcrLoop();
- setDossierStatus('');
- return stopDossierCamera().then(function () {
- return stopHtml5QrCamera();
+ if (!qrCamera) {
+ return Promise.resolve();
+ }
+ var instance = qrCamera;
+ qrCamera = null;
+ return instance.stop().catch(function () {
+ return null;
+ }).then(function () {
+ try {
+ instance.clear();
+ } catch (e) {
+ /* ignore */
+ }
});
}
function startQrScanner() {
- if (qrStarting || !qrReaderEl || !qrPanel.open) {
+ if (qrStarting || qrCamera || !qrReaderEl || !qrPanel.open) {
return;
}
qrStarting = true;
hideQrFeedback();
lastQrRaw = '';
- if (getQrMode() === 'dossier') {
- if (dossierStream) {
- qrStarting = false;
- return;
- }
- startDossierCamera().catch(function () {
- showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-camera') || '');
- }).then(function () {
- qrStarting = false;
- });
- return;
- }
-
- if (qrCamera) {
- qrStarting = false;
- return;
- }
-
loadHtml5Qrcode().then(function () {
- if (!qrPanel.open || getQrMode() !== 'participant') {
+ if (!qrPanel.open) {
qrStarting = false;
return;
}
- var formats = getScanFormats();
- var config = formats ? {formatsToSupport: formats} : undefined;
+ var config = window.Html5QrcodeSupportedFormats
+ ? {formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE]}
+ : undefined;
qrCamera = new Html5Qrcode('inscr-mobile-qr-reader', config);
return qrCamera.start(
{facingMode: 'environment'},
@@ -3866,174 +3644,6 @@ if ($strLangue == 'fr') {
});
}
- function restartQrScanner() {
- stopQrScanner().then(function () {
- lastQrRaw = '';
- qrResolvePending = false;
- if (qrPanel.open) {
- startQrScanner();
- }
- });
- }
-
- function extractDossierNumber(raw) {
- var text = String(raw || '').trim().replace(/\s+/g, '');
- if (!text) {
- return '';
- }
- if (/^\d+$/.test(text)) {
- return text;
- }
- var matches = text.match(/\d+/g);
- if (!matches || !matches.length) {
- return '';
- }
- matches.sort(function (a, b) {
- return b.length - a.length;
- });
- return matches[0];
- }
-
- function getBibInput() {
- var inputId = qrPanel.getAttribute('data-qr-bib-input-id');
- if (!inputId) {
- return null;
- }
- return document.getElementById(inputId);
- }
-
- function applyDossierNumber(strNum, silent) {
- if (!strNum) {
- if (!silent) {
- showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-dossier-invalid') || '');
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
- }
- return false;
- }
-
- if (strNum === lastDossierNum) {
- return true;
- }
-
- var bibInput = getBibInput();
- if (!bibInput) {
- lastDossierNum = strNum;
- stopDossierOcrLoop();
- setDossierStatus('');
- var previewTpl = qrPanel.getAttribute('data-qr-msg-dossier-preview') || '%s';
- showQrFeedback('success', previewTpl.replace('%s', strNum));
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
- return true;
- }
-
- lastDossierNum = strNum;
- bibInput.value = strNum;
- bibInput.classList.add('inscr-mobile-bib--proposed');
- bibInput.focus();
- bibInput.scrollIntoView({block: 'center', behavior: 'smooth'});
-
- stopDossierOcrLoop();
- setDossierStatus('');
-
- var tpl = qrPanel.getAttribute('data-qr-msg-dossier-found') || '%s';
- showQrFeedback('success', tpl.replace('%s', strNum));
-
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
- return true;
- }
-
- function scanDossierImageFile(file) {
- if (!file) {
- return;
- }
- setDossierStatus(qrPanel.getAttribute('data-qr-msg-dossier-ocr-busy') || '', true);
- new Promise(function (resolve, reject) {
- var reader = new FileReader();
- reader.onload = function () {
- resolve(reader.result);
- };
- reader.onerror = reject;
- reader.readAsDataURL(file);
- }).then(function (dataUrl) {
- return runOcrOnImageSource(dataUrl, false);
- }).catch(function () {
- showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-dossier-ocr-fail') || '');
- }).then(function () {
- if (qrPhotoInput) {
- qrPhotoInput.value = '';
- }
- });
- }
-
- function captureVideoFrame() {
- var video = dossierVideo || (qrReaderEl ? qrReaderEl.querySelector('video') : null);
- if (!video || !video.videoWidth) {
- return null;
- }
- var canvas = document.createElement('canvas');
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
- canvas.getContext('2d').drawImage(video, 0, 0);
- return canvas;
- }
-
- function runOcrOnImageSource(imageSource, silent) {
- if (ocrBusy) {
- return Promise.resolve(false);
- }
- ocrBusy = true;
- if (qrOcrBtn) {
- qrOcrBtn.disabled = true;
- }
- if (!silent) {
- showQrFeedback('info', qrPanel.getAttribute('data-qr-msg-dossier-ocr-busy') || '');
- } else if (ocrFirstRun) {
- ocrFirstRun = false;
- setDossierStatus(qrPanel.getAttribute('data-qr-msg-dossier-scanning') || '', true);
- }
-
- return getOcrWorker().then(function (worker) {
- return worker.recognize(imageSource);
- }).then(function (result) {
- var rawText = result && result.data ? String(result.data.text || '') : '';
- return applyDossierNumber(extractDossierNumber(rawText.replace(/\s+/g, '')), silent);
- }).catch(function () {
- if (!silent) {
- showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-dossier-ocr-fail') || '');
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
- }
- return false;
- }).then(function (found) {
- ocrBusy = false;
- if (qrOcrBtn) {
- qrOcrBtn.disabled = false;
- }
- if (!found && silent && getQrMode() === 'dossier' && qrPanel.open) {
- setDossierStatus(qrPanel.getAttribute('data-qr-msg-dossier-scanning') || '', true);
- }
- return found;
- });
- }
-
- function runOcrCapture(silent) {
- var canvas = captureVideoFrame();
- if (!canvas) {
- if (!silent) {
- showQrFeedback('warning', qrPanel.getAttribute('data-qr-msg-camera') || '');
- }
- return Promise.resolve(false);
- }
- return runOcrOnImageSource(canvas, !!silent);
- }
-
function onQrDecoded(decodedText) {
if (!decodedText || decodedText === lastQrRaw || qrResolvePending) {
return;
@@ -4044,7 +3654,6 @@ if ($strLangue == 'fr') {
var resolveUrl = qrPanel.getAttribute('data-qr-resolve-url');
var eveId = qrPanel.getAttribute('data-qr-eve-id');
var lang = qrPanel.getAttribute('data-qr-lang') || 'fr';
-
if (!resolveUrl || !eveId) {
qrResolvePending = false;
return;
@@ -4066,12 +3675,9 @@ if ($strLangue == 'fr') {
qrResolvePending = false;
if (!data || !data.state) {
showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-invalid') || '');
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
+ window.setTimeout(function () { lastQrRaw = ''; }, 2000);
return;
}
-
if (data.state === 'success' && data.pec_id) {
stopQrScanner().then(function () {
var url = new URL(window.location.href);
@@ -4080,82 +3686,299 @@ if ($strLangue == 'fr') {
});
return;
}
-
if (data.state === 'wrong_event') {
var wrongTpl = qrPanel.getAttribute('data-qr-msg-wrong') || '';
- var wrongMsg = data.message || wrongTpl.replace('%s', data.eve_nom || '');
- showQrFeedback('warning', wrongMsg);
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2500);
+ showQrFeedback('warning', data.message || wrongTpl.replace('%s', data.eve_nom || ''));
+ window.setTimeout(function () { lastQrRaw = ''; }, 2500);
return;
}
-
showQrFeedback('danger', data.message || qrPanel.getAttribute('data-qr-msg-invalid') || '');
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
+ window.setTimeout(function () { lastQrRaw = ''; }, 2000);
}).catch(function () {
qrResolvePending = false;
showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-invalid') || '');
- window.setTimeout(function () {
- lastQrRaw = '';
- }, 2000);
- });
- }
-
- updateQrModeUi();
-
- qrModeInputs.forEach(function (input) {
- input.addEventListener('change', function () {
- lastDossierNum = '';
- updateQrModeUi();
- hideQrFeedback();
- restartQrScanner();
- });
- });
-
- if (qrOcrBtn) {
- qrOcrBtn.addEventListener('click', function () {
- runOcrCapture(false);
- });
- }
-
- if (qrPhotoInput) {
- qrPhotoInput.addEventListener('change', function () {
- if (!qrPhotoInput.files || !qrPhotoInput.files.length) {
- return;
- }
- scanDossierImageFile(qrPhotoInput.files[0]);
- });
- }
-
- var bibInput = getBibInput();
- if (bibInput) {
- bibInput.addEventListener('input', function () {
- bibInput.classList.remove('inscr-mobile-bib--proposed');
+ window.setTimeout(function () { lastQrRaw = ''; }, 2000);
});
}
qrPanel.addEventListener('toggle', function () {
if (qrPanel.open) {
- lastDossierNum = '';
- updateQrModeUi();
startQrScanner();
} else {
stopQrScanner();
- terminateOcrWorker();
hideQrFeedback();
lastQrRaw = '';
- lastDossierNum = '';
qrResolvePending = false;
}
});
+ })();
- if (qrPanel.open) {
- updateQrModeUi();
- startQrScanner();
+ (function () {
+ var BIB_MAX_LEN = 6;
+ var STABLE_READS = 2;
+ var tesseractPromise = null;
+ var activeScan = null;
+
+ function loadTesseract() {
+ if (window.Tesseract) {
+ return Promise.resolve();
+ }
+ if (tesseractPromise) {
+ return tesseractPromise;
+ }
+ tesseractPromise = new Promise(function (resolve, reject) {
+ var script = document.createElement('script');
+ script.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js';
+ script.onload = function () { resolve(); };
+ script.onerror = function () {
+ tesseractPromise = null;
+ reject(new Error('load'));
+ };
+ document.head.appendChild(script);
+ });
+ return tesseractPromise;
}
+
+ function extractBibNumber(raw) {
+ var matches = String(raw || '').match(/\d+/g);
+ if (!matches || !matches.length) {
+ return '';
+ }
+ if (matches.length > 1) {
+ return '';
+ }
+ var num = matches[0];
+ if (num.length < 1 || num.length > BIB_MAX_LEN) {
+ return '';
+ }
+ return num;
+ }
+
+ function captureCenterFrame(videoEl) {
+ if (!videoEl || !videoEl.videoWidth) {
+ return null;
+ }
+ var w = videoEl.videoWidth;
+ var h = videoEl.videoHeight;
+ var cropW = Math.round(w * 0.55);
+ var cropH = Math.round(h * 0.3);
+ var sx = Math.round((w - cropW) / 2);
+ var sy = Math.round((h - cropH) / 2);
+ var canvas = document.createElement('canvas');
+ canvas.width = cropW;
+ canvas.height = cropH;
+ canvas.getContext('2d').drawImage(videoEl, sx, sy, cropW, cropH, 0, 0, cropW, cropH);
+ return canvas;
+ }
+
+ function BibScanSession(block) {
+ this.block = block;
+ this.panel = block.querySelector('.inscr-mobile-bib-scan-panel');
+ this.toggleBtn = block.querySelector('.inscr-mobile-bib-scan-toggle');
+ this.statusEl = this.panel ? this.panel.querySelector('.inscr-mobile-bib-scan-status') : null;
+ this.videoWrap = this.panel ? this.panel.querySelector('.inscr-mobile-bib-scan-video') : null;
+ this.inputId = this.panel ? this.panel.getAttribute('data-bib-input-id') : '';
+ this.stream = null;
+ this.video = null;
+ this.worker = null;
+ this.workerPromise = null;
+ this.intervalId = null;
+ this.ocrBusy = false;
+ this.pendingNum = '';
+ this.pendingCount = 0;
+ }
+
+ BibScanSession.prototype.getInput = function () {
+ return this.inputId ? document.getElementById(this.inputId) : null;
+ };
+
+ BibScanSession.prototype.setStatus = function (text, active) {
+ if (!this.statusEl) {
+ return;
+ }
+ this.statusEl.textContent = text || '';
+ this.statusEl.classList.toggle('is-active', !!active);
+ };
+
+ BibScanSession.prototype.getWorker = function () {
+ var self = this;
+ if (self.worker) {
+ return Promise.resolve(self.worker);
+ }
+ if (self.workerPromise) {
+ return self.workerPromise;
+ }
+ self.workerPromise = loadTesseract().then(function () {
+ return Tesseract.createWorker('eng');
+ }).then(function (worker) {
+ self.worker = worker;
+ var params = {tessedit_char_whitelist: '0123456789'};
+ if (window.Tesseract && Tesseract.PSM && Tesseract.PSM.SINGLE_LINE) {
+ params.tessedit_pageseg_mode = Tesseract.PSM.SINGLE_LINE;
+ }
+ return worker.setParameters(params).then(function () {
+ return self.worker;
+ });
+ }).catch(function (err) {
+ self.workerPromise = null;
+ throw err;
+ });
+ return self.workerPromise;
+ };
+
+ BibScanSession.prototype.stop = function () {
+ if (this.intervalId) {
+ window.clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ if (this.stream) {
+ this.stream.getTracks().forEach(function (track) { track.stop(); });
+ this.stream = null;
+ }
+ if (this.video) {
+ this.video.srcObject = null;
+ this.video = null;
+ }
+ if (this.videoWrap) {
+ this.videoWrap.innerHTML = '';
+ }
+ this.pendingNum = '';
+ this.pendingCount = 0;
+ this.setStatus('');
+ if (this.panel) {
+ this.panel.hidden = true;
+ }
+ if (this.toggleBtn) {
+ this.toggleBtn.setAttribute('aria-expanded', 'false');
+ this.toggleBtn.classList.remove('is-active');
+ }
+ if (activeScan === this) {
+ activeScan = null;
+ }
+ };
+
+ BibScanSession.prototype.applyNumber = function (strNum) {
+ var input = this.getInput();
+ if (!input) {
+ return;
+ }
+ input.value = strNum;
+ input.classList.add('inscr-mobile-bib--proposed');
+ input.focus();
+ var tpl = this.panel.getAttribute('data-msg-found') || '%s';
+ this.setStatus(tpl.replace('%s', strNum), false);
+ this.stop();
+ };
+
+ BibScanSession.prototype.considerNumber = function (strNum) {
+ if (!strNum) {
+ this.pendingNum = '';
+ this.pendingCount = 0;
+ return;
+ }
+ if (strNum === this.pendingNum) {
+ this.pendingCount += 1;
+ } else {
+ this.pendingNum = strNum;
+ this.pendingCount = 1;
+ }
+ if (this.pendingCount >= STABLE_READS) {
+ this.applyNumber(strNum);
+ }
+ };
+
+ BibScanSession.prototype.runOcr = function () {
+ var self = this;
+ if (self.ocrBusy || !self.video || activeScan !== self) {
+ return;
+ }
+ var canvas = captureCenterFrame(self.video);
+ if (!canvas) {
+ return;
+ }
+ self.ocrBusy = true;
+ self.getWorker().then(function (worker) {
+ return worker.recognize(canvas);
+ }).then(function (result) {
+ var rawText = result && result.data ? String(result.data.text || '') : '';
+ self.considerNumber(extractBibNumber(rawText));
+ }).catch(function () {
+ /* ignore transient OCR errors */
+ }).then(function () {
+ self.ocrBusy = false;
+ });
+ };
+
+ BibScanSession.prototype.start = function () {
+ var self = this;
+ if (!self.panel || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+ self.setStatus(self.panel.getAttribute('data-msg-camera') || '', false);
+ return;
+ }
+ if (activeScan && activeScan !== self) {
+ activeScan.stop();
+ }
+ activeScan = self;
+ self.panel.hidden = false;
+ if (self.toggleBtn) {
+ self.toggleBtn.setAttribute('aria-expanded', 'true');
+ self.toggleBtn.classList.add('is-active');
+ }
+ self.setStatus(self.panel.getAttribute('data-msg-scanning') || '', true);
+ self.getWorker().catch(function () { /* preload */ });
+
+ navigator.mediaDevices.getUserMedia({
+ video: {facingMode: 'environment'},
+ audio: false
+ }).then(function (stream) {
+ if (activeScan !== self) {
+ stream.getTracks().forEach(function (track) { track.stop(); });
+ return;
+ }
+ self.stream = stream;
+ self.video = document.createElement('video');
+ self.video.setAttribute('playsinline', 'true');
+ self.video.setAttribute('autoplay', 'true');
+ self.video.muted = true;
+ self.video.srcObject = stream;
+ self.videoWrap.innerHTML = '';
+ self.videoWrap.appendChild(self.video);
+ return self.video.play();
+ }).then(function () {
+ if (activeScan !== self) {
+ return;
+ }
+ self.runOcr();
+ self.intervalId = window.setInterval(function () {
+ self.runOcr();
+ }, 2500);
+ }).catch(function () {
+ self.setStatus(self.panel.getAttribute('data-msg-camera') || '', false);
+ self.stop();
+ });
+ };
+
+ document.querySelectorAll('.inscr-mobile-field-block[data-inscr-field="bib"]').forEach(function (block) {
+ var toggleBtn = block.querySelector('.inscr-mobile-bib-scan-toggle');
+ var panel = block.querySelector('.inscr-mobile-bib-scan-panel');
+ if (!toggleBtn || !panel) {
+ return;
+ }
+ var session = new BibScanSession(block);
+ toggleBtn.addEventListener('click', function () {
+ if (activeScan === session && !panel.hidden) {
+ session.stop();
+ return;
+ }
+ session.start();
+ });
+ var input = session.getInput();
+ if (input) {
+ input.addEventListener('input', function () {
+ input.classList.remove('inscr-mobile-bib--proposed');
+ });
+ }
+ });
})();
(function () {
diff --git a/php/inc_fx_inscriptions_mobile.php b/php/inc_fx_inscriptions_mobile.php
index d06f39c0..42f2801d 100644
--- a/php/inc_fx_inscriptions_mobile.php
+++ b/php/inc_fx_inscriptions_mobile.php
@@ -206,26 +206,11 @@ function fxInscrMobileFallbackTexte($strClef, $strLangue) {
'inscr_mobile_qr_not_found' => $blnFr ? 'Participant introuvable pour cet événement.' : 'Participant not found for this event.',
'inscr_mobile_qr_wrong_event' => $blnFr ? 'Ce code QR appartient à un autre événement : %s' : 'This QR code belongs to another event: %s',
'inscr_mobile_qr_camera_error' => $blnFr ? 'Impossible d\'accéder à la caméra.' : 'Unable to access the camera.',
- 'inscr_mobile_qr_mode_label' => $blnFr ? 'Mode de lecture' : 'Scan mode',
- 'inscr_mobile_qr_mode_participant' => $blnFr ? 'Participant (facture)' : 'Participant (invoice)',
- 'inscr_mobile_qr_mode_dossier' => $blnFr ? 'Numéro de dossard (OCR)' : 'Bib number (OCR)',
- 'inscr_mobile_qr_dossier_hint' => $blnFr
- ? 'Cadrez le numéro imprimé sur le dossier. Lecture OCR automatique toutes les 2 s, ou « Choisir une photo » pour une image.'
- : 'Frame the printed number on the folder. Automatic OCR every 2 s, or « Choose a photo » for an image file.',
- 'inscr_mobile_qr_dossier_invalid' => $blnFr ? 'Aucun numéro de dossard détecté.' : 'No bib number detected.',
- 'inscr_mobile_qr_dossier_found' => $blnFr ? 'Numéro proposé : %s — appuyez sur OK pour enregistrer.' : 'Suggested number: %s — press OK to save.',
- 'inscr_mobile_qr_dossier_no_fiche' => $blnFr ? 'Ouvrez d\'abord la fiche d\'un participant (scannez sa facture).' : 'Open a participant record first (scan their invoice).',
- 'inscr_mobile_qr_dossier_ocr_btn' => $blnFr ? 'Analyser maintenant' : 'Analyze now',
- 'inscr_mobile_qr_dossier_ocr_hint' => $blnFr
- ? 'Mode OCR uniquement — pas de code QR ni code-barres pour les dossards.'
- : 'OCR mode only — no QR or barcode for bib numbers.',
- 'inscr_mobile_qr_dossier_ocr_busy' => $blnFr ? 'Lecture du numéro en cours…' : 'Reading number…',
- 'inscr_mobile_qr_dossier_ocr_fail' => $blnFr ? 'Impossible de lire le numéro. Réessayez ou saisissez-le manuellement.' : 'Could not read the number. Try again or enter it manually.',
- 'inscr_mobile_qr_dossier_scanning' => $blnFr ? 'Lecture OCR — cadrez le numéro imprimé' : 'OCR reading — frame the printed number',
- 'inscr_mobile_qr_dossier_preview' => $blnFr
- ? 'Numéro détecté : %s (ouvrez une fiche participant pour l\'associer)'
- : 'Number detected: %s (open a participant record to assign it)',
- 'inscr_mobile_qr_dossier_photo_btn' => $blnFr ? 'Choisir une photo' : 'Choose a photo',
+ 'inscr_mobile_bib_scan_btn' => $blnFr ? 'Scanner le numéro' : 'Scan number',
+ 'inscr_mobile_bib_scan_hint' => $blnFr ? 'Cadrez un seul numéro au centre, puis maintenez stable.' : 'Frame a single number in the centre and hold steady.',
+ 'inscr_mobile_bib_scan_scanning' => $blnFr ? 'Lecture en cours...' : 'Reading...',
+ 'inscr_mobile_bib_scan_found' => $blnFr ? 'Numéro %s détecté — validez avec OK.' : 'Number %s detected — press OK.',
+ 'inscr_mobile_bib_scan_invalid' => $blnFr ? 'Numéro illisible. Cadrez un seul numéro.' : 'Unreadable number. Frame a single number.',
);
}
@@ -939,7 +924,7 @@ function fxInscrMobileRenderEventPicker($tabEveIds, $strLangue, $strBaseUrl) {
echo '';
}
-function fxInscrMobileRenderQrPanel($intEveId, $strLangue, $strContext, $arrOptions = array()) {
+function fxInscrMobileRenderQrPanel($intEveId, $strLangue) {
global $vDomaine;
$intComId = intval($_SESSION['com_id'] ?? 0);
@@ -947,73 +932,37 @@ function fxInscrMobileRenderQrPanel($intEveId, $strLangue, $strContext, $arrOpti
return;
}
- $blnDossierMode = !empty($arrOptions['dossier_mode']);
- $strDefaultMode = 'participant';
- if ($strContext === 'fiche' && $blnDossierMode) {
- $strDefaultMode = 'dossier';
- }
- $blnOpenPanel = ($strContext === 'fiche' && $strDefaultMode === 'dossier');
-
$strQrAjaxUrl = $vDomaine . '/ajax_inscr_mobile_qr.php';
- $strBibInputId = isset($arrOptions['bib_input_id']) ? trim((string)$arrOptions['bib_input_id']) : '';
- echo ' '
- . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_hint')) . ' '
- . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_dossier_hint')) . ' ' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_hint')) . '';
fxInscrMobileRenderPanelSummary(fxInscrMobileT('inscr_mobile_qr_title'), 'fa-qrcode');
echo '
';
echo '
' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_bib_scan_hint')) . '
'; + echo ''; + echo ''; echo '' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_dossier_ocr_hint')) . '
'; - echo '