Initial barcode scanner webapp
Camera-based barcode scanner using ZXing for decoding. Requests wide video (256x2048), rotates to landscape for decoding, applies contrast boost and tries both HybridBinarizer and GlobalHistogramBinarizer. Includes camera selector, manual/auto focus controls, and focus slider. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
vendor/
|
||||
dist/
|
||||
25
Makefile
Normal file
25
Makefile
Normal file
@@ -0,0 +1,25 @@
|
||||
DEST ?= dist
|
||||
|
||||
SRC = index.html app.mjs
|
||||
|
||||
.PHONY: all build deploy clean
|
||||
|
||||
all: build
|
||||
|
||||
node_modules/.package-lock.json: package.json
|
||||
npm install
|
||||
@touch $@
|
||||
|
||||
vendor/zxing.min.js: node_modules/.package-lock.json
|
||||
@mkdir -p vendor
|
||||
cp node_modules/@zxing/library/umd/index.min.js vendor/zxing.min.js
|
||||
|
||||
build: vendor/zxing.min.js
|
||||
|
||||
deploy: build
|
||||
@mkdir -p $(DEST)/vendor
|
||||
cp $(SRC) $(DEST)/
|
||||
cp vendor/zxing.min.js $(DEST)/vendor/
|
||||
|
||||
clean:
|
||||
rm -rf vendor dist
|
||||
364
app.mjs
Normal file
364
app.mjs
Normal file
@@ -0,0 +1,364 @@
|
||||
// app.mjs — fixed-line barcode scanner v1
|
||||
|
||||
const video = document.getElementById('video');
|
||||
const overlay = document.getElementById('overlay');
|
||||
const ctx = overlay.getContext('2d');
|
||||
const result_el = document.getElementById('result');
|
||||
const start_btn = document.getElementById('start-btn');
|
||||
const debug_el = document.getElementById('debug');
|
||||
const camera_select = document.getElementById('camera-select');
|
||||
const cam_info = document.getElementById('cam-info');
|
||||
const focus_row = document.getElementById('focus-row');
|
||||
const focus_slider = document.getElementById('focus-slider');
|
||||
const focus_val_el = document.getElementById('focus-val');
|
||||
const focus_auto = document.getElementById('focus-auto');
|
||||
// Canvases for ZXing decode pipeline
|
||||
const strip_canvas = document.createElement('canvas');
|
||||
const strip_ctx = strip_canvas.getContext('2d', { willReadFrequently: true });
|
||||
const band_canvas = document.createElement('canvas');
|
||||
const band_ctx = band_canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
let running = false;
|
||||
let last_decode_ms = 0;
|
||||
const DECODE_INTERVAL_MS = 80;
|
||||
let last_frame_ts = 0;
|
||||
let fps = 0;
|
||||
let last_format = '—';
|
||||
let decode_attempts = 0;
|
||||
let last_error = '—';
|
||||
let focus_info = '—';
|
||||
|
||||
// Center band of the frame fed to ZXing (fraction of video height)
|
||||
const STRIP_START = 0.35;
|
||||
const STRIP_END = 0.65;
|
||||
|
||||
let flash_until_ms = 0;
|
||||
|
||||
|
||||
// --- Camera ---
|
||||
|
||||
let active_track = null;
|
||||
|
||||
async function start_camera(device_id = null) {
|
||||
if (active_track) {
|
||||
active_track.stop();
|
||||
}
|
||||
try {
|
||||
const video_constraints = device_id
|
||||
? { deviceId: { exact: device_id }, width: { ideal: 256 }, height: { ideal: 2048 } }
|
||||
: { facingMode: { exact: 'environment' }, width: { ideal: 256 }, height: { ideal: 2048 } };
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: video_constraints,
|
||||
audio: false,
|
||||
});
|
||||
video.srcObject = stream;
|
||||
await new Promise(res => { video.onloadedmetadata = res; });
|
||||
await video.play();
|
||||
|
||||
const track = stream.getVideoTracks()[0];
|
||||
active_track = track;
|
||||
const caps = track.getCapabilities();
|
||||
await log_focus_caps(caps);
|
||||
|
||||
const has_continuous = (caps.focusMode ?? []).includes('continuous');
|
||||
const has_distance = !!caps.focusDistance;
|
||||
|
||||
if (has_continuous || has_distance) {
|
||||
focus_row.style.display = 'flex';
|
||||
}
|
||||
|
||||
if (has_distance) {
|
||||
focus_slider.min = caps.focusDistance.min;
|
||||
focus_slider.max = caps.focusDistance.max;
|
||||
focus_slider.step = (caps.focusDistance.max - caps.focusDistance.min) / 100;
|
||||
focus_slider.value = 0.3;
|
||||
focus_slider.oninput = () => set_focus_distance(track, parseFloat(focus_slider.value));
|
||||
}
|
||||
|
||||
if (!has_continuous) {
|
||||
focus_auto.checked = false;
|
||||
focus_auto.disabled = true;
|
||||
}
|
||||
|
||||
focus_auto.onchange = () => apply_focus(track, has_continuous, has_distance);
|
||||
await apply_focus(track, has_continuous, has_distance);
|
||||
|
||||
// Tap to focus (replace listener on camera switch)
|
||||
overlay.onclick = e => tap_to_focus(e, track);
|
||||
|
||||
start_btn.style.display = 'none';
|
||||
running = true;
|
||||
request_frame();
|
||||
} catch (err) {
|
||||
show_error(`Camera error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Focus ---
|
||||
|
||||
async function apply_focus(track, has_continuous, has_distance) {
|
||||
const auto = focus_auto.checked;
|
||||
focus_slider.disabled = auto;
|
||||
try {
|
||||
if (auto && has_continuous) {
|
||||
await track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] });
|
||||
focus_info = 'continuous';
|
||||
} else if (has_distance) {
|
||||
await set_focus_distance(track, parseFloat(focus_slider.value));
|
||||
}
|
||||
} catch (err) {
|
||||
focus_info = `focus failed: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function set_focus_distance(track, dist) {
|
||||
focus_val_el.textContent = `${dist.toFixed(2)}m`;
|
||||
try {
|
||||
await track.applyConstraints({ advanced: [{ focusMode: 'manual', focusDistance: dist }] });
|
||||
focus_info = `manual ${dist.toFixed(2)}m`;
|
||||
} catch (err) {
|
||||
focus_info = `dist failed: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function tap_to_focus(e, track) {
|
||||
const rect = overlay.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = (e.clientY - rect.top) / rect.height;
|
||||
try {
|
||||
await track.applyConstraints({
|
||||
advanced: [{ pointsOfInterest: [{ x, y }] }],
|
||||
});
|
||||
focus_info = `poi ${x.toFixed(2)},${y.toFixed(2)}`;
|
||||
} catch (err) {
|
||||
focus_info = `poi failed: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
let focus_caps_info = '—';
|
||||
|
||||
async function log_focus_caps(caps) {
|
||||
const parts = [];
|
||||
if (caps.focusMode) { parts.push(`modes:${caps.focusMode.join(',')}`); }
|
||||
if (caps.focusDistance) { parts.push(`dist:${caps.focusDistance.min}-${caps.focusDistance.max}`); }
|
||||
focus_caps_info = parts.join(' ') || 'none';
|
||||
focus_info = focus_caps_info;
|
||||
}
|
||||
|
||||
|
||||
// --- Render loop ---
|
||||
|
||||
function request_frame() {
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
function frame(ts) {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dW = overlay.clientWidth;
|
||||
const dH = overlay.clientHeight;
|
||||
const pW = Math.round(dW * dpr);
|
||||
const pH = Math.round(dH * dpr);
|
||||
|
||||
if (overlay.width !== pW || overlay.height !== pH) {
|
||||
overlay.width = pW;
|
||||
overlay.height = pH;
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
fps = last_frame_ts ? Math.round(1000 / (ts - last_frame_ts)) : 0;
|
||||
last_frame_ts = ts;
|
||||
|
||||
ctx.clearRect(0, 0, dW, dH);
|
||||
|
||||
const glowing = ts < flash_until_ms;
|
||||
draw_scan_lines(dW, dH, glowing);
|
||||
|
||||
if (ts - last_decode_ms > DECODE_INTERVAL_MS) {
|
||||
last_decode_ms = ts;
|
||||
attempt_decode();
|
||||
}
|
||||
|
||||
const vW = video.videoWidth;
|
||||
const vH = video.videoHeight;
|
||||
const zxing_status = window.ZXing ? 'ok' : 'MISSING';
|
||||
debug_el.innerHTML = `vid:${vW}×${vH} overlay:${overlay.width}×${overlay.height} ${fps}fps<br>strip:${strip_canvas.width}×${strip_canvas.height} band:${band_canvas.width}×${band_canvas.height} dpr:${dpr}<br>caps:${focus_caps_info}<br>focus:${focus_info} attempts:${decode_attempts} zxing:${zxing_status}<br>err:${last_error} last:${last_format}`;
|
||||
|
||||
request_frame();
|
||||
}
|
||||
|
||||
|
||||
// --- Scan line drawing ---
|
||||
|
||||
function draw_scan_lines(dW, dH, glowing) {
|
||||
const h_margin = Math.round(dW * 0.04);
|
||||
const cy = Math.round(dH * 0.5);
|
||||
|
||||
const color_main = glowing ? '#00ff88' : 'rgba(255,80,80,0.9)';
|
||||
const color_band = glowing ? 'rgba(0,255,136,0.08)' : 'rgba(255,80,80,0.06)';
|
||||
|
||||
// Shaded band showing the decode strip
|
||||
ctx.fillStyle = color_band;
|
||||
ctx.fillRect(0, dH * STRIP_START, dW, dH * (STRIP_END - STRIP_START));
|
||||
|
||||
// Center scan line
|
||||
ctx.shadowColor = glowing ? '#00ff88' : 'transparent';
|
||||
ctx.shadowBlur = glowing ? 10 : 0;
|
||||
ctx.strokeStyle = color_main;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(h_margin, cy);
|
||||
ctx.lineTo(dW - h_margin, cy);
|
||||
ctx.stroke();
|
||||
|
||||
// End caps
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = glowing ? 'rgba(0,255,136,0.8)' : 'rgba(255,80,80,0.7)';
|
||||
ctx.lineWidth = 2;
|
||||
const cap = 10;
|
||||
for (const x of [h_margin, dW - h_margin]) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, cy - cap);
|
||||
ctx.lineTo(x, cy + cap);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- ZXing decode ---
|
||||
|
||||
let zxing_reader = null;
|
||||
|
||||
function get_reader() {
|
||||
if (!window.ZXing) {
|
||||
return null;
|
||||
}
|
||||
if (!zxing_reader) {
|
||||
zxing_reader = new ZXing.MultiFormatReader();
|
||||
}
|
||||
return zxing_reader;
|
||||
}
|
||||
|
||||
function attempt_decode() {
|
||||
const reader = get_reader();
|
||||
const vW = video.videoWidth;
|
||||
const vH = video.videoHeight;
|
||||
if (!reader || !vW || !vH) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Full frame into decoder — rotate if camera swapped dims to portrait
|
||||
const landscape = vW >= vH;
|
||||
const dst_w = landscape ? vW : vH;
|
||||
const dst_h = landscape ? vH : vW;
|
||||
if (strip_canvas.width !== dst_w || strip_canvas.height !== dst_h) {
|
||||
strip_canvas.width = dst_w;
|
||||
strip_canvas.height = dst_h;
|
||||
}
|
||||
if (landscape) {
|
||||
strip_ctx.drawImage(video, 0, 0, vW, vH);
|
||||
} else {
|
||||
strip_ctx.save();
|
||||
strip_ctx.translate(dst_w / 2, dst_h / 2);
|
||||
strip_ctx.rotate(-Math.PI / 2);
|
||||
strip_ctx.drawImage(video, -vW / 2, -vH / 2, vW, vH);
|
||||
strip_ctx.restore();
|
||||
}
|
||||
|
||||
// Crop to a thin band around the center row — only decode what's on the aim line
|
||||
const band_h = Math.max(32, Math.round(strip_canvas.height * 0.15));
|
||||
const band_y = Math.round((strip_canvas.height - band_h) / 2);
|
||||
if (band_canvas.width !== strip_canvas.width || band_canvas.height !== band_h) {
|
||||
band_canvas.width = strip_canvas.width;
|
||||
band_canvas.height = band_h;
|
||||
}
|
||||
band_ctx.filter = 'contrast(1.8) brightness(1.05)';
|
||||
band_ctx.drawImage(strip_canvas, 0, band_y, strip_canvas.width, band_h, 0, 0, strip_canvas.width, band_h);
|
||||
band_ctx.filter = 'none';
|
||||
|
||||
decode_attempts++;
|
||||
const source = new ZXing.HTMLCanvasElementLuminanceSource(band_canvas);
|
||||
const binarizers = [ZXing.HybridBinarizer, ZXing.GlobalHistogramBinarizer];
|
||||
let decoded = false;
|
||||
for (const Binarizer of binarizers) {
|
||||
try {
|
||||
const bitmap = new ZXing.BinaryBitmap(new Binarizer(source));
|
||||
const result = reader.decode(bitmap);
|
||||
on_success(result.getText(), result.getBarcodeFormat());
|
||||
decoded = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
last_error = e?.name ?? String(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_decoded_text = null;
|
||||
|
||||
function on_success(text, format) {
|
||||
flash_until_ms = performance.now() + 1200;
|
||||
last_format = String(format);
|
||||
result_el.textContent = `${text} [${format}]`;
|
||||
result_el.classList.add('found');
|
||||
result_el.classList.remove('error');
|
||||
if (text !== last_decoded_text) {
|
||||
navigator.vibrate?.(60);
|
||||
last_decoded_text = text;
|
||||
}
|
||||
}
|
||||
|
||||
function show_error(msg) {
|
||||
result_el.textContent = msg;
|
||||
result_el.classList.add('error');
|
||||
result_el.classList.remove('found');
|
||||
}
|
||||
|
||||
|
||||
// --- Init ---
|
||||
|
||||
async function init() {
|
||||
await start_camera();
|
||||
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const cameras = devices.filter(d => d.kind === 'videoinput');
|
||||
|
||||
const scored = cameras.map(d => {
|
||||
const label = d.label.toLowerCase();
|
||||
let score = 0;
|
||||
// Strongly prefer back camera index 0
|
||||
if (label.match(/\b0\b.*facing back|camera2 0/)) { score += 100; }
|
||||
else if (label.includes('facing back') || label.includes('back')) { score += 10; }
|
||||
// Penalise specialised lenses
|
||||
if (label.includes('tele')) { score -= 50; }
|
||||
if (label.includes('ultra')) { score -= 20; }
|
||||
return { d, score };
|
||||
});
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
|
||||
cam_info.textContent = scored.map(({ d }) => d.label || 'no-label').join(' | ');
|
||||
camera_select.innerHTML = scored.map(({ d }) =>
|
||||
`<option value="${d.deviceId}">${d.label || d.deviceId.slice(0, 12)}</option>`
|
||||
).join('');
|
||||
camera_select.style.display = 'block';
|
||||
camera_select.onchange = () => start_camera(camera_select.value);
|
||||
|
||||
const best_id = scored[0].d.deviceId;
|
||||
if (best_id !== active_track?.getSettings().deviceId) {
|
||||
camera_select.value = best_id;
|
||||
await start_camera(best_id);
|
||||
}
|
||||
}
|
||||
|
||||
start_btn.addEventListener('click', init);
|
||||
|
||||
if (
|
||||
location.protocol === 'https:' ||
|
||||
location.hostname === 'localhost' ||
|
||||
location.hostname === '127.0.0.1'
|
||||
) {
|
||||
init();
|
||||
}
|
||||
170
index.html
Normal file
170
index.html
Normal file
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Barcode Scanner</title>
|
||||
<style>
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent: #00e87a;
|
||||
--dim: #5a6475;
|
||||
--surface: #12161c;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#camera-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Transparent overlay — only draws scan lines */
|
||||
#overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#debug {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 8px;
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.6);
|
||||
text-shadow: 0 1px 2px #000;
|
||||
pointer-events: none;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Bottom result panel */
|
||||
#bottom-panel {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 18px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid #1e2530;
|
||||
}
|
||||
|
||||
#result {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: #181e28;
|
||||
border: 1px solid #1e2a38;
|
||||
font-size: 14px;
|
||||
color: var(--dim);
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#result.found {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: #0d1f15;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#result.error {
|
||||
color: #ff5555;
|
||||
border-color: #ff5555;
|
||||
}
|
||||
|
||||
#focus-row {
|
||||
display: none;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #dce4ef;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#focus-slider {
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
#focus-val {
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#start-btn {
|
||||
padding: 13px 30px;
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="camera-view">
|
||||
<video id="video" autoplay playsinline muted></video>
|
||||
<canvas id="overlay"></canvas>
|
||||
<div id="debug"></div>
|
||||
</div>
|
||||
<div id="bottom-panel">
|
||||
<div id="cam-info" style="width:100%;font-size:11px;color:#5a6475;word-break:break-all;"></div>
|
||||
<div id="result">Point camera at a barcode</div>
|
||||
<div id="focus-row">
|
||||
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;">
|
||||
<input id="focus-auto" type="checkbox" checked>
|
||||
Auto
|
||||
</label>
|
||||
<input id="focus-slider" type="range" min="0" max="1.62" step="0.01" value="0.3" disabled>
|
||||
<span id="focus-val">0.30m</span>
|
||||
</div>
|
||||
<select id="camera-select" style="display:none; width:100%; padding:8px; background:#181e28; color:#dce4ef; border:1px solid #1e2a38; border-radius:8px; font-family:inherit;"></select>
|
||||
<button id="start-btn">Start Camera</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="vendor/zxing.min.js"></script>
|
||||
<script type="module" src="app.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
46
package-lock.json
generated
Normal file
46
package-lock.json
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "phone-barcode",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "phone-barcode",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@zxing/library": "0.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/library": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.19.2.tgz",
|
||||
"integrity": "sha512-0aCQIbGXsDHcBuRYuZRZfJeLRYiPfdLxIlh5bY7k+k2bkxFwzla+75VyFi1rnjU5hqaZogdSH+Dw013YDsQOjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ts-custom-error": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@zxing/text-encoding": "~0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/text-encoding": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
|
||||
"license": "(Unlicense OR Apache-2.0)",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ts-custom-error": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
|
||||
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "phone-barcode",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@zxing/library": "0.19.2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user