Support for Barcode reader (#650)

* Add Barcode Input Component and Integrate Quagga for Scanning

- Introduced a new BarcodeInput component for scanning barcodes using the Quagga library.
- Updated package.json and package-lock.json to include Quagga as a dependency.
- Enhanced form themes to accommodate the new BarcodeInput component.
- Added localization support for barcode scanning actions in English.
- Updated blocks_types.json to register the new barcode input type.

These changes improve the application's functionality by allowing users to scan barcodes directly within forms, enhancing user experience and data input efficiency.

* Update Barcode scanner UI

* Barcode decoder as user selection

* improve barcode
This commit is contained in:
Chirag Chhatrala
2025-01-03 20:37:58 +05:30
committed by GitHub
parent 09c4417731
commit 1285dc18d3
29 changed files with 839 additions and 15 deletions

View File

@@ -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"
@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: () => []
}
},
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
},
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>

View File

@@ -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()
}
})
.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
}
this.quaggaInitialized = true
Quagga.start()
Quagga.onDetected((result) => {
if (result.codeResult) {
this.$emit('barcodeDetected', result.codeResult.code)
this.cancelCamera()
}
})
})
}
},
cancelCamera() {
this.isCapturing = false
this.capturedImage = null
if (this.quaggaInitialized) {
Quagga.stop()
this.quaggaInitialized = false
}
this.webcam.stop()
this.$emit("stopWebcam")
},

View File

@@ -35,7 +35,8 @@
<div
class="flex items-center"
:class="[
theme.SelectInput.minHeight
theme.SelectInput.minHeight,
'ltr:pr-8 rtl:pl-8'
]"
>
<transition
@@ -76,9 +77,12 @@
</div>
</transition>
</div>
<span class="absolute inset-y-0 ltr:right-0 rtl:left-0 rtl:!right-auto flex items-center ltr:pr-2 rtl:pl-2 rtl:!pr-0 pointer-events-none">
<div
class="absolute inset-y-0 ltr:right-6 rtl:left-6 w-10 pointer-events-none bg-gradient-to-r from-transparent to-white dark:to-notion-dark-light"
/>
<span class="absolute inset-y-0 ltr:right-0 rtl:left-0 rtl:!right-auto flex items-center ltr:pr-2 rtl:pl-2 rtl:!pr-0 pointer-events-none bg-white">
<Icon
name="heroicons:chevron-up-down-16-solid"
name="heroicons:chevron-up-down-16-solid"
class="h-5 w-5 text-gray-500"
/>
</span>

View File

@@ -223,7 +223,8 @@ export default {
url: 'TextInput',
email: 'TextInput',
phone_number: 'TextInput',
matrix: 'MatrixInput'
matrix: 'MatrixInput',
barcode: 'BarcodeInput'
}[field.type]
},
isPublicFormPage() {
@@ -338,6 +339,10 @@ export default {
inputProperties.columns = field.columns
}
if (field.type === 'barcode') {
inputProperties.decoders = field.decoders
}
if (['select','multi_select'].includes(field.type) && !this.isFieldRequired) {
inputProperties.clearable = true
}

View File

@@ -77,6 +77,27 @@
/>
</div>
<!-- Barcode Reader -->
<div
v-if="field.type === 'barcode'"
class="px-4"
>
<EditorSectionHeader
icon="i-material-symbols-barcode-scanner-rounded"
title="Barcode Reader"
/>
<select-input
name="decoders"
class="mt-4"
:form="field"
:options="barcodeDecodersOptions"
label="Decoders"
:searchable="true"
:multiple="true"
help="Select the decoders you want to use"
/>
</div>
<div
v-if="field.type === 'rating'"
class="px-4"
@@ -611,7 +632,15 @@ export default {
editorToolbarCustom: [
['bold', 'italic', 'underline', 'link']
],
allCountries: countryCodes
allCountries: countryCodes,
barcodeDecodersOptions: [
{ name: 'EAN-13 (European Article Number)', value: 'ean_reader' },
{ name: 'EAN-8 (European Article Number)', value: 'ean_8_reader' },
{ name: 'UPC-A (Universal Product Code)', value: 'upc_reader' },
{ name: 'UPC-E (Universal Product Code)', value: 'upc_e_reader' },
{ name: 'Code 128', value: 'code_128_reader' },
{ name: 'Code 39', value: 'code_39_reader' },
]
}
},
@@ -828,6 +857,9 @@ export default {
selection_data:{
'Row 1': null
}
},
barcode: {
decoders: ['ean_reader', 'upc_reader']
}
}
if (this.field.type in defaultFieldValues) {