Feature: add simple mobile camera scan mode
This commit is contained in:
160
app.js
160
app.js
@@ -58,6 +58,11 @@ const googleRestoreBtn = document.getElementById("googleRestoreBtn");
|
||||
const quickSearchInput = document.getElementById("quickSearchInput");
|
||||
const quickSearchResults = document.getElementById("quickSearchResults");
|
||||
const loanedFilterBtn = document.getElementById("loanedFilterBtn");
|
||||
const scannerStatus = document.getElementById("scannerStatus");
|
||||
const scannerStartBtn = document.getElementById("scannerStartBtn");
|
||||
const scannerStopBtn = document.getElementById("scannerStopBtn");
|
||||
const scannerVideo = document.getElementById("scannerVideo");
|
||||
const scannerLastCode = document.getElementById("scannerLastCode");
|
||||
const gamesList = document.getElementById("gamesList");
|
||||
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
||||
let editingGameId = null;
|
||||
@@ -65,6 +70,12 @@ let pendingRestoreMode = "merge";
|
||||
let quickSearchTerm = "";
|
||||
let googleStatus = { configured: false, connected: false, email: "" };
|
||||
let showLoanedOnly = false;
|
||||
let scannerDetector = null;
|
||||
let scannerStream = null;
|
||||
let scannerRunning = false;
|
||||
let scannerLoopId = null;
|
||||
let scannerLastCodeValue = "";
|
||||
let scannerLastCodeAt = 0;
|
||||
|
||||
toolsToggleBtn.addEventListener("click", () => {
|
||||
gamesDrawer.classList.remove("open");
|
||||
@@ -106,6 +117,9 @@ document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
toolsDrawer.classList.remove("open");
|
||||
gamesDrawer.classList.remove("open");
|
||||
if (scannerRunning) {
|
||||
stopScanner("Camera arretee.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,6 +175,18 @@ loanedFilterBtn.addEventListener("click", () => {
|
||||
renderGames();
|
||||
});
|
||||
|
||||
if (scannerStartBtn) {
|
||||
scannerStartBtn.addEventListener("click", async () => {
|
||||
await startScanner();
|
||||
});
|
||||
}
|
||||
|
||||
if (scannerStopBtn) {
|
||||
scannerStopBtn.addEventListener("click", () => {
|
||||
stopScanner("Camera arretee.");
|
||||
});
|
||||
}
|
||||
|
||||
platformForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -894,6 +920,131 @@ function resetEditMode() {
|
||||
cancelEditBtn.classList.add("hidden");
|
||||
}
|
||||
|
||||
function updateScannerStatus(message) {
|
||||
if (scannerStatus) {
|
||||
scannerStatus.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
function scannerSupported() {
|
||||
return typeof window !== "undefined" && "BarcodeDetector" in window && navigator.mediaDevices;
|
||||
}
|
||||
|
||||
async function startScanner() {
|
||||
if (!scannerVideo || !scannerStartBtn || !scannerStopBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scannerSupported()) {
|
||||
updateScannerStatus("Scan non supporte sur ce navigateur. Utilise Chrome mobile recente.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (scannerRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
scannerDetector = new window.BarcodeDetector({
|
||||
formats: ["ean_13", "ean_8", "upc_a", "upc_e", "code_128", "code_39", "qr_code"],
|
||||
});
|
||||
scannerStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: { ideal: "environment" },
|
||||
},
|
||||
audio: false,
|
||||
});
|
||||
|
||||
scannerVideo.srcObject = scannerStream;
|
||||
await scannerVideo.play();
|
||||
scannerRunning = true;
|
||||
scannerVideo.classList.remove("hidden");
|
||||
scannerStartBtn.classList.add("hidden");
|
||||
scannerStopBtn.classList.remove("hidden");
|
||||
updateScannerStatus("Scan en cours... vise le code-barres de la boite.");
|
||||
scanLoop();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
updateScannerStatus("Impossible d'acceder a la camera. Verifie les permissions.");
|
||||
stopScanner();
|
||||
}
|
||||
}
|
||||
|
||||
function stopScanner(message) {
|
||||
if (scannerLoopId) {
|
||||
cancelAnimationFrame(scannerLoopId);
|
||||
scannerLoopId = null;
|
||||
}
|
||||
|
||||
if (scannerVideo) {
|
||||
scannerVideo.pause();
|
||||
scannerVideo.srcObject = null;
|
||||
scannerVideo.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (scannerStream) {
|
||||
for (const track of scannerStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
scannerStream = null;
|
||||
}
|
||||
|
||||
scannerRunning = false;
|
||||
if (scannerStartBtn) {
|
||||
scannerStartBtn.classList.remove("hidden");
|
||||
}
|
||||
if (scannerStopBtn) {
|
||||
scannerStopBtn.classList.add("hidden");
|
||||
}
|
||||
updateScannerStatus(message || "Camera inactive.");
|
||||
}
|
||||
|
||||
async function scanLoop() {
|
||||
if (!scannerRunning || !scannerDetector || !scannerVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const barcodes = await scannerDetector.detect(scannerVideo);
|
||||
if (barcodes.length > 0) {
|
||||
const rawValue = normalizeText(barcodes[0].rawValue);
|
||||
if (rawValue) {
|
||||
const now = Date.now();
|
||||
if (rawValue !== scannerLastCodeValue || now - scannerLastCodeAt > 1800) {
|
||||
scannerLastCodeValue = rawValue;
|
||||
scannerLastCodeAt = now;
|
||||
applyScannedCode(rawValue);
|
||||
stopScanner(`Code detecte: ${rawValue}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
scannerLoopId = requestAnimationFrame(() => {
|
||||
scanLoop();
|
||||
});
|
||||
}
|
||||
|
||||
function applyScannedCode(codeValue) {
|
||||
if (scannerLastCode) {
|
||||
scannerLastCode.textContent = `Dernier code detecte: ${codeValue}`;
|
||||
scannerLastCode.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (quickSearchInput) {
|
||||
quickSearchInput.value = codeValue;
|
||||
quickSearchTerm = codeValue;
|
||||
renderSearchResults();
|
||||
}
|
||||
|
||||
if (titleInput && !normalizeText(titleInput.value)) {
|
||||
titleInput.value = codeValue;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
@@ -1059,11 +1210,20 @@ async function bootstrap() {
|
||||
await hydrateFromApi();
|
||||
normalizeState();
|
||||
render();
|
||||
if (scannerSupported()) {
|
||||
updateScannerStatus("Camera inactive. Appuie sur Demarrer scan.");
|
||||
} else {
|
||||
updateScannerStatus("Scan non supporte sur ce navigateur.");
|
||||
}
|
||||
handleGoogleCallbackResult();
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
stopScanner();
|
||||
});
|
||||
|
||||
function handleGoogleCallbackResult() {
|
||||
const url = new URL(window.location.href);
|
||||
const googleParam = url.searchParams.get("google");
|
||||
|
||||
12
index.html
12
index.html
@@ -123,6 +123,18 @@
|
||||
<div class="games-actions-bar">
|
||||
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
|
||||
</div>
|
||||
<div class="scanner-zone">
|
||||
<div class="scanner-header">
|
||||
<strong>Scan camera (mobile)</strong>
|
||||
<p id="scannerStatus" class="scanner-status">Camera inactive.</p>
|
||||
</div>
|
||||
<div class="scanner-actions">
|
||||
<button id="scannerStartBtn" type="button" class="btn-secondary">Demarrer scan</button>
|
||||
<button id="scannerStopBtn" type="button" class="btn-secondary hidden">Arreter scan</button>
|
||||
</div>
|
||||
<video id="scannerVideo" class="scanner-video hidden" autoplay playsinline muted></video>
|
||||
<p id="scannerLastCode" class="scanner-last-code hidden"></p>
|
||||
</div>
|
||||
|
||||
<div class="search-zone">
|
||||
<label>
|
||||
|
||||
45
styles.css
45
styles.css
@@ -276,6 +276,51 @@ h1 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.scanner-zone {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.9rem;
|
||||
background: #f7fbf8;
|
||||
}
|
||||
|
||||
.scanner-header {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.scanner-header strong {
|
||||
font-size: 0.92rem;
|
||||
color: #19334a;
|
||||
}
|
||||
|
||||
.scanner-status {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.scanner-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin: 0.65rem 0;
|
||||
}
|
||||
|
||||
.scanner-video {
|
||||
width: min(100%, 420px);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ccd8e6;
|
||||
background: #101820;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scanner-last-code {
|
||||
margin: 0.6rem 0 0;
|
||||
font-size: 0.86rem;
|
||||
color: #204564;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quick-search-results {
|
||||
margin-top: 0.65rem;
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user