Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QR code reading support #23

Merged
merged 1 commit into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions lippukala/static/lippukala/pos-qr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable max-classes-per-file */

/* globals BarcodeDetector */

class PosQRNative {
constructor() {
this.barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] });
}

async init() {
return Boolean(this.barcodeDetector);
}

async detectFromVideo(video) {
return this.barcodeDetector.detect(video);
}
}

class PosQR {
static hasBarcodeDetector() {
return typeof BarcodeDetector !== "undefined";
}

constructor({ addLogEntry, onFoundQRCode }) {
this.addLogEntry = addLogEntry;
this.onFoundQRCode = onFoundQRCode;

const video = document.createElement("video");
video.id = "posqr-video";
document.body.appendChild(video);

this.video = video;
this.detecting = false;
this.detector = null;
this.interval = null;
this.media = null;
}

async init() {
if (PosQR.hasBarcodeDetector()) {
this.addLogEntry("Käytetään sisäänrakennettua QR-koodinlukijaa");
this.detector = new PosQRNative();
} else {
this.addLogEntry("Ei QR-koodinlukijaa");
throw new Error("No QR code detector");
}
await this.detector.init();
this.addLogEntry("QR-koodinlukija valmis");
}

updateDOM() {
const started = this.isStarted();
document.body.classList.toggle("qr-started", started);
}

isStarted() {
return Boolean(this.media) && Boolean(this.interval);
}

isInitialized() {
return Boolean(this.detector);
}

async doDetectQR() {
const { video } = this;
try {
if (this.detecting) {
console.warn("Already detecting");
return;
}
this.detecting = true;
const t0 = performance.now();
for (const barcode of await this.detector.detectFromVideo(video)) {
this.onFoundQRCode(barcode);
}
const t1 = performance.now();
console.debug("QR detect time", Math.round(t1 - t0));
} finally {
this.detecting = false;
}
}

async start() {
if (!this.isInitialized()) await this.init();
await this.stop();
try {
this.media = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
frameRate: { ideal: 10 },
},
audio: false,
});
this.video.srcObject = this.media;
await this.video.play();
this.addLogEntry("Kamera käynnistetty");
this.interval = setInterval(() => this.doDetectQR(), 300);
} catch (err) {
this.addLogEntry(`QR-koodinlukijan käynnistäminen epäonnistui: ${err}`);
}
this.updateDOM();
}

async stop() {
if (this.media) {
for (const track of this.media.getTracks()) {
track.stop();
}
this.media = null;
}
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
this.updateDOM();
}
}

window.PosQR = PosQR;
20 changes: 20 additions & 0 deletions lippukala/static/lippukala/pos.css
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ body.code-used-here .statustext {
border-left: 1px solid #333;
}

#camera-btn.started {
background: linear-gradient(to bottom, #a3cb38, #009432);
}

input#code {
padding: 0.25rem;
border: none;
Expand Down Expand Up @@ -189,6 +193,22 @@ textarea#log:hover {
}
}

#posqr-video {
display: none;
}

#posqr-canvas {
display: none;
position: absolute;
left: 0;
bottom: 0;
max-width: 20vw;
}

#posqr-canvas.started {
display: block;
}

@media (max-width: 550px) {
body {
font-size: 20pt;
Expand Down
32 changes: 32 additions & 0 deletions lippukala/static/lippukala/pos.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* globals PosQR */

/**
* @typedef {Object} Code
* @property {boolean} used
Expand Down Expand Up @@ -38,6 +40,9 @@ let currentlyShownId = null;
/** The current year, as a string. */
const thisYearString = String(new Date().getFullYear());

/** @type {PosQR|null} */
let posQR = null;

/**
* @param {string} id ID or selector
* @returns {HTMLElement}
Expand Down Expand Up @@ -301,6 +306,29 @@ function debounce(fn, delay) {
};
}

async function handleCameraClick() {
if (!posQR) {
posQR = new PosQR({
addLogEntry,
onFoundQRCode: ({ rawValue }) => {
const text = rawValue.trim();
if (/^\d+$/.test(text)) {
$("#code").value = text;
search(true);
} else {
this.addLogEntry(`QR-koodi ei ole numero: ${text}`);
}
},
});
}
if (posQR.isStarted()) {
await posQR.stop();
} else {
await posQR.start();
}
$("#camera-btn").classList.toggle("started", posQR.isStarted());
}

window.init = async function init() {
showCode(null); // reset dom state
search(false); // reset dom state
Expand All @@ -312,6 +340,10 @@ window.init = async function init() {
$("#codeform").addEventListener("submit", debounce(formSubmit, 250), true);
$("#confirm-form").addEventListener("submit", onConfirmCode, true);
$("#confirm-dialog").addEventListener("close", cancelConfirm, true);
$("#camera-btn").addEventListener("click", () => handleCameraClick(), true);
if (!PosQR.hasBarcodeDetector()) {
$("#camera-btn").hidden = true;
}
$("#clear-btn").addEventListener(
"click",
() => {
Expand Down
2 changes: 2 additions & 0 deletions lippukala/templates/lippukala/pos.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
<title>POS</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="{% static 'lippukala/pos.css' %}" />
<script src="{% static 'lippukala/pos-qr.js' %}" id="qr-script"></script>
<script src="{% static 'lippukala/pos.js' %}"></script>
</head>
<body onload="init()">
<form id="codeform">
<input placeholder="koodi tähän" type="search" id="code" autofocus autocomplete="off" />
<button type="button" id="accept-btn">&#x2705;</button>
<button type="button" id="clear-btn">&#x274C;</button>
<button type="button" id="camera-btn">&#x1F3A5;</button>
</form>
<div id="status">&nbsp;</div>
<dialog id="confirm-dialog">
Expand Down