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 quickSearchInput = document.getElementById("quickSearchInput");
|
||||||
const quickSearchResults = document.getElementById("quickSearchResults");
|
const quickSearchResults = document.getElementById("quickSearchResults");
|
||||||
const loanedFilterBtn = document.getElementById("loanedFilterBtn");
|
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 gamesList = document.getElementById("gamesList");
|
||||||
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
const gameCardTemplate = document.getElementById("gameCardTemplate");
|
||||||
let editingGameId = null;
|
let editingGameId = null;
|
||||||
@@ -65,6 +70,12 @@ let pendingRestoreMode = "merge";
|
|||||||
let quickSearchTerm = "";
|
let quickSearchTerm = "";
|
||||||
let googleStatus = { configured: false, connected: false, email: "" };
|
let googleStatus = { configured: false, connected: false, email: "" };
|
||||||
let showLoanedOnly = false;
|
let showLoanedOnly = false;
|
||||||
|
let scannerDetector = null;
|
||||||
|
let scannerStream = null;
|
||||||
|
let scannerRunning = false;
|
||||||
|
let scannerLoopId = null;
|
||||||
|
let scannerLastCodeValue = "";
|
||||||
|
let scannerLastCodeAt = 0;
|
||||||
|
|
||||||
toolsToggleBtn.addEventListener("click", () => {
|
toolsToggleBtn.addEventListener("click", () => {
|
||||||
gamesDrawer.classList.remove("open");
|
gamesDrawer.classList.remove("open");
|
||||||
@@ -106,6 +117,9 @@ document.addEventListener("keydown", (event) => {
|
|||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
toolsDrawer.classList.remove("open");
|
toolsDrawer.classList.remove("open");
|
||||||
gamesDrawer.classList.remove("open");
|
gamesDrawer.classList.remove("open");
|
||||||
|
if (scannerRunning) {
|
||||||
|
stopScanner("Camera arretee.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,6 +175,18 @@ loanedFilterBtn.addEventListener("click", () => {
|
|||||||
renderGames();
|
renderGames();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (scannerStartBtn) {
|
||||||
|
scannerStartBtn.addEventListener("click", async () => {
|
||||||
|
await startScanner();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scannerStopBtn) {
|
||||||
|
scannerStopBtn.addEventListener("click", () => {
|
||||||
|
stopScanner("Camera arretee.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
platformForm.addEventListener("submit", async (event) => {
|
platformForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -894,6 +920,131 @@ function resetEditMode() {
|
|||||||
cancelEditBtn.classList.add("hidden");
|
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) {
|
function normalizeText(value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
@@ -1059,11 +1210,20 @@ async function bootstrap() {
|
|||||||
await hydrateFromApi();
|
await hydrateFromApi();
|
||||||
normalizeState();
|
normalizeState();
|
||||||
render();
|
render();
|
||||||
|
if (scannerSupported()) {
|
||||||
|
updateScannerStatus("Camera inactive. Appuie sur Demarrer scan.");
|
||||||
|
} else {
|
||||||
|
updateScannerStatus("Scan non supporte sur ce navigateur.");
|
||||||
|
}
|
||||||
handleGoogleCallbackResult();
|
handleGoogleCallbackResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
stopScanner();
|
||||||
|
});
|
||||||
|
|
||||||
function handleGoogleCallbackResult() {
|
function handleGoogleCallbackResult() {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const googleParam = url.searchParams.get("google");
|
const googleParam = url.searchParams.get("google");
|
||||||
|
|||||||
12
index.html
12
index.html
@@ -123,6 +123,18 @@
|
|||||||
<div class="games-actions-bar">
|
<div class="games-actions-bar">
|
||||||
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
|
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
|
||||||
</div>
|
</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">
|
<div class="search-zone">
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
45
styles.css
45
styles.css
@@ -276,6 +276,51 @@ h1 {
|
|||||||
margin-bottom: 0.8rem;
|
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 {
|
.quick-search-results {
|
||||||
margin-top: 0.65rem;
|
margin-top: 0.65rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user