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"> <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" /> <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/toastify-js"></script>
<script src="https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js"></script>
<style> <style>
:root{color-scheme: light dark;} :root{color-scheme: light dark;}
@@ -1202,10 +1203,6 @@
style="flex:1;min-width:200px;" style="flex:1;min-width:200px;"
placeholder="e.g. 15-03-58-80" placeholder="e.g. 15-03-58-80"
title="Type Device ID and press Enter to add"/> 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" <button id="btn-scan" class="check-btn"
style="padding:6px 10px;border-radius:10px;border:1px solid #ccc;cursor:pointer;"> style="padding:6px 10px;border-radius:10px;border:1px solid #ccc;cursor:pointer;">
&#128247; Scan ID &#128247; Scan ID
@@ -2632,9 +2629,7 @@
}); });
</script> </script>
<script type="module"> <script>
import "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@8.0.0/bundle/ScanbotSDK.ui2.min.js";
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Toast helpers (Toastify is global from the non-module script tag above) // Toast helpers (Toastify is global from the non-module script tag above)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -2668,12 +2663,6 @@
}).showToast(); }).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 // State
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -2682,47 +2671,189 @@
let confirming = false; let confirming = false;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Device ID parsing/validation // Barcode scanner (BarcodeDetector + ZXing)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function normalizeCandidate(text) { const scannerOverlay = document.getElementById("scanner-overlay");
if (!text) return null; 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 '-' const scanCanvas = document.createElement("canvas");
t = t.replace(/[:\s_]+/g, "-"); const scanCtx = scanCanvas.getContext("2d", { willReadFrequently: true });
// If 8 hex chars with no separators, convert to XX-XX-XX-XX let scanEngine = null;
if (/^[0-9A-F]{8}$/.test(t)) { let nativeDetector = null;
t = t.match(/../g).join("-"); 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");
}
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 */ }
} }
// If it looks like 4 groups of 2 hex, normalize if (window.ZXing) {
const parts = t.split("-").filter(Boolean); const hints = new Map();
if (parts.length === 4 && parts.every(p => /^[0-9A-F]{2}$/.test(p))) { hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, [ZXing.BarcodeFormat.CODE_128]);
t = parts.join("-"); hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
zxingReader = new ZXing.MultiFormatReader();
zxingReader.setHints(hints);
scanEngine = "zxing";
return true;
} }
return t; scanEngine = null;
return false;
} }
function isValidDeviceId(t) { async function startScanCamera() {
return /^[0-9A-F]{2}(-[0-9A-F]{2}){3}$/.test(t); 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;
}
scannerVideo.srcObject = scanStream;
await scannerVideo.play().catch(() => {});
scanTrack = scanStream.getVideoTracks()[0];
return true;
} }
function makeMiniBtn(label, kind, onClick) { function stopScanCamera() {
const btn = document.createElement("button"); if (scanRafId) cancelAnimationFrame(scanRafId);
btn.className = `btn mini ${kind}`; scanRafId = null;
btn.textContent = label; scanning = false;
btn.onclick = onClick; if (scanStream) scanStream.getTracks().forEach(t => t.stop());
return btn; scanStream = null;
scanTrack = null;
scannerVideo.srcObject = null;
} }
function onAction(id, n) { function handleScanCameraError(err) {
// Placeholder for later: API call, WebSocket emit, etc. let msg;
toastInfo(`Clicked Action ${n} for ${id}`); 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) // Confirm modal (queue-based)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -2796,55 +2927,8 @@
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Buttons // Buttons
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
document.getElementById("btn-scan").onclick = async () => { document.getElementById("btn-scan").onclick = () => {
try { openScannerOverlay();
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.");
}
}; };
</script> </script>