From e8883cfbf867054b2bae98ce4567b2c06eee1c77 Mon Sep 17 00:00:00 2001 From: stephan Date: Mon, 22 Jun 2026 15:02:54 -0400 Subject: [PATCH] Refactor mobile registration QR scanning and update UI elements This commit streamlines the mobile registration module by removing unused QR mode functionalities and updating the UI for the bib scanning process. It introduces new feedback messages and adjusts the layout for improved user experience. The version code is updated to 4.72.694 to reflect these changes. Additionally, the SQL file is modified to align with the new bib scan terminology. --- css/style.css | 118 ++-- inc_footer_scripts.php | 757 +++++++++-------------- php/inc_fx_inscriptions_mobile.php | 121 ++-- php/inc_settings.php | 2 +- sql/MSIN-inscriptions-mobile-qr-i18n.sql | 60 +- 5 files changed, 395 insertions(+), 663 deletions(-) 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());