Replace Scanbot SDK with native barcode scanner.

Use BarcodeDetector and ZXing for unlimited device ID scanning instead of the Scanbot trial SDK.
This commit is contained in:
lakshay
2026-06-29 13:46:46 -04:00
parent 266ddcdfcb
commit 705080a554

View File

@@ -8,6 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css" />
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<script src="https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js"></script>
<style>
:root{color-scheme: light dark;}
@@ -1202,10 +1203,6 @@
style="flex:1;min-width:200px;"
placeholder="e.g. 15-03-58-80"
title="Type Device ID and press Enter to add"/>
<!-- <button id="scan-btn"
style="padding:6px 10px;border-radius:10px;border:1px solid #ccc;cursor:pointer;">
<i class="fas fa-barcode"></i> Scan
</button> -->
<button id="btn-scan" class="check-btn"
style="padding:6px 10px;border-radius:10px;border:1px solid #ccc;cursor:pointer;">
&#128247; Scan ID
@@ -2632,9 +2629,7 @@
});
</script>
<script type="module">
import "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@8.0.0/bundle/ScanbotSDK.ui2.min.js";
<script>
// -------------------------------------------------------------------------
// Toast helpers (Toastify is global from the non-module script tag above)
// -------------------------------------------------------------------------
@@ -2668,12 +2663,6 @@
}).showToast();
}
// -------------------------------------------------------------------------
// Scanbot init
// -------------------------------------------------------------------------
const cdn = "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@8.0.0/bundle/bin/complete/";
const sdk = await ScanbotSDK.initialize({ enginePath: cdn, licenseKey: "" });
// -------------------------------------------------------------------------
// State
// -------------------------------------------------------------------------
@@ -2682,47 +2671,189 @@
let confirming = false;
// -------------------------------------------------------------------------
// Device ID parsing/validation
// Barcode scanner (BarcodeDetector + ZXing)
// -------------------------------------------------------------------------
function normalizeCandidate(text) {
if (!text) return null;
const scannerOverlay = document.getElementById("scanner-overlay");
const scannerVideo = document.getElementById("scanner-video");
const scannerCancel = document.getElementById("scanner-cancel");
let t = String(text).trim().toUpperCase();
let scanStream = null;
let scanTrack = null;
let scanning = false;
let scanRafId = null;
let lastDecodeTime = 0;
const DECODE_INTERVAL = 120;
// Convert common separators to '-'
t = t.replace(/[:\s_]+/g, "-");
const scanCanvas = document.createElement("canvas");
const scanCtx = scanCanvas.getContext("2d", { willReadFrequently: true });
// If 8 hex chars with no separators, convert to XX-XX-XX-XX
if (/^[0-9A-F]{8}$/.test(t)) {
t = t.match(/../g).join("-");
let scanEngine = null;
let nativeDetector = null;
let zxingReader = null;
let scanEngineReady = false;
function normalizeBarcode(raw) {
if (!raw) return null;
const digits = String(raw).replace(/\D/g, "");
if (digits.length !== 8) return null;
return digits.replace(/(\d\d)(\d\d)(\d\d)(\d\d)/, "$1-$2-$3-$4");
}
// If it looks like 4 groups of 2 hex, normalize
const parts = t.split("-").filter(Boolean);
if (parts.length === 4 && parts.every(p => /^[0-9A-F]{2}$/.test(p))) {
t = parts.join("-");
async function initScanEngine() {
if (scanEngineReady) return scanEngine !== null;
scanEngineReady = true;
if ("BarcodeDetector" in window) {
try {
const formats = await window.BarcodeDetector.getSupportedFormats();
if (formats.includes("code_128")) {
nativeDetector = new window.BarcodeDetector({ formats: ["code_128"] });
scanEngine = "native";
return true;
}
} catch (e) { /* fall through */ }
}
return t;
if (window.ZXing) {
const hints = new Map();
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, [ZXing.BarcodeFormat.CODE_128]);
hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
zxingReader = new ZXing.MultiFormatReader();
zxingReader.setHints(hints);
scanEngine = "zxing";
return true;
}
function isValidDeviceId(t) {
return /^[0-9A-F]{2}(-[0-9A-F]{2}){3}$/.test(t);
scanEngine = null;
return false;
}
function makeMiniBtn(label, kind, onClick) {
const btn = document.createElement("button");
btn.className = `btn mini ${kind}`;
btn.textContent = label;
btn.onclick = onClick;
return btn;
async function startScanCamera() {
try {
scanStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: { ideal: "environment" },
width: { ideal: 1280 },
height: { ideal: 720 },
focusMode: "continuous"
},
audio: false
});
} catch (err) {
handleScanCameraError(err);
return false;
}
function onAction(id, n) {
// Placeholder for later: API call, WebSocket emit, etc.
toastInfo(`Clicked Action ${n} for ${id}`);
scannerVideo.srcObject = scanStream;
await scannerVideo.play().catch(() => {});
scanTrack = scanStream.getVideoTracks()[0];
return true;
}
function stopScanCamera() {
if (scanRafId) cancelAnimationFrame(scanRafId);
scanRafId = null;
scanning = false;
if (scanStream) scanStream.getTracks().forEach(t => t.stop());
scanStream = null;
scanTrack = null;
scannerVideo.srcObject = null;
}
function handleScanCameraError(err) {
let msg;
if (err && (err.name === "NotAllowedError" || err.name === "SecurityError")) {
msg = "Camera access was blocked. Allow camera permission for this page, then try again. The page must be served over HTTPS.";
} else if (err && err.name === "NotFoundError") {
msg = "No camera found on this device.";
} else {
msg = "Could not start the camera. Serve this page over HTTPS or localhost and try again.";
}
toastErr(msg);
closeScannerOverlay();
}
function drawCropToCanvas() {
const vw = scannerVideo.videoWidth;
const vh = scannerVideo.videoHeight;
if (!vw || !vh) return false;
const cropW = Math.round(vw * 0.82);
const cropH = Math.round(cropW / 2.6);
const sx = Math.round((vw - cropW) / 2);
const sy = Math.round((vh - cropH) / 2);
if (scanCanvas.width !== cropW || scanCanvas.height !== cropH) {
scanCanvas.width = cropW;
scanCanvas.height = cropH;
}
scanCtx.drawImage(scannerVideo, sx, sy, cropW, cropH, 0, 0, cropW, cropH);
return true;
}
function handleDecodedBarcode(text) {
const id = normalizeBarcode(text);
if (!id) return;
if (accepted.has(id) || pendingQueue.includes(id)) return;
enqueueForConfirm(id);
toastOk(`Queued ${id}`);
if (navigator.vibrate) navigator.vibrate(40);
}
async function scanTick(now) {
if (!scanning) return;
scanRafId = requestAnimationFrame(scanTick);
if (now - lastDecodeTime < DECODE_INTERVAL) return;
lastDecodeTime = now;
if (!drawCropToCanvas()) return;
let text = null;
try {
if (scanEngine === "native") {
const codes = await nativeDetector.detect(scanCanvas);
if (codes && codes.length) text = codes[0].rawValue;
} else if (scanEngine === "zxing") {
const lum = new ZXing.HTMLCanvasElementLuminanceSource(scanCanvas);
const bitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(lum));
try {
const res = zxingReader.decode(bitmap);
text = res ? res.getText() : null;
} catch (e) { /* no barcode this frame */ }
finally { zxingReader.reset(); }
}
} catch (e) { /* keep scanning */ }
if (text) handleDecodedBarcode(text);
}
function closeScannerOverlay() {
stopScanCamera();
scannerOverlay.style.display = "none";
}
async function openScannerOverlay() {
const ok = await initScanEngine();
if (!ok) {
toastErr("Barcode scanner unavailable in this browser.");
return;
}
scannerOverlay.style.display = "flex";
const camOk = await startScanCamera();
if (!camOk) return;
scanning = true;
lastDecodeTime = 0;
scanRafId = requestAnimationFrame(scanTick);
}
scannerCancel.addEventListener("click", () => {
closeScannerOverlay();
if (pendingQueue.length) processQueue();
});
window.addEventListener("pagehide", stopScanCamera);
// -------------------------------------------------------------------------
// Confirm modal (queue-based)
// -------------------------------------------------------------------------
@@ -2796,55 +2927,8 @@
// -------------------------------------------------------------------------
// Buttons
// -------------------------------------------------------------------------
document.getElementById("btn-scan").onclick = async () => {
try {
const config = new ScanbotSDK.UI.Config.BarcodeScannerScreenConfiguration();
// Depending on Scanbot version/features, you may be able to restrict formats here.
// Example (pseudo): config.barcodeFormats = [ScanbotSDK.BarcodeFormat.CODE_128, ...];
const result = await ScanbotSDK.UI.createBarcodeScanner(config);
const items = result?.items ?? [];
if (!items.length) {
toastErr("No barcodes found.");
return;
}
let valid = 0;
let invalid = 0;
let duplicates = 0;
for (const item of items) {
const raw = item?.barcode?.text ?? "";
const norm = normalizeCandidate(raw);
if (!norm || !isValidDeviceId(norm)) {
invalid++;
continue;
}
if (accepted.has(norm) || pendingQueue.includes(norm)) {
duplicates++;
continue;
}
enqueueForConfirm(norm);
valid++;
}
if (valid === 0) {
toastErr(`No valid device IDs found. Invalid: ${invalid}, duplicates: ${duplicates}.`);
return;
}
toastOk(`Found ${valid} valid device ID(s). Confirming…`);
processQueue();
} catch (err) {
console.error(err);
toastErr("Scanner error. Check console for details.");
}
document.getElementById("btn-scan").onclick = () => {
openScannerOverlay();
};
</script>