Tested and working, added ability to use vite
This commit is contained in:
Generated
+14
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
||||
"svelte": "^5.56.4",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "^4.1.10"
|
||||
@@ -953,6 +954,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-basic-ssl": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz",
|
||||
"integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:host": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
||||
"svelte": "^5.56.4",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "^4.1.10"
|
||||
|
||||
+13
-2
@@ -107,37 +107,48 @@
|
||||
}
|
||||
|
||||
async function handleBarcode(barcode) {
|
||||
console.log('[App] handleBarcode:', barcode)
|
||||
resetScanFlow()
|
||||
scanBarcode = barcode
|
||||
scanLoading = true
|
||||
|
||||
// Step 1: Check local DB by barcode
|
||||
try {
|
||||
console.log('[App] searching local DB for barcode:', barcode)
|
||||
const results = await api.searchFoodsByBarcode(barcode)
|
||||
console.log('[App] local search results:', results?.length ?? 0)
|
||||
if (results && results.length > 0) {
|
||||
localFood = results[0]
|
||||
scanLogQuantity = defaultQuantity(localFood)
|
||||
scanLoading = false
|
||||
scanPhase = 'localFound'
|
||||
console.log('[App] local food found:', localFood.name)
|
||||
return
|
||||
}
|
||||
console.log('[App] not found locally, trying OpenFoodFacts…')
|
||||
} catch (e) {
|
||||
console.warn('[App] local search failed:', e)
|
||||
// Local lookup failed — try OFF anyway
|
||||
}
|
||||
|
||||
// Step 2: Not found locally → try OFF
|
||||
try {
|
||||
console.log('[App] fetching OFF product:', barcode)
|
||||
offFood = await api.offProduct(barcode)
|
||||
scanLoading = false
|
||||
scanPhase = 'offFound'
|
||||
console.log('[App] OFF product found:', offFood?.product_name ?? offFood?.name)
|
||||
} catch (e) {
|
||||
console.warn('[App] OFF lookup failed:', e.message)
|
||||
// OFF miss (404) or network error
|
||||
scanLoading = false
|
||||
if (e.message?.includes('404') || e.message?.includes('not found')) {
|
||||
scanPhase = 'notFound'
|
||||
console.log('[App] OFF product not found (404)')
|
||||
} else {
|
||||
scanError = e.message
|
||||
scanPhase = 'error'
|
||||
console.error('[App] OFF lookup error:', e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,8 +583,8 @@
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
.macro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.form-actions {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
let stopHandle = null
|
||||
|
||||
function handleDetected(barcode) {
|
||||
console.log('[BarcodeScanner] 🎯 barcode detected:', barcode)
|
||||
// Stop scanning once we have a barcode
|
||||
if (stopHandle) {
|
||||
stopHandle.stop()
|
||||
@@ -28,6 +29,7 @@
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
console.error('[BarcodeScanner] camera error:', err)
|
||||
loading = false
|
||||
if (isPermissionDeniedError(err)) {
|
||||
cameraUnavailable = true
|
||||
@@ -45,6 +47,7 @@
|
||||
e.preventDefault()
|
||||
const code = manualBarcode.trim()
|
||||
if (!code) return
|
||||
console.log('[BarcodeScanner] manual barcode submitted:', code)
|
||||
handleDetected(code)
|
||||
}
|
||||
|
||||
|
||||
@@ -538,8 +538,8 @@
|
||||
}
|
||||
|
||||
.macro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
+45
-11
@@ -8,8 +8,13 @@
|
||||
*
|
||||
* NOTE: getUserMedia requires a secure context — HTTPS via the Caddy
|
||||
* reverse proxy must be in place before phone testing (spec §5).
|
||||
*
|
||||
* DEBUGGING: Open browser DevTools (F12) → Console to see scanner logs.
|
||||
* Set localStorage.debugScanner = 'true' for verbose per-frame logging.
|
||||
*/
|
||||
|
||||
const DEBUG = typeof globalThis !== 'undefined' && globalThis.localStorage?.getItem('debugScanner') === 'true'
|
||||
|
||||
// zxing-wasm is the fallback decoder; imported lazily so Chromium users
|
||||
// on the native path never pay the WASM download cost.
|
||||
// The module exposes readBarcodesFromImageData for ImageData input.
|
||||
@@ -82,57 +87,80 @@ export function startScanner(videoEl, { onDetect, onError }) {
|
||||
// Extract the current video frame into our canvas
|
||||
const vw = videoEl.videoWidth
|
||||
const vh = videoEl.videoHeight
|
||||
if (vw === 0 || vh === 0) return // not playing yet
|
||||
if (vw === 0 || vh === 0) {
|
||||
if (DEBUG) console.warn('[scanner] video dimensions are 0 — not playing yet?')
|
||||
return
|
||||
}
|
||||
|
||||
canvas.width = vw
|
||||
canvas.height = vh
|
||||
ctx.drawImage(videoEl, 0, 0, vw, vh)
|
||||
|
||||
if (DEBUG) console.log('[scanner] frame extracted', vw, '×', vh)
|
||||
|
||||
let barcode = null
|
||||
|
||||
if (detector) {
|
||||
// Native BarcodeDetector path
|
||||
try {
|
||||
const detections = await detector.detect(canvas)
|
||||
if (DEBUG) console.log('[scanner] BarcodeDetector returned', detections.length, 'results')
|
||||
if (detections.length > 0 && !stopped) {
|
||||
barcode = detections[0].rawValue
|
||||
if (DEBUG) console.log('[scanner] 🎯 detected via BarcodeDetector:', barcode)
|
||||
}
|
||||
} catch {
|
||||
// Native detector can throw on some frames; ignore and try next
|
||||
} catch (e) {
|
||||
console.warn('[scanner] BarcodeDetector error on frame:', e)
|
||||
}
|
||||
} else {
|
||||
// zxing-wasm fallback — load once
|
||||
if (!zxingReader) {
|
||||
const mod = await import('zxing-wasm')
|
||||
// zxing-wasm v3 exports readBarcodesFromImageData
|
||||
zxingReader = mod.readBarcodesFromImageData
|
||||
try {
|
||||
if (DEBUG) console.log('[scanner] loading zxing-wasm…')
|
||||
const mod = await import('zxing-wasm')
|
||||
// zxing-wasm v3 exports readBarcodesFromImageData
|
||||
zxingReader = mod.readBarcodesFromImageData
|
||||
if (DEBUG) console.log('[scanner] zxing-wasm loaded')
|
||||
} catch (e) {
|
||||
console.error('[scanner] failed to load zxing-wasm:', e)
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const imageData = ctx.getImageData(0, 0, vw, vh)
|
||||
const results = await zxingReader(imageData, {
|
||||
formats: BARCODE_FORMATS,
|
||||
})
|
||||
if (DEBUG) console.log('[scanner] zxing returned', results.length, 'results')
|
||||
if (results.length > 0 && !stopped) {
|
||||
barcode = results[0].text
|
||||
if (DEBUG) console.log('[scanner] 🎯 detected via zxing:', barcode)
|
||||
}
|
||||
} catch {
|
||||
// zxing decode errors on non-barcode frames — ignore
|
||||
} catch (e) {
|
||||
console.warn('[scanner] zxing decode error on frame:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (barcode && !stopped) {
|
||||
onDetect(barcode)
|
||||
}
|
||||
} catch {
|
||||
// Frame extraction can fail if video isn't ready yet — ignore
|
||||
} catch (e) {
|
||||
console.warn('[scanner] frame extraction error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start camera ──────────────────────────────────────────────────────
|
||||
;(async () => {
|
||||
try {
|
||||
// Request a minimum resolution for better barcode detection.
|
||||
// Desktop webcams default to very low res (e.g. 320×240) which makes
|
||||
// fine barcode lines hard to read. 640×480 is a good baseline.
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment' },
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { min: 640, ideal: 1280 },
|
||||
height: { min: 480, ideal: 720 },
|
||||
},
|
||||
audio: false,
|
||||
})
|
||||
if (stopped) {
|
||||
@@ -143,9 +171,15 @@ export function startScanner(videoEl, { onDetect, onError }) {
|
||||
videoEl.srcObject = stream
|
||||
await videoEl.play()
|
||||
|
||||
if (DEBUG) {
|
||||
console.log('[scanner] camera started, dimensions:', videoEl.videoWidth, '×', videoEl.videoHeight)
|
||||
console.log('[scanner] decoder:', detector ? 'native BarcodeDetector' : 'zxing-wasm')
|
||||
}
|
||||
|
||||
// Start decode loop at ~4 fps
|
||||
timer = setInterval(decodeFrame, DECODE_INTERVAL_MS)
|
||||
} catch (err) {
|
||||
console.error('[scanner] getUserMedia failed:', err)
|
||||
if (!stopped) {
|
||||
onError(err instanceof Error ? err : new Error(String(err)))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import basicSsl from '@vitejs/plugin-basic-ssl'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
plugins: [svelte(), basicSsl()],
|
||||
server: {
|
||||
// Dev proxy so the frontend can call /api without CORS in production-style setups
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user