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 '
'; + echo ' data-qr-msg-camera="' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_camera_error')) . '">'; echo ''; fxInscrMobileRenderPanelSummary(fxInscrMobileT('inscr_mobile_qr_title'), 'fa-qrcode'); echo ''; echo '
'; - - echo '
'; - echo ''; - if ($blnDossierMode || $strContext === 'list') { - echo ''; - } - echo '
'; - - echo '

' - . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_hint')) . '

'; - echo '

' - . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_dossier_hint')) . '

'; - + echo '

' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_hint')) . '

'; echo ''; - echo ''; echo '
'; echo '
'; + echo '
'; +} + +function fxInscrMobileRenderBibScanBlock($strBibInputId, $strLangue) { + echo '
'; + echo '

' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_bib_scan_hint')) . '

'; + echo '
'; + echo '
'; echo '
'; - echo '
'; - echo ''; - echo ''; - echo '

' . fxInscrMobileEsc(fxInscrMobileT('inscr_mobile_qr_dossier_ocr_hint')) . '

'; - echo '
'; - echo ''; } function fxInscrMobileRenderList($intEveId, $strLangue, $strBaseUrl, $arrReq) { @@ -1034,7 +983,7 @@ function fxInscrMobileRenderList($intEveId, $strLangue, $strBaseUrl, $arrReq) { } fxInscrMobileRenderPageHead(fxInscrMobileT('inscr_mobile_title'), $strEveNom); - fxInscrMobileRenderQrPanel($intEveId, $strLangue, 'list', array('dossier_mode' => true)); + fxInscrMobileRenderQrPanel($intEveId, $strLangue); // Recherche (repliable — fermée par défaut pour montrer la liste d'abord) echo '
'; @@ -1283,28 +1232,34 @@ function fxInscrMobileRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq, $ echo ''; } - if (fxInscrMobileCanScan($intComId, $intEveId)) { - fxInscrMobileRenderQrPanel($intEveId, $strLangue, 'fiche', array( - 'dossier_mode' => $blnCanBib, - 'bib_input_id' => $blnCanBib ? ($strBibInputId . $intParId) : '', - )); - } - if ($blnHasEditable) { echo '
'; fxInscrMobileRenderSectionTitle(fxInscrMobileT('inscr_mobile_section_editable'), 'fa-pencil-square-o'); if ($blnCanBib) { + $strFullBibInputId = $strBibInputId . $intParId; + $blnCanBibScan = fxInscrMobileCanScan($intComId, $intEveId); echo '
'; echo '
' . fxInscrMobileEsc(fxInscrMobileT('promoteur_bib_number')) . '
'; echo '
'; - echo ''; + echo ''; + if ($blnCanBibScan) { + echo ''; + } echo ''; - echo '
'; + echo '
'; + if ($blnCanBibScan) { + fxInscrMobileRenderBibScanBlock($strFullBibInputId, $strLangue); + } + echo ''; } if ($blnCanStatut) { diff --git a/php/inc_settings.php b/php/inc_settings.php index b0ed6103..f467531e 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.693'); +define('_VERSION_CODE', '4.72.694'); define('_DATE_CODE', '2026-06-22'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/sql/MSIN-inscriptions-mobile-qr-i18n.sql b/sql/MSIN-inscriptions-mobile-qr-i18n.sql index 0455dbe9..57cfda04 100644 --- a/sql/MSIN-inscriptions-mobile-qr-i18n.sql +++ b/sql/MSIN-inscriptions-mobile-qr-i18n.sql @@ -1,5 +1,4 @@ --- Inscriptions mobile - lecteur QR + scan dossard OCR (complet) --- Executer le fichier EN ENTIER dans phpMyAdmin (onglet SQL, coller tout, Go). +-- Inscriptions mobile - lecteur QR (liste) + scan dossard sur fiche DELETE FROM info WHERE info_prg = 'compte.php' @@ -10,20 +9,11 @@ WHERE info_prg = 'compte.php' 'inscr_mobile_qr_not_found', 'inscr_mobile_qr_wrong_event', 'inscr_mobile_qr_camera_error', - 'inscr_mobile_qr_mode_label', - 'inscr_mobile_qr_mode_participant', - 'inscr_mobile_qr_mode_dossier', - 'inscr_mobile_qr_dossier_hint', - 'inscr_mobile_qr_dossier_invalid', - 'inscr_mobile_qr_dossier_found', - 'inscr_mobile_qr_dossier_no_fiche', - 'inscr_mobile_qr_dossier_ocr_btn', - 'inscr_mobile_qr_dossier_ocr_hint', - 'inscr_mobile_qr_dossier_ocr_busy', - 'inscr_mobile_qr_dossier_ocr_fail', - 'inscr_mobile_qr_dossier_scanning', - 'inscr_mobile_qr_dossier_preview', - 'inscr_mobile_qr_dossier_photo_btn' + 'inscr_mobile_bib_scan_btn', + 'inscr_mobile_bib_scan_hint', + 'inscr_mobile_bib_scan_scanning', + 'inscr_mobile_bib_scan_found', + 'inscr_mobile_bib_scan_invalid' ); INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) VALUES @@ -39,31 +29,13 @@ INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_ ('inscr_mobile_qr_wrong_event', 'en', 'This QR code belongs to another event: %s', '', 'compte.php', '', 0, 1, '', '', '', NOW()), ('inscr_mobile_qr_camera_error', 'fr', 'Impossible d''acceder a la camera.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), ('inscr_mobile_qr_camera_error', 'en', 'Unable to access the camera.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_mode_label', 'fr', 'Mode de lecture', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_mode_label', 'en', 'Scan mode', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_mode_participant', 'fr', 'Participant (facture)', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_mode_participant', 'en', 'Participant (invoice)', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_mode_dossier', 'fr', 'Numero de dossard (OCR)', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_mode_dossier', 'en', 'Bib number (OCR)', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_hint', 'fr', 'Cadrez le numero imprime sur le dossier. Lecture OCR automatique toutes les 2 s, ou Choisir une photo pour une image.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_hint', 'en', 'Frame the printed number on the folder. Automatic OCR every 2 s, or Choose a photo for an image file.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_invalid', 'fr', 'Aucun numero de dossard detecte.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_invalid', 'en', 'No bib number detected.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_found', 'fr', 'Numero propose : %s - appuyez sur OK pour enregistrer.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_found', 'en', 'Suggested number: %s - press OK to save.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_no_fiche', 'fr', 'Ouvrez d''abord la fiche d''un participant (scannez sa facture).', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_no_fiche', 'en', 'Open a participant record first (scan their invoice).', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_btn', 'fr', 'Analyser maintenant', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_btn', 'en', 'Analyze now', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_hint', 'fr', 'Mode OCR uniquement - pas de code QR ni code-barres pour les dossards.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_hint', 'en', 'OCR mode only - no QR or barcode for bib numbers.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_busy', 'fr', 'Lecture du numero en cours...', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_busy', 'en', 'Reading number...', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_fail', 'fr', 'Impossible de lire le numero. Reessayez ou saisissez-le manuellement.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_ocr_fail', 'en', 'Could not read the number. Try again or enter it manually.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_scanning', 'fr', 'Lecture OCR - cadrez le numero imprime', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_scanning', 'en', 'OCR reading - frame the printed number', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_preview', 'fr', 'Numero detecte : %s (ouvrez une fiche participant pour l''associer)', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_preview', 'en', 'Number detected: %s (open a participant record to assign it)', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_photo_btn', 'fr', 'Choisir une photo', '', 'compte.php', '', 0, 1, '', '', '', NOW()), -('inscr_mobile_qr_dossier_photo_btn', 'en', 'Choose a photo', '', 'compte.php', '', 0, 1, '', '', '', NOW()); +('inscr_mobile_bib_scan_btn', 'fr', 'Scanner le numero', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_btn', 'en', 'Scan number', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_hint', 'fr', 'Cadrez un seul numero au centre et maintenez stable.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_hint', 'en', 'Frame a single number in the centre and hold steady.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_scanning', 'fr', 'Lecture en cours...', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_scanning', 'en', 'Reading...', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_found', 'fr', 'Numero %s detecte - validez avec OK.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_found', 'en', 'Number %s detected - press OK.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_invalid', 'fr', 'Numero illisible. Cadrez un seul numero.', '', 'compte.php', '', 0, 1, '', '', '', NOW()), +('inscr_mobile_bib_scan_invalid', 'en', 'Unreadable number. Frame a single number.', '', 'compte.php', '', 0, 1, '', '', '', NOW());