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