mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Use stimulus controller to implement barcode scanner.
This commit is contained in:
parent
1460894946
commit
cb683f611c
4 changed files with 89 additions and 324 deletions
86
assets/controllers/pages/barcode_scan_controller.js
Normal file
86
assets/controllers/pages/barcode_scan_controller.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
import * as ZXing from "@zxing/library";
|
||||||
|
|
||||||
|
/* stimulusFetch: 'lazy' */
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static targets = [ "source" ]
|
||||||
|
|
||||||
|
codeReader = null;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log('Init Scanner');
|
||||||
|
this.codeReader = new ZXing.BrowserMultiFormatReader();
|
||||||
|
this.initScanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
codeScannedHandler(result, err) {
|
||||||
|
if (result) {
|
||||||
|
//@ts-ignore
|
||||||
|
document.getElementById('scan_dialog_input').value = result.text;
|
||||||
|
//Submit form
|
||||||
|
//@ts-ignore
|
||||||
|
document.getElementById('scan_dialog_form').submit();
|
||||||
|
}
|
||||||
|
if (err && !(err instanceof ZXing.NotFoundException)) {
|
||||||
|
console.error(err);
|
||||||
|
//document.getElementById('result').textContent = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initScanner() {
|
||||||
|
let selectedDeviceId;
|
||||||
|
|
||||||
|
this.codeReader.listVideoInputDevices()
|
||||||
|
.then((videoInputDevices) => {
|
||||||
|
if (videoInputDevices.length >= 1) {
|
||||||
|
const sourceSelect = document.getElementById('sourceSelect');
|
||||||
|
|
||||||
|
|
||||||
|
videoInputDevices.forEach((element) => {
|
||||||
|
const sourceOption = document.createElement('option');
|
||||||
|
sourceOption.text = element.label;
|
||||||
|
sourceOption.value = element.deviceId;
|
||||||
|
sourceSelect.appendChild(sourceOption);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Try to retrieve last selected webcam...
|
||||||
|
let last_cam_id = localStorage.getItem('scanner_last_cam_id');
|
||||||
|
if (!!last_cam_id) {
|
||||||
|
//selectedDeviceId = localStorage.getItem('scanner_last_cam_id');
|
||||||
|
sourceSelect.value = last_cam_id;
|
||||||
|
} else {
|
||||||
|
selectedDeviceId = videoInputDevices[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSelect.onchange = () => {
|
||||||
|
//@ts-ignore
|
||||||
|
selectedDeviceId = sourceSelect.value;
|
||||||
|
localStorage.setItem('scanner_last_cam_id', selectedDeviceId);
|
||||||
|
changeHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('sourceSelectPanel').classList.remove('d-none');
|
||||||
|
document.getElementById('video').classList.remove('d-none');
|
||||||
|
document.getElementById('scanner-warning').classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let changeHandler = () => {
|
||||||
|
this.codeReader.reset();
|
||||||
|
this.codeReader.decodeFromVideoDevice(selectedDeviceId, 'video', (result, err) => this.codeScannedHandler(result, err));
|
||||||
|
console.log(`Started continous decode from camera with id ${selectedDeviceId}`)
|
||||||
|
};
|
||||||
|
|
||||||
|
//Register Change Src Button
|
||||||
|
//document.getElementById('changeSrcBtn').addEventListener('click', changeHandler);
|
||||||
|
|
||||||
|
//Try to start logging automatically.
|
||||||
|
changeHandler();
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,12 +14,13 @@ const RegisterEventHelper = class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.registerToasts();
|
this.registerToasts();
|
||||||
this.registerTooltips();
|
this.registerTooltips();
|
||||||
this.registerFileInput();
|
|
||||||
this.registerJumpToTopBtn();
|
this.registerJumpToTopBtn();
|
||||||
|
|
||||||
this.registerTriStateCheckboxes();
|
this.registerTriStateCheckboxes();
|
||||||
|
this.registerFileInput();
|
||||||
|
|
||||||
this.registerSpecialCharInput();
|
this.registerSpecialCharInput();
|
||||||
|
|
||||||
this.registerHoverPics();
|
this.registerHoverPics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,101 +31,6 @@ import * as ZXing from "@zxing/library";
|
||||||
*
|
*
|
||||||
***********************************/
|
***********************************/
|
||||||
|
|
||||||
|
|
||||||
//Register greek input in search fields.
|
|
||||||
$(document).on("ajaxUI:start ajaxUI:reload", function() {
|
|
||||||
//@ts-ignore
|
|
||||||
$("input[type=text], textarea, input[type=search]").unbind("keydown").keydown(function (event : KeyboardEvent) {
|
|
||||||
let greek = event.altKey;
|
|
||||||
|
|
||||||
let greek_char : string = "";
|
|
||||||
if (greek){
|
|
||||||
switch(event.key) {
|
|
||||||
case "w": //Omega
|
|
||||||
greek_char = '\u2126';
|
|
||||||
break;
|
|
||||||
case "u":
|
|
||||||
case "m": //Micro
|
|
||||||
greek_char = "\u00B5";
|
|
||||||
break;
|
|
||||||
case "p": //Phi
|
|
||||||
greek_char = "\u03C6";
|
|
||||||
break;
|
|
||||||
case "a": //Alpha
|
|
||||||
greek_char = "\u03B1";
|
|
||||||
break;
|
|
||||||
case "b": //Beta
|
|
||||||
greek_char = "\u03B2";
|
|
||||||
break;
|
|
||||||
case "c": //Gamma
|
|
||||||
greek_char = "\u03B3";
|
|
||||||
break;
|
|
||||||
case "d": //Delta
|
|
||||||
greek_char = "\u03B4";
|
|
||||||
break;
|
|
||||||
case "l": //Pound
|
|
||||||
greek_char = "\u00A3";
|
|
||||||
break;
|
|
||||||
case "y": //Yen
|
|
||||||
greek_char = "\u00A5";
|
|
||||||
break;
|
|
||||||
case "o": //Yen
|
|
||||||
greek_char = "\u00A4";
|
|
||||||
break;
|
|
||||||
case "1": //Sum symbol
|
|
||||||
greek_char = "\u2211";
|
|
||||||
break;
|
|
||||||
case "2": //Integral
|
|
||||||
greek_char = "\u222B";
|
|
||||||
break;
|
|
||||||
case "3": //Less-than or equal
|
|
||||||
greek_char = "\u2264";
|
|
||||||
break;
|
|
||||||
case "4": //Greater than or equal
|
|
||||||
greek_char = "\u2265";
|
|
||||||
break;
|
|
||||||
case "5": //PI
|
|
||||||
greek_char = "\u03c0";
|
|
||||||
break;
|
|
||||||
case "q": //Copyright
|
|
||||||
greek_char = "\u00A9";
|
|
||||||
break;
|
|
||||||
case "e": //Euro
|
|
||||||
greek_char = "\u20AC";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(greek_char=="") return;
|
|
||||||
|
|
||||||
let $txt = $(this);
|
|
||||||
//@ts-ignore
|
|
||||||
let caretPos = $txt[0].selectionStart;
|
|
||||||
let textAreaTxt = $txt.val().toString();
|
|
||||||
$txt.val(textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos) );
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
//@ts-ignore
|
|
||||||
this.greek_once = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
//Register bootstrap select picker
|
|
||||||
$(document).on("ajaxUI:reload ajaxUI:start", function () {
|
|
||||||
//@ts-ignore
|
|
||||||
$(".selectpicker").selectpicker({
|
|
||||||
dropdownAlignRight: 'auto',
|
|
||||||
container: '#content',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Use bootstrap tooltips for the most tooltips
|
|
||||||
$(document).on("ajaxUI:start ajaxUI:reload ajaxUI:dt_loaded", function () {
|
|
||||||
$(".tooltip").remove();
|
|
||||||
$('a[title], button[title], span[title], h6[title], h3[title], i.fas[title]')
|
|
||||||
//@ts-ignore
|
|
||||||
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add bootstrap treeview on divs with data-tree-data attribute
|
// Add bootstrap treeview on divs with data-tree-data attribute
|
||||||
$(document).on("ajaxUI:start ajaxUI:reload", function() {
|
$(document).on("ajaxUI:start ajaxUI:reload", function() {
|
||||||
$("[data-tree-data]").each(function(index, element) {
|
$("[data-tree-data]").each(function(index, element) {
|
||||||
|
@ -263,60 +168,6 @@ $(document).on("ajaxUI:reload", function () {
|
||||||
$(".file").fileinput();
|
$(".file").fileinput();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("ajaxUI:start ajaxUI:reload", function () {
|
|
||||||
$('input.tagsinput').each(function() {
|
|
||||||
|
|
||||||
//Use typeahead if an autocomplete url was specified.
|
|
||||||
if($(this).data('autocomplete')) {
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
var engine = new Bloodhound({
|
|
||||||
//@ts-ignore
|
|
||||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
|
|
||||||
//@ts-ignore
|
|
||||||
queryTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
|
|
||||||
remote: {
|
|
||||||
url: $(this).data('autocomplete'),
|
|
||||||
wildcard: 'QUERY'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
$(this).tagsinput({
|
|
||||||
typeaheadjs: {
|
|
||||||
name: 'tags',
|
|
||||||
source: engine.ttAdapter()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
} else { //Init tagsinput without typeahead
|
|
||||||
//@ts-ignore
|
|
||||||
$(this).tagsinput();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the button, to jump to the top of the page.
|
|
||||||
*/
|
|
||||||
$(document).on("ajaxUI:start", function registerJumpToTop() {
|
|
||||||
$(window).scroll(function () {
|
|
||||||
if ($(this).scrollTop() > 50) {
|
|
||||||
$('#back-to-top').fadeIn();
|
|
||||||
} else {
|
|
||||||
$('#back-to-top').fadeOut();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// scroll body to 0px on click
|
|
||||||
$('#back-to-top').click(function () {
|
|
||||||
$('#back-to-top').tooltip('hide');
|
|
||||||
$('body,html').animate({
|
|
||||||
scrollTop: 0
|
|
||||||
}, 800);
|
|
||||||
return false;
|
|
||||||
}).tooltip();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This listener keeps track of which tab is currently selected (using hash and localstorage) and will try to open
|
* This listener keeps track of which tab is currently selected (using hash and localstorage) and will try to open
|
||||||
|
@ -374,48 +225,6 @@ $(document).on("ajaxUI:reload ajaxUI:start ajaxUI:dt_loaded", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
* Register the button which is used to
|
|
||||||
*/
|
|
||||||
$(document).on("ajaxUI:start", function() {
|
|
||||||
let $sidebar = $("#fixed-sidebar");
|
|
||||||
let $container = $("#main");
|
|
||||||
let $toggler = $('#sidebar-toggle-button');
|
|
||||||
|
|
||||||
function sidebarHide() {
|
|
||||||
$sidebar.hide();
|
|
||||||
$container.removeClass('col-md-9 col-lg-10 offset-md-3 offset-lg-2');
|
|
||||||
$container.addClass('col-12');
|
|
||||||
$toggler.html('<i class="fas fa-angle-right"></i>');
|
|
||||||
$toggler.data('hidden', true);
|
|
||||||
localStorage.setItem('sidebarHidden', 'true');
|
|
||||||
}
|
|
||||||
function sidebarShow() {
|
|
||||||
let $sidebar = $("#fixed-sidebar");
|
|
||||||
$sidebar.show();
|
|
||||||
let $container = $("#main");
|
|
||||||
$container.removeClass('col-12');
|
|
||||||
$container.addClass('col-md-9 col-lg-10 offset-md-3 offset-lg-2');
|
|
||||||
$toggler.html('<i class="fas fa-angle-left"></i>');
|
|
||||||
$toggler.data('hidden', false);
|
|
||||||
localStorage.setItem('sidebarHidden', 'false');
|
|
||||||
}
|
|
||||||
|
|
||||||
//Make the state persistent over reloads
|
|
||||||
if(localStorage.getItem('sidebarHidden') === 'true') {
|
|
||||||
sidebarHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Register handler
|
|
||||||
$toggler.click(function() {
|
|
||||||
if($(this).data('hidden')) {
|
|
||||||
sidebarShow();
|
|
||||||
} else {
|
|
||||||
sidebarHide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
//Register typeaheads
|
//Register typeaheads
|
||||||
$(document).on("ajaxUI:reload ajaxUI:start attachment:create", function () {
|
$(document).on("ajaxUI:reload ajaxUI:start attachment:create", function () {
|
||||||
|
@ -459,51 +268,6 @@ $(document).on("ajaxUI:reload ajaxUI:start attachment:create", function () {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("ajaxUI:start", function () {
|
|
||||||
function decodeHTML(html) {
|
|
||||||
var txt = document.createElement('textarea');
|
|
||||||
txt.innerHTML = html;
|
|
||||||
return txt.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMarkdown() {
|
|
||||||
$('.markdown').each(function() {
|
|
||||||
let unescaped = marked(decodeHTML( $(this).data('markdown')));
|
|
||||||
//@ts-ignore
|
|
||||||
let escaped = DOMPurify.sanitize(unescaped);
|
|
||||||
$(this).html(escaped);
|
|
||||||
//Remove markdown from DOM
|
|
||||||
$(this).removeAttr('data-markdown');
|
|
||||||
|
|
||||||
//Make all links external
|
|
||||||
$('a', this).addClass('link-external').attr('target', '_blank').attr('rel', 'noopener');
|
|
||||||
//Bootstrapify objects
|
|
||||||
$('table', this).addClass('table table-hover table-striped table-bordered');
|
|
||||||
});
|
|
||||||
|
|
||||||
//Latex rendering have to be done after markdown parsing
|
|
||||||
$('.latex').each(function(index, element) {
|
|
||||||
//@ts-ignore
|
|
||||||
window.renderMathInElement(element, {
|
|
||||||
delimiters: [
|
|
||||||
{left: "$$", right: "$$", display: true},
|
|
||||||
{left: "$", right: "$", display: false},
|
|
||||||
{left: "\\(", right: "\\)", display: false},
|
|
||||||
{left: "\\[", right: "\\]", display: true}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Configure markdown
|
|
||||||
marked.setOptions({
|
|
||||||
gfm: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
parseMarkdown();
|
|
||||||
$(document).on("ajaxUI:reload", parseMarkdown);
|
|
||||||
$(document).on("ajaxUI:dt_loaded", parseMarkdown);
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on("ajaxUI:start ajaxUI:reload attachment:create", function() {
|
$(document).on("ajaxUI:start ajaxUI:reload attachment:create", function() {
|
||||||
let updater = function() {
|
let updater = function() {
|
||||||
|
@ -571,92 +335,6 @@ $(document).on("ajaxUI:reload", function() {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
//Reuse codereader between multiple requests
|
|
||||||
const codeReader = new ZXing.BrowserMultiFormatReader();
|
|
||||||
|
|
||||||
//Init barcode scanner
|
|
||||||
$(document).on("ajaxUI:start ajaxUI:reload", function() {
|
|
||||||
|
|
||||||
//Skip if we are not on scanner page...
|
|
||||||
if (!document.getElementById('scan_dialog_form')) {
|
|
||||||
|
|
||||||
codeReader.reset();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let selectedDeviceId;
|
|
||||||
|
|
||||||
|
|
||||||
//Save it for later, so we can reset it
|
|
||||||
console.log('ZXing code reader initialized');
|
|
||||||
codeReader.listVideoInputDevices()
|
|
||||||
.then((videoInputDevices) => {
|
|
||||||
if (videoInputDevices.length >= 1) {
|
|
||||||
const sourceSelect = document.getElementById('sourceSelect');
|
|
||||||
|
|
||||||
|
|
||||||
videoInputDevices.forEach((element) => {
|
|
||||||
const sourceOption = document.createElement('option');
|
|
||||||
sourceOption.text = element.label;
|
|
||||||
sourceOption.value = element.deviceId;
|
|
||||||
sourceSelect.appendChild(sourceOption);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Try to retrieve last selected webcam...
|
|
||||||
let last_cam_id = localStorage.getItem('scanner_last_cam_id');
|
|
||||||
if (!!last_cam_id) {
|
|
||||||
//selectedDeviceId = localStorage.getItem('scanner_last_cam_id');
|
|
||||||
$(sourceSelect).val(last_cam_id);
|
|
||||||
} else {
|
|
||||||
selectedDeviceId = videoInputDevices[0].deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSelect.onchange = () => {
|
|
||||||
//@ts-ignore
|
|
||||||
selectedDeviceId = sourceSelect.value;
|
|
||||||
localStorage.setItem('scanner_last_cam_id', selectedDeviceId);
|
|
||||||
changeHandler();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('sourceSelectPanel').classList.remove('d-none');
|
|
||||||
document.getElementById('video').classList.remove('d-none');
|
|
||||||
document.getElementById('scanner-warning').classList.add('d-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let changeHandler = () => {
|
|
||||||
codeReader.reset();
|
|
||||||
codeReader.decodeFromVideoDevice(selectedDeviceId, 'video', (result, err) => {
|
|
||||||
if (result) {
|
|
||||||
//@ts-ignore
|
|
||||||
document.getElementById('scan_dialog_input').value = result.text;
|
|
||||||
//Submit form
|
|
||||||
//@ts-ignore
|
|
||||||
document.getElementById('scan_dialog_form').submit();
|
|
||||||
}
|
|
||||||
if (err && !(err instanceof ZXing.NotFoundException)) {
|
|
||||||
console.error(err);
|
|
||||||
//document.getElementById('result').textContent = err
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`Started continous decode from camera with id ${selectedDeviceId}`)
|
|
||||||
};
|
|
||||||
|
|
||||||
//Register Change Src Button
|
|
||||||
//document.getElementById('changeSrcBtn').addEventListener('click', changeHandler);
|
|
||||||
|
|
||||||
//Try to start logging automatically.
|
|
||||||
changeHandler();
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//Need for proper body padding, with every navbar height
|
//Need for proper body padding, with every navbar height
|
||||||
$(window).resize(function () {
|
$(window).resize(function () {
|
||||||
let height : number = $('#navbar').height() + 10;
|
let height : number = $('#navbar').height() + 10;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="offset-sm-3 col-sm-9">
|
<div class="offset-sm-3 col-sm-9">
|
||||||
<video id="video" width="100%" height="auto" class="scanner-video img-thumbnail d-none">
|
<video id="video" width="100%" height="auto" class="scanner-video img-thumbnail d-none" {{ stimulus_controller('pages/barcode_scan') }}>
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue