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