[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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user