-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for Barcode reader #650
Changes from all commits
5b4370a
551cf80
c45bbeb
7920389
3f9cc7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,171 @@ | ||||||||||||||||||||||||||||
<template> | ||||||||||||||||||||||||||||
<input-wrapper v-bind="inputWrapperProps"> | ||||||||||||||||||||||||||||
<template #label> | ||||||||||||||||||||||||||||
<slot name="label" /> | ||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<div | ||||||||||||||||||||||||||||
v-if="isScanning" | ||||||||||||||||||||||||||||
class="relative w-full" | ||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||
<CameraUpload | ||||||||||||||||||||||||||||
:is-barcode-mode="true" | ||||||||||||||||||||||||||||
:decoders="decoders" | ||||||||||||||||||||||||||||
@stop-webcam="stopScanning" | ||||||||||||||||||||||||||||
@barcode-detected="handleBarcodeDetected" | ||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<div | ||||||||||||||||||||||||||||
v-else-if="scannedValue" | ||||||||||||||||||||||||||||
class="flex items-center justify-between border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 rounded-lg px-4 py-2" | ||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||
<div class="flex-1 break-all"> | ||||||||||||||||||||||||||||
{{ scannedValue }} | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
<button | ||||||||||||||||||||||||||||
v-if="!disabled" | ||||||||||||||||||||||||||||
type="button" | ||||||||||||||||||||||||||||
class="pt-1 text-gray-400 hover:text-gray-600" | ||||||||||||||||||||||||||||
@click="clearValue" | ||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||
<Icon | ||||||||||||||||||||||||||||
name="i-heroicons-x-mark-20-solid" | ||||||||||||||||||||||||||||
class="h-5 w-5" | ||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||
</button> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<div | ||||||||||||||||||||||||||||
v-else | ||||||||||||||||||||||||||||
:style="inputStyle" | ||||||||||||||||||||||||||||
class="flex flex-col w-full items-center justify-center transition-colors duration-40" | ||||||||||||||||||||||||||||
:class="[ | ||||||||||||||||||||||||||||
{'!cursor-not-allowed':disabled, 'cursor-pointer':!disabled}, | ||||||||||||||||||||||||||||
theme.fileInput.input, | ||||||||||||||||||||||||||||
theme.fileInput.borderRadius, | ||||||||||||||||||||||||||||
theme.fileInput.spacing.horizontal, | ||||||||||||||||||||||||||||
theme.fileInput.spacing.vertical, | ||||||||||||||||||||||||||||
theme.fileInput.fontSize, | ||||||||||||||||||||||||||||
theme.fileInput.minHeight, | ||||||||||||||||||||||||||||
{'border-red-500 border-2':hasError}, | ||||||||||||||||||||||||||||
'focus:outline-none focus:ring-2' | ||||||||||||||||||||||||||||
]" | ||||||||||||||||||||||||||||
tabindex="0" | ||||||||||||||||||||||||||||
role="button" | ||||||||||||||||||||||||||||
aria-label="Click to open a camera" | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance accessibility with dynamic aria-labels The aria-label is static and doesn't reflect the component's current state (scanning/scanned/empty). -aria-label="Click to open a camera"
+:aria-label="isScanning ? 'Camera is active for barcode scanning' : scannedValue ? 'Scanned barcode value: ' + scannedValue : 'Click to open camera for barcode scanning'"
|
||||||||||||||||||||||||||||
@click="startScanning" | ||||||||||||||||||||||||||||
@keydown.enter.prevent="startScanning" | ||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||
<div class="flex w-full items-center justify-center"> | ||||||||||||||||||||||||||||
<div class="text-center"> | ||||||||||||||||||||||||||||
<template v-if="!scannedValue && !isScanning"> | ||||||||||||||||||||||||||||
<div class="text-gray-500 w-full flex justify-center"> | ||||||||||||||||||||||||||||
<svg | ||||||||||||||||||||||||||||
xmlns="http://www.w3.org/2000/svg" | ||||||||||||||||||||||||||||
fill="none" | ||||||||||||||||||||||||||||
viewBox="0 0 24 24" | ||||||||||||||||||||||||||||
stroke-width="1.5" | ||||||||||||||||||||||||||||
stroke="currentColor" | ||||||||||||||||||||||||||||
class="w-5 h-5" | ||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||
<path | ||||||||||||||||||||||||||||
stroke-linecap="round" | ||||||||||||||||||||||||||||
stroke-linejoin="round" | ||||||||||||||||||||||||||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" | ||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||
</svg> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<p class="mt-2 text-sm text-gray-500 font-medium select-none"> | ||||||||||||||||||||||||||||
{{ $t('forms.barcodeInput.clickToOpenCamera') }} | ||||||||||||||||||||||||||||
</p> | ||||||||||||||||||||||||||||
<div class="w-full items-center justify-center mt-2 hidden sm:flex"> | ||||||||||||||||||||||||||||
<UButton | ||||||||||||||||||||||||||||
icon="i-heroicons-camera" | ||||||||||||||||||||||||||||
color="white" | ||||||||||||||||||||||||||||
class="px-2" | ||||||||||||||||||||||||||||
@click.stop="startScanning" | ||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<template #error> | ||||||||||||||||||||||||||||
<slot name="error" /> | ||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||
</input-wrapper> | ||||||||||||||||||||||||||||
</template> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<script> | ||||||||||||||||||||||||||||
import { inputProps, useFormInput } from './useFormInput.js' | ||||||||||||||||||||||||||||
import InputWrapper from './components/InputWrapper.vue' | ||||||||||||||||||||||||||||
import CameraUpload from './components/CameraUpload.vue' | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
export default { | ||||||||||||||||||||||||||||
name: 'BarcodeInput', | ||||||||||||||||||||||||||||
components: { InputWrapper, CameraUpload }, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
props: { | ||||||||||||||||||||||||||||
...inputProps, | ||||||||||||||||||||||||||||
decoders: { | ||||||||||||||||||||||||||||
type: Array, | ||||||||||||||||||||||||||||
default: () => [] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
Comment on lines
+113
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add validation and documentation for decoders prop The decoders prop lacks validation for supported Quagga decoder types and documentation for API users. decoders: {
type: Array,
default: () => [],
+ validator: (value) => {
+ const supportedDecoders = ['code_128_reader', 'ean_reader', 'ean_8_reader', 'code_39_reader', 'upc_reader'];
+ return value.every(decoder => supportedDecoders.includes(decoder));
+ },
+ description: 'Array of Quagga decoder types to enable. Supported values: code_128_reader, ean_reader, ean_8_reader, code_39_reader, upc_reader'
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
setup(props, context) { | ||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||
...useFormInput(props, context), | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
data: () => ({ | ||||||||||||||||||||||||||||
isScanning: false, | ||||||||||||||||||||||||||||
scannedValue: null | ||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
watch: { | ||||||||||||||||||||||||||||
scannedValue: { | ||||||||||||||||||||||||||||
handler(value) { | ||||||||||||||||||||||||||||
this.compVal = value | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
immediate: true | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
beforeUnmount() { | ||||||||||||||||||||||||||||
this.stopScanning() | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
methods: { | ||||||||||||||||||||||||||||
startScanning() { | ||||||||||||||||||||||||||||
if (this.disabled) return | ||||||||||||||||||||||||||||
this.isScanning = true | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
Comment on lines
+144
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for camera initialization The startScanning method should handle potential camera access errors and provide user feedback. startScanning() {
if (this.disabled) return
+ try {
this.isScanning = true
+ this.$emit('scan-start')
+ } catch (error) {
+ this.handleScanError(error)
+ }
} Add these methods: methods: {
handleScanError(error) {
this.isScanning = false;
this.$emit('scan-error', error);
// Provide user feedback through UI
console.error('Camera initialization failed:', error);
}
} |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
stopScanning() { | ||||||||||||||||||||||||||||
this.isScanning = false | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
handleBarcodeDetected(code) { | ||||||||||||||||||||||||||||
this.scannedValue = code | ||||||||||||||||||||||||||||
this.stopScanning() | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
clearValue() { | ||||||||||||||||||||||||||||
this.scannedValue = null | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
</script> | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
<style scoped> | ||||||||||||||||||||||||||||
video { | ||||||||||||||||||||||||||||
/* Ensure the video displays properly */ | ||||||||||||||||||||||||||||
max-width: 100%; | ||||||||||||||||||||||||||||
height: auto; | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
</style> |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -21,6 +21,7 @@ | |||||||||||||||||||||||
class="p-2 px-4 flex items-center justify-center text-xs space-x-2" | ||||||||||||||||||||||||
> | ||||||||||||||||||||||||
<span | ||||||||||||||||||||||||
v-if="!isBarcodeMode" | ||||||||||||||||||||||||
class="cursor-pointer rounded-full w-14 h-14 border-2 grid place-content-center" | ||||||||||||||||||||||||
@click="processCapturedImage" | ||||||||||||||||||||||||
> | ||||||||||||||||||||||||
|
@@ -98,9 +99,10 @@ | |||||||||||||||||||||||
<script> | ||||||||||||||||||||||||
import Webcam from "webcam-easy" | ||||||||||||||||||||||||
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js" | ||||||||||||||||||||||||
import Quagga from 'quagga' | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export default { | ||||||||||||||||||||||||
name: "FileInput", | ||||||||||||||||||||||||
name: "CameraUpload", | ||||||||||||||||||||||||
props: { | ||||||||||||||||||||||||
theme: { | ||||||||||||||||||||||||
type: Object, default: () => { | ||||||||||||||||||||||||
|
@@ -111,13 +113,22 @@ export default { | |||||||||||||||||||||||
return CachedDefaultTheme.getInstance() | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
isBarcodeMode: { | ||||||||||||||||||||||||
type: Boolean, | ||||||||||||||||||||||||
default: false | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
decoders: { | ||||||||||||||||||||||||
type: Array, | ||||||||||||||||||||||||
default: () => [] | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
emits: ['stopWebcam', 'uploadImage'], | ||||||||||||||||||||||||
emits: ['stopWebcam', 'uploadImage', 'barcodeDetected'], | ||||||||||||||||||||||||
data: () => ({ | ||||||||||||||||||||||||
webcam: null, | ||||||||||||||||||||||||
isCapturing: false, | ||||||||||||||||||||||||
capturedImage: null, | ||||||||||||||||||||||||
cameraPermissionStatus: "loading", | ||||||||||||||||||||||||
quaggaInitialized: false | ||||||||||||||||||||||||
}), | ||||||||||||||||||||||||
computed: { | ||||||||||||||||||||||||
videoDisplay() { | ||||||||||||||||||||||||
|
@@ -142,6 +153,9 @@ export default { | |||||||||||||||||||||||
.start() | ||||||||||||||||||||||||
.then(() => { | ||||||||||||||||||||||||
this.cameraPermissionStatus = "allowed" | ||||||||||||||||||||||||
if (this.isBarcodeMode) { | ||||||||||||||||||||||||
this.initQuagga() | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+156
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for barcode initialization While camera initialization errors are handled, there's no error handling if the camera succeeds but barcode initialization fails. This could leave users waiting indefinitely. Add error handling: if (this.isBarcodeMode) {
- this.initQuagga()
+ this.initQuagga().catch(err => {
+ console.error('Failed to initialize barcode scanner:', err);
+ this.cameraPermissionStatus = 'unknown';
+ this.cancelCamera();
+ });
}
|
||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
.catch((err) => { | ||||||||||||||||||||||||
console.error(err) | ||||||||||||||||||||||||
|
@@ -152,9 +166,46 @@ export default { | |||||||||||||||||||||||
this.cameraPermissionStatus = "unknown" | ||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
initQuagga() { | ||||||||||||||||||||||||
if (!this.quaggaInitialized) { | ||||||||||||||||||||||||
Quagga.init({ | ||||||||||||||||||||||||
inputStream: { | ||||||||||||||||||||||||
name: "Live", | ||||||||||||||||||||||||
type: "LiveStream", | ||||||||||||||||||||||||
target: document.getElementById("webcam"), | ||||||||||||||||||||||||
constraints: { | ||||||||||||||||||||||||
facingMode: "environment" | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
decoder: { | ||||||||||||||||||||||||
readers: this.decoders || [] | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
locate: true | ||||||||||||||||||||||||
}, (err) => { | ||||||||||||||||||||||||
if (err) { | ||||||||||||||||||||||||
console.error('Quagga initialization failed:', err) | ||||||||||||||||||||||||
return | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+186
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Provide user feedback on Quagga initialization failure In the Apply this diff to handle initialization errors: if (err) {
console.error('Quagga initialization failed:', err)
+ this.cameraPermissionStatus = 'error'
+ this.isCapturing = false
+ this.$emit('stopWebcam')
return
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
this.quaggaInitialized = true | ||||||||||||||||||||||||
Quagga.start() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Quagga.onDetected((result) => { | ||||||||||||||||||||||||
if (result.codeResult) { | ||||||||||||||||||||||||
this.$emit('barcodeDetected', result.codeResult.code) | ||||||||||||||||||||||||
this.cancelCamera() | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
Comment on lines
+193
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add cleanup for Quagga event listeners The Add cleanup in the if (this.quaggaInitialized) {
+ Quagga.offDetected();
Quagga.stop();
this.quaggaInitialized = false;
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
cancelCamera() { | ||||||||||||||||||||||||
this.isCapturing = false | ||||||||||||||||||||||||
this.capturedImage = null | ||||||||||||||||||||||||
if (this.quaggaInitialized) { | ||||||||||||||||||||||||
Quagga.stop() | ||||||||||||||||||||||||
this.quaggaInitialized = false | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
this.webcam.stop() | ||||||||||||||||||||||||
this.$emit("stopWebcam") | ||||||||||||||||||||||||
}, | ||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add XSS protection for scanned values
The scanned value is directly rendered in the template without sanitization, which could be a security risk if malicious QR codes are scanned.
Add this method to the component: