[MOV] web: move Barcode scanner to community

Move all file related to Barcode scanner (JS/XML/CSS)

Note: also the CSS class is renamed as it's not related to mobile
exclusively

Part-of: odoo/odoo#100082
This commit is contained in:
Romeo Fragomeli
2022-09-15 11:05:22 +02:00
parent 795891484a
commit 71a3048ec1
4 changed files with 320 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<metadata id="metadata27">image/svg+xml</metadata>
<g class="layer">
<title>Layer 1</title>
<g id="g20">
<g id="g12">
<rect height="186" id="rect2" width="44" x="61" y="57"/>
<rect height="186" id="rect4" width="44" x="236.5" y="57"/>
<rect height="186" id="rect6" width="44" x="295" y="57"/>
<rect height="186" id="rect8" width="24" x="129.5" y="57"/>
<rect height="186" id="rect10" width="14" x="193" y="57"/>
</g>
<g id="g18">
<polygon id="polygon14" points="59.5,269.5 25.5,269.5 25.5,30.5 59.5,30.5 59.5,19.5 14.5,19.5 14.5,280.5 59.5,280.5 "/>
<polygon id="polygon16" points="339.4999694824219,19.5 339.4999694824219,30.5 374.4999694824219,30.5 374.4999694824219,269.5 339.4999694824219,269.5 339.4999694824219,280.5 385.4999694824219,280.5 385.4999694824219,19.5 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,278 @@
/** @odoo-module **/
/* global BarcodeDetector */
import Dialog from "web.OwlDialog";
import { delay } from "web.concurrency";
import { loadJS, templates } from "@web/core/assets";
const { App, Component, EventBus, onMounted, onWillStart, onWillUnmount, useRef } = owl;
import { _t } from "web.core";
const bus = new EventBus();
const busOk = "BarcodeDialog-Ok";
const busError = "BarcodeDialog-Error";
/**
* Check for HTMLVideoElement readiness.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
*/
const HAVE_NOTHING = 0;
const HAVE_METADATA = 1;
function isVideoElementReady(video) {
return ![HAVE_NOTHING, HAVE_METADATA].includes(video.readyState);
}
/**
* Builder for BarcodeDetector-like polyfill class using ZXing library.
*
* @param {ZXing} ZXing Zxing library
* @returns {class} ZxingBarcodeDetector class
*/
function buildZXingBarcodeDetector(ZXing) {
const ZXingFormats = new Map([
["aztec", ZXing.BarcodeFormat.AZTEC],
["code_39", ZXing.BarcodeFormat.CODE_39],
["code_128", ZXing.BarcodeFormat.CODE_128],
["data_matrix", ZXing.BarcodeFormat.DATA_MATRIX],
["ean_8", ZXing.BarcodeFormat.EAN_8],
["ean_13", ZXing.BarcodeFormat.EAN_13],
["itf", ZXing.BarcodeFormat.ITF],
["pdf417", ZXing.BarcodeFormat.PDF_417],
["qr_code", ZXing.BarcodeFormat.QR_CODE],
["upc_a", ZXing.BarcodeFormat.UPC_A],
["upc_e", ZXing.BarcodeFormat.UPC_E],
]);
const allSupportedFormats = Array.from(ZXingFormats.keys());
/**
* ZXingBarcodeDetector class
*
* BarcodeDetector-like polyfill class using ZXing library.
* API follows the Shape Detection Web API (specifically Barcode Detection).
*/
class ZXingBarcodeDetector {
/**
* @param {object} opts
* @param {Array} opts.formats list of codes' formats to detect
*/
constructor(opts = {}) {
const formats = opts.formats || allSupportedFormats;
const hints = new Map([
[
ZXing.DecodeHintType.POSSIBLE_FORMATS,
formats.map((format) => ZXingFormats.get(format)),
],
// Enable Scanning at 90 degrees rotation
// https://github.com/zxing-js/library/issues/291
[ZXing.DecodeHintType.TRY_HARDER, true],
]);
this.reader = new ZXing.MultiFormatReader();
this.reader.setHints(hints);
}
/**
* Detect codes in image.
*
* @param {HTMLVideoElement} video source video element
* @returns {Promise<Array>} array of detected codes
*/
async detect(video) {
if (!(video instanceof HTMLVideoElement)) {
throw new DOMException(
"imageDataFrom() requires an HTMLVideoElement",
"InvalidArgumentError"
);
}
if (!isVideoElementReady(video)) {
throw new DOMException("HTMLVideoElement is not ready", "InvalidStateError");
}
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas);
const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource));
try {
const result = this.reader.decode(binaryBitmap);
const format = Array.from(ZXingFormats).find(
([k, val]) => val === result.getBarcodeFormat()
);
const rawValue = result.getText();
return [
{
format,
rawValue,
},
];
} catch (err) {
if (err.name === "NotFoundException") {
return [];
}
throw err;
}
}
}
/**
* Supported codes formats
*
* @static
* @returns {Array}
*/
ZXingBarcodeDetector.getSupportedFormats = async () => allSupportedFormats;
return ZXingBarcodeDetector;
}
class BarcodeDialog extends Component {
/**
* @override
*/
setup() {
this.videoPreviewRef = useRef("videoPreview");
this.interval = null;
this.stream = null;
this.detector = null;
onWillStart(async () => {
let DetectorClass;
// Use Barcode Detection API if available.
// As support is still bleeding edge (mainly Chrome on Android),
// also provides a fallback using ZXing library.
if ("BarcodeDetector" in window) {
DetectorClass = BarcodeDetector;
} else {
await loadJS("/web/static/lib/zxing-library/zxing-library.js");
DetectorClass = buildZXingBarcodeDetector(window.ZXing);
}
const formats = await DetectorClass.getSupportedFormats();
this.detector = new DetectorClass({ formats });
});
onMounted(async () => {
const constraints = {
video: { facingMode: this.props.facingMode },
audio: false,
};
try {
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (err) {
const errors = {
NotFoundError: _t("No device can be found."),
NotAllowedError: _t("Odoo needs your authorization first."),
};
const errorMessage =
_t("Could not start scanning. ") + (errors[err.name] || err.message);
this.onError(new Error(errorMessage));
return;
}
this.videoPreviewRef.el.srcObject = this.stream;
await this.isVideoReady();
this.interval = setInterval(this.detectCode.bind(this), 100);
});
onWillUnmount(() => {
clearInterval(this.interval);
this.interval = null;
if (this.stream) {
this.stream.getTracks().forEach((track) => track.stop());
this.stream = null;
}
});
}
/**
* Check for camera preview element readiness
*
* @returns {Promise} resolves when the video element is ready
*/
async isVideoReady() {
// FIXME: even if it shouldn't happened, a timeout could be useful here.
return new Promise(async (resolve) => {
while (!isVideoElementReady(this.videoPreviewRef.el)) {
await delay(10);
}
resolve();
});
}
/**
* Detection success handler
*
* @param {string} result found code
*/
onResult(result) {
this.props.onClose();
bus.trigger(busOk, result);
}
/**
* Detection error handler
*
* @param {Error} error
*/
onError(error) {
this.props.onClose();
bus.trigger(busError, { error });
}
/**
* Attempt to detect codes in the current camera preview's frame
*/
async detectCode() {
try {
const codes = await this.detector.detect(this.videoPreviewRef.el);
if (codes.length === 0) {
return;
}
this.onResult(codes[0].rawValue);
} catch (err) {
this.onError(err);
}
}
}
Object.assign(BarcodeDialog, {
components: {
Dialog,
},
template: "web.BarcodeDialog",
});
/**
* Check for BarcodeScanner support
* @returns {boolean}
*/
export function isBarcodeScannerSupported() {
return navigator.mediaDevices && navigator.mediaDevices.getUserMedia;
}
/**
* Opens the BarcodeScanning dialog and begins code detection using the device's camera.
*
* @returns {Promise<string>} resolves when a {qr,bar}code has been detected
*/
export async function scanBarcode(facingMode = "environment") {
const promise = new Promise((resolve, reject) => {
bus.on(busOk, null, resolve);
bus.on(busError, null, reject);
});
const appForBarcodeDialog = new App(BarcodeDialog, {
env: owl.Component.env,
dev: owl.Component.env.isDebug(),
templates,
translatableAttributes: ["data-tooltip"],
translateFn: _t,
props: {
onClose: () => appForBarcodeDialog.destroy(),
facingMode: facingMode,
},
});
await appForBarcodeDialog.mount(document.body);
return promise;
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.BarcodeDialog" owl="1">
<Dialog title="'Barcode Scanner'" onClosed="props.onClose" fullscreen="true">
<div class="p-0 d-flex justify-content-center flex-column">
<video t-ref="videoPreview" autoplay="true" playsinline="true" class="" style="max-width: 100%; max-height: 100%;"/>
</div>
<t t-set-slot="buttons">
<button class="btn btn-secondary" t-on-click="props.onClose">Cancel</button>
</t>
</Dialog>
</t>
</templates>
@@ -69,3 +69,12 @@ $oi-sizes: (
.oi-pulse {
animation: fa-spin 1s infinite steps(8);
}
.o_barcode {
-webkit-mask: url('/web/static/img/barcode.svg') center/contain no-repeat;
mask: url('/web/static/img/barcode.svg') center/contain no-repeat;
background-color: $o-brand-primary;
width: 26px;
margin-left: 10px;
border: 0;
}