Label Scanner Enhancements: LCSC barcode, create part, augmented scanning (#1194)

* added handling of LCSC barcode decoding and part loading on Label Scanner

* when a part is scanned and not found, the scanner did not redraw so scanning subsequent parts was not possible without reloading the browser page.  fixed the barcode scanner initialization and shutdown so it redraws properly after part not found

* added redirection to part page on successful scan of lcsc, digikey, and mouser barcodes.   added create part button if part does not exist in database

* added augmented mode to label scanner to use vendor labels for part lookup to see part storage location quickly

* shrink camera height on mobile so augmented information can been viewed onscreen

* handle momentarily bad reads from qrcode library

* removed augmented checkbox and combined functionality into info mode checkbox.  changed barcode scanner to use XHR callback for barcode decoding to avoid problems with form submission and camera caused by page reloaded when part not found.

* fix scanning of part-db barcodes to redirect to storage location or part lots.   made scan result messages conditional for parts or other non-part barcodes

* fix static analysis errors

* added unit tests for meeting code coverage report

* fix @MayNiklas reported bug:  when manually submitting the form (from a barcode scan or manual input) redirect to Create New part screen for the decoded information instead of showing 'Format Unknown' popup error message

* fix @d-buchmann bug:  clear 'scan-augmented-result' field upon rescan of new barcode

* fix @d-buchmann bug: after scanning in Info mode, if Info mode is turned off when scanning a part that did not exist, it now redirects user to create part page

* fix @d-buchmann bug: make barcode decode table 100% width of page

* fix bug with manual form submission where a part does not exist but decodes properly which causes the camera to not redraw on page reload due to unclean shutdown. this is an existing bug in the scanner interface.

steps to produce the issue:
- have camera active
- put in code in Input
- info mode ticked
- click submit button

on page reload the camera does not reactivate

* fixed translation messages

* Use symfony native functions to generate the routes for part creation

* Use native request functions for request param parsing

* Refactored LCSCBarcocdeScanResult to be an value object like the other Barcode results

* Added test for LCSCBarcodeScanResult

* Fixed exception when submitting form for info mode

* Made BarcodeSourceType a backed enum, so that it can be used in Request::getEnum()

* Moved database queries from BarcodeRedirector to PartRepository

* Fixed modeEnum parsing

* Fixed test errors

* Refactored BarcodeRedirector logic to be more universal

* Fixed BarcodeScanResultHandler test

* Refactored BarcodeScanResultHandler to be able to resolve arbitary entities from scans

* Moved barcode to info provider logic from Controller to BarcodeScanResultHandler service

* Improved augmentented info styling and allow to use it with normal form submit too

* Correctly handle nullable infoURL in ScanController

* Replaced the custom controller for fragment replacements with symfony streams

This does not require a complete new endpoint

* Removed data-lookup-url attribute from scan read box

* Removed unused translations

* Added basic info block when an storage location was found for an barcode

* Fixed phpstan issues

* Fixed tests

* Fixed part image for mobile view

* Added more tests for BarcodeScanResultHandler service

* Fixed tests

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
This commit is contained in:
swdee 2026-02-23 09:26:44 +13:00 committed by GitHub
parent 8ef9dd432f
commit c29605ef23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1370 additions and 344 deletions

View file

@ -20,6 +20,10 @@
import { Controller } from '@hotwired/stimulus';
import { Toast } from 'bootstrap';
/**
* The purpose of this controller, is to show all containers.
* They should already be added via turbo-streams, but have to be called for to show them.
*/
export default class extends Controller {
connect() {
//Move all toasts from the page into our toast container and show them
@ -33,4 +37,4 @@ export default class extends Controller {
const toast = new Toast(this.element);
toast.show();
}
}
}

View file

@ -21,17 +21,31 @@ import {Controller} from "@hotwired/stimulus";
//import * as ZXing from "@zxing/library";
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
import { generateCsrfToken, generateCsrfHeaders } from "../csrf_protection_controller";
/* stimulusFetch: 'lazy' */
export default class extends Controller {
//codeReader = null;
_scanner = null;
_submitting = false;
_lastDecodedText = "";
_onInfoChange = null;
connect() {
console.log('Init Scanner');
// Prevent double init if connect fires twice
if (this._scanner) return;
// clear last decoded barcode when state changes on info box
const info = document.getElementById("scan_dialog_info_mode");
if (info) {
this._onInfoChange = () => {
this._lastDecodedText = "";
};
info.addEventListener("change", this._onInfoChange);
}
const isMobile = window.matchMedia("(max-width: 768px)").matches;
//This function ensures, that the qrbox is 70% of the total viewport
let qrboxFunction = function(viewfinderWidth, viewfinderHeight) {
@ -45,29 +59,61 @@ export default class extends Controller {
}
//Try to get the number of cameras. If the number is 0, then the promise will fail, and we show the warning dialog
Html5Qrcode.getCameras().catch((devices) => {
document.getElementById('scanner-warning').classList.remove('d-none');
Html5Qrcode.getCameras().catch(() => {
document.getElementById("scanner-warning")?.classList.remove("d-none");
});
this._scanner = new Html5QrcodeScanner(this.element.id, {
fps: 10,
qrbox: qrboxFunction,
// Key change: shrink preview height on mobile
...(isMobile ? { aspectRatio: 1.0 } : {}),
experimentalFeatures: {
//This option improves reading quality on android chrome
useBarCodeDetectorIfSupported: true
}
useBarCodeDetectorIfSupported: true,
},
}, false);
this._scanner.render(this.onScanSuccess.bind(this));
}
disconnect() {
this._scanner.pause();
this._scanner.clear();
// If we already stopped/cleared before submit, nothing to do.
const scanner = this._scanner;
this._scanner = null;
this._lastDecodedText = "";
// Unbind info-mode change handler (always do this, even if scanner is null)
const info = document.getElementById("scan_dialog_info_mode");
if (info && this._onInfoChange) {
info.removeEventListener("change", this._onInfoChange);
}
this._onInfoChange = null;
if (!scanner) return;
try {
const p = scanner.clear?.();
if (p && typeof p.then === "function") p.catch(() => {});
} catch (_) {
// ignore
}
}
onScanSuccess(decodedText, decodedResult) {
//Put our decoded Text into the input box
onScanSuccess(decodedText) {
if (!decodedText) return;
const normalized = String(decodedText).trim();
if (!normalized) return;
// scan once per barcode
if (normalized === this._lastDecodedText) return;
// Mark as handled immediately (prevents spam even if callback fires repeatedly)
this._lastDecodedText = normalized;
document.getElementById('scan_dialog_input').value = decodedText;
//Submit form
document.getElementById('scan_dialog_form').requestSubmit();