@archbase/components 3.0.25 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buttons/ArchbaseSpeedDial.d.ts +59 -0
- package/dist/buttons/index.d.ts +2 -0
- package/dist/charts/ArchbaseArcGauge.d.ts +55 -0
- package/dist/charts/ArchbaseGantt.d.ts +70 -0
- package/dist/charts/ArchbaseLinearGauge.d.ts +55 -0
- package/dist/charts/ArchbaseResourceTimeline.d.ts +61 -0
- package/dist/charts/ArchbaseSparkline.d.ts +45 -0
- package/dist/charts/index.d.ts +10 -0
- package/dist/containers/ArchbaseExpansionPanel.d.ts +60 -0
- package/dist/containers/ArchbaseFloatingPanel.d.ts +91 -0
- package/dist/containers/index.d.ts +4 -0
- package/dist/datagrid/export/ArchbaseDataGridExcelExport.d.ts +67 -0
- package/dist/datagrid/export/index.d.ts +2 -0
- package/dist/datagrid/index.d.ts +1 -0
- package/dist/display/ArchbaseAnimationWrapper.d.ts +73 -0
- package/dist/display/ArchbaseBarcodeGenerator.d.ts +50 -0
- package/dist/display/ArchbaseCarousel.d.ts +100 -0
- package/dist/display/ArchbaseCodeViewer.d.ts +63 -0
- package/dist/display/ArchbaseDiffViewer.d.ts +41 -0
- package/dist/display/ArchbaseJsonTree.d.ts +34 -0
- package/dist/display/ArchbaseLightbox.d.ts +89 -0
- package/dist/display/ArchbaseMarquee.d.ts +46 -0
- package/dist/display/ArchbasePhotoAlbum.d.ts +58 -0
- package/dist/display/ArchbaseQRCode.d.ts +44 -0
- package/dist/display/ArchbaseTooltipRich.d.ts +82 -0
- package/dist/display/ArchbaseVirtualList.d.ts +116 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/editors/ArchbaseAIPromptInput.d.ts +69 -0
- package/dist/editors/ArchbaseAsyncMultiSelect.d.ts +1 -1
- package/dist/editors/ArchbaseAsyncSelect.d.ts +1 -1
- package/dist/editors/ArchbaseBarcodeScanner.d.ts +63 -0
- package/dist/editors/ArchbaseCepInput.d.ts +26 -0
- package/dist/editors/ArchbaseColorGradientPicker.d.ts +80 -0
- package/dist/editors/ArchbaseCpfCnpjInput.d.ts +21 -0
- package/dist/editors/ArchbaseCurrencyInput.d.ts +25 -0
- package/dist/editors/ArchbaseDualListbox.d.ts +61 -0
- package/dist/editors/ArchbaseInPlaceEditor.d.ts +67 -0
- package/dist/editors/ArchbaseLightGrid.d.ts +56 -0
- package/dist/editors/ArchbaseMentionInput.d.ts +75 -0
- package/dist/editors/ArchbaseMultiEmail.d.ts +53 -0
- package/dist/editors/ArchbaseMultiViewCalendar.d.ts +47 -0
- package/dist/editors/ArchbaseNumberStepper.d.ts +66 -0
- package/dist/editors/ArchbaseOTPInput.d.ts +27 -0
- package/dist/editors/ArchbasePhoneInput.d.ts +52 -0
- package/dist/editors/ArchbaseRangeSlider.d.ts +22 -0
- package/dist/editors/ArchbaseSignaturePad.d.ts +60 -0
- package/dist/editors/ArchbaseSpeechToTextInput.d.ts +106 -0
- package/dist/editors/ArchbaseSpreadsheet.d.ts +56 -0
- package/dist/editors/ArchbaseTagInput.d.ts +55 -0
- package/dist/editors/index.d.ts +40 -0
- package/dist/feedback/ArchbaseChunkProgressBar.d.ts +35 -0
- package/dist/feedback/ArchbaseRipple.d.ts +43 -0
- package/dist/feedback/ArchbaseSkeleton.d.ts +115 -0
- package/dist/feedback/index.d.ts +6 -0
- package/dist/filters/ArchbaseColumnSelector.d.ts +38 -0
- package/dist/filters/index.d.ts +2 -0
- package/dist/forms/ArchbaseFieldArray.d.ts +80 -0
- package/dist/forms/ArchbaseFormWizard.d.ts +73 -0
- package/dist/forms/ArchbaseStepper.d.ts +51 -0
- package/dist/forms/index.d.ts +8 -0
- package/dist/forms/useArchbaseFormWizard.d.ts +74 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useArchbaseConfirm.d.ts +59 -0
- package/dist/image/editor/index.d.ts +43 -4
- package/dist/image/index.d.ts +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +22510 -13820
- package/dist/list/ArchbaseTreeList.d.ts +76 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/navigation/ArchbaseActionSheet.d.ts +70 -0
- package/dist/navigation/ArchbaseAppBar.d.ts +63 -0
- package/dist/navigation/ArchbaseBottomNavigation.d.ts +61 -0
- package/dist/navigation/ArchbaseContextMenu.d.ts +76 -0
- package/dist/navigation/ArchbaseKeyboardShortcuts.d.ts +85 -0
- package/dist/navigation/ArchbaseSpotlight.d.ts +69 -0
- package/dist/navigation/index.d.ts +12 -0
- package/dist/notification/ArchbaseNotificationCenter.d.ts +56 -0
- package/dist/notification/index.d.ts +2 -0
- package/dist/onboarding/ArchbaseOnboardingTour.d.ts +27 -76
- package/dist/viewers/ArchbaseDocViewer.d.ts +40 -0
- package/dist/viewers/ArchbasePdfBuilder.d.ts +53 -0
- package/dist/viewers/ArchbaseXmlViewer.d.ts +36 -0
- package/dist/viewers/index.d.ts +6 -0
- package/package.json +41 -18
- package/src/buttons/ArchbaseSpeedDial.tsx +282 -0
- package/src/buttons/index.ts +3 -0
- package/src/charts/ArchbaseArcGauge.tsx +325 -0
- package/src/charts/ArchbaseGantt.tsx +494 -0
- package/src/charts/ArchbaseLinearGauge.tsx +288 -0
- package/src/charts/ArchbaseResourceTimeline.tsx +433 -0
- package/src/charts/ArchbaseSparkline.tsx +193 -0
- package/src/charts/index.ts +20 -1
- package/src/containers/ArchbaseExpansionPanel.tsx +208 -0
- package/src/containers/ArchbaseFloatingPanel.tsx +448 -0
- package/src/containers/index.ts +17 -0
- package/src/datagrid/components/pagination/grid-pagination.tsx +2 -2
- package/src/datagrid/export/ArchbaseDataGridExcelExport.tsx +293 -0
- package/src/datagrid/export/index.ts +8 -0
- package/src/datagrid/index.tsx +2 -0
- package/src/display/ArchbaseAnimationWrapper.tsx +272 -0
- package/src/display/ArchbaseBarcodeGenerator.tsx +303 -0
- package/src/display/ArchbaseCarousel.tsx +679 -0
- package/src/display/ArchbaseCodeViewer.tsx +271 -0
- package/src/display/ArchbaseDiffViewer.tsx +108 -0
- package/src/display/ArchbaseJsonTree.tsx +150 -0
- package/src/display/ArchbaseLightbox.tsx +279 -0
- package/src/display/ArchbaseMarquee.tsx +268 -0
- package/src/display/ArchbasePhotoAlbum.tsx +415 -0
- package/src/display/ArchbaseQRCode.tsx +137 -0
- package/src/display/ArchbaseTooltipRich.tsx +317 -0
- package/src/display/ArchbaseVirtualList.tsx +618 -0
- package/src/display/index.ts +94 -1
- package/src/editors/ArchbaseAIPromptInput.tsx +432 -0
- package/src/editors/ArchbaseBarcodeScanner.tsx +570 -0
- package/src/editors/ArchbaseCepInput.tsx +173 -0
- package/src/editors/ArchbaseColorGradientPicker.tsx +460 -0
- package/src/editors/ArchbaseCpfCnpjInput.tsx +193 -0
- package/src/editors/ArchbaseCurrencyInput.tsx +100 -0
- package/src/editors/ArchbaseDualListbox.tsx +671 -0
- package/src/editors/ArchbaseInPlaceEditor.tsx +406 -0
- package/src/editors/ArchbaseLightGrid.tsx +538 -0
- package/src/editors/ArchbaseMentionInput.tsx +384 -0
- package/src/editors/ArchbaseMultiEmail.tsx +367 -0
- package/src/editors/ArchbaseMultiViewCalendar.tsx +439 -0
- package/src/editors/ArchbaseNumberStepper.tsx +219 -0
- package/src/editors/ArchbaseOTPInput.tsx +133 -0
- package/src/editors/ArchbaseOperationHoursEditor.tsx +1 -1
- package/src/editors/ArchbasePhoneInput.tsx +355 -0
- package/src/editors/ArchbaseRangeSlider.tsx +109 -0
- package/src/editors/ArchbaseSignaturePad.tsx +425 -0
- package/src/editors/ArchbaseSpeechToTextInput.tsx +444 -0
- package/src/editors/ArchbaseSpreadsheet.tsx +388 -0
- package/src/editors/ArchbaseTagInput.tsx +326 -0
- package/src/editors/index.tsx +92 -0
- package/src/feedback/ArchbaseChunkProgressBar.tsx +156 -0
- package/src/feedback/ArchbaseRipple.tsx +278 -0
- package/src/feedback/ArchbaseSkeleton.tsx +419 -0
- package/src/feedback/index.ts +22 -1
- package/src/filters/ArchbaseColumnSelector.tsx +268 -0
- package/src/filters/index.ts +2 -0
- package/src/forms/ArchbaseFieldArray.tsx +305 -0
- package/src/forms/ArchbaseFormWizard.tsx +284 -0
- package/src/forms/ArchbaseStepper.tsx +137 -0
- package/src/forms/index.ts +24 -2
- package/src/forms/useArchbaseFormWizard.ts +283 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useArchbaseConfirm.ts +293 -0
- package/src/image/editor/functions/image-processing.ts +5 -0
- package/src/image/editor/index.tsx +305 -391
- package/src/image/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/list/ArchbaseTreeList.tsx +422 -0
- package/src/list/index.ts +3 -0
- package/src/navigation/ArchbaseActionSheet.tsx +301 -0
- package/src/navigation/ArchbaseAppBar.tsx +233 -0
- package/src/navigation/ArchbaseBottomNavigation.tsx +243 -0
- package/src/navigation/ArchbaseContextMenu.tsx +298 -0
- package/src/navigation/ArchbaseKeyboardShortcuts.tsx +472 -0
- package/src/navigation/ArchbaseSpotlight.tsx +483 -0
- package/src/navigation/index.ts +48 -1
- package/src/notification/ArchbaseNotificationCenter.tsx +502 -0
- package/src/notification/index.ts +2 -0
- package/src/onboarding/ArchbaseOnboardingTour.tsx +229 -102
- package/src/viewers/ArchbaseDocViewer.tsx +584 -0
- package/src/viewers/ArchbasePdfBuilder.tsx +291 -0
- package/src/viewers/ArchbaseXmlViewer.tsx +142 -0
- package/src/viewers/index.ts +12 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchbaseBarcodeScanner — scanner de codigos de barras e QR codes usando a API nativa BarcodeDetector
|
|
3
|
+
* com fallback para navegadores que nao suportam a API.
|
|
4
|
+
* Integrado ao DataSource (v1/v2) com suporte a modo inline e modal.
|
|
5
|
+
* @status stable
|
|
6
|
+
*/
|
|
7
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
8
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
9
|
+
import { Box, Button, Group, Input, Modal, Stack, Text, useMantineTheme } from '@mantine/core';
|
|
10
|
+
import { IconScan, IconCamera, IconCameraOff } from '@tabler/icons-react';
|
|
11
|
+
import type { DataSourceEvent, IArchbaseDataSourceBase } from '@archbase/data';
|
|
12
|
+
import { DataSourceEventNames, useArchbaseDidUpdate, useArchbaseV1V2Compatibility } from '@archbase/data';
|
|
13
|
+
import { useValidationErrors } from '@archbase/core';
|
|
14
|
+
import { useForceUpdate } from '@mantine/hooks';
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
BarcodeDetector: any;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ArchbaseBarcodeScannerProps<T, ID> {
|
|
23
|
+
/** Fonte de dados onde sera atribuido o valor do scanner (V1 ou V2) */
|
|
24
|
+
dataSource?: IArchbaseDataSourceBase<T>;
|
|
25
|
+
/** Campo onde devera ser atribuido o valor do scanner na fonte de dados */
|
|
26
|
+
dataField?: string;
|
|
27
|
+
/** Valor controlado (uso standalone) */
|
|
28
|
+
value?: string;
|
|
29
|
+
/** Titulo do campo */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Descricao do campo */
|
|
32
|
+
description?: string;
|
|
33
|
+
/** Ultimo erro ocorrido */
|
|
34
|
+
error?: string;
|
|
35
|
+
/** Indicador se o preenchimento e obrigatorio */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
/** Indicador se o scanner esta desabilitado */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Indicador se e somente leitura */
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** Continuar escaneando apos primeira leitura (default false) */
|
|
42
|
+
continuous?: boolean;
|
|
43
|
+
/** Formatos de codigo aceitos */
|
|
44
|
+
formats?: string[];
|
|
45
|
+
/** Intervalo entre tentativas de deteccao em ms (default 500) */
|
|
46
|
+
scanInterval?: number;
|
|
47
|
+
/** Callback quando um codigo e escaneado */
|
|
48
|
+
onScan?: (code: string, format: string) => void;
|
|
49
|
+
/** Callback quando ocorre um erro */
|
|
50
|
+
onError?: (error: Error) => void;
|
|
51
|
+
/** Largura do video (default 320) */
|
|
52
|
+
width?: number;
|
|
53
|
+
/** Altura do video (default 240) */
|
|
54
|
+
height?: number;
|
|
55
|
+
/** Mostrar botao de lanterna (default false) */
|
|
56
|
+
showTorchButton?: boolean;
|
|
57
|
+
/** Renderizar como botao que abre modal (default false) */
|
|
58
|
+
asModal?: boolean;
|
|
59
|
+
/** Titulo do modal (default 'Escanear codigo') */
|
|
60
|
+
modalTitle?: string;
|
|
61
|
+
/** Label do botao (default 'Escanear') */
|
|
62
|
+
buttonLabel?: string;
|
|
63
|
+
/** Icone do botao */
|
|
64
|
+
buttonIcon?: ReactNode;
|
|
65
|
+
/** Overlay customizado sobre o video */
|
|
66
|
+
overlay?: ReactNode;
|
|
67
|
+
/** Estilo do componente */
|
|
68
|
+
style?: CSSProperties;
|
|
69
|
+
/** Classe CSS */
|
|
70
|
+
className?: string;
|
|
71
|
+
/** Evento quando o valor e alterado */
|
|
72
|
+
onChangeValue?: (value: string) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface DetectedBarcode {
|
|
76
|
+
rawValue: string;
|
|
77
|
+
format: string;
|
|
78
|
+
boundingBox: DOMRectReadOnly;
|
|
79
|
+
cornerPoints: { x: number; y: number }[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DEFAULT_FORMATS = ['qr_code', 'ean_13', 'ean_8', 'code_128', 'code_39'];
|
|
83
|
+
|
|
84
|
+
function ArchbaseBarcodeScannerInner<T, ID>(
|
|
85
|
+
props: ArchbaseBarcodeScannerProps<T, ID> & { onClose?: () => void },
|
|
86
|
+
) {
|
|
87
|
+
const {
|
|
88
|
+
dataSource,
|
|
89
|
+
dataField,
|
|
90
|
+
value: propValue,
|
|
91
|
+
continuous = false,
|
|
92
|
+
formats = DEFAULT_FORMATS,
|
|
93
|
+
scanInterval = 500,
|
|
94
|
+
onScan,
|
|
95
|
+
onError,
|
|
96
|
+
width = 320,
|
|
97
|
+
height = 240,
|
|
98
|
+
showTorchButton = false,
|
|
99
|
+
overlay,
|
|
100
|
+
onChangeValue,
|
|
101
|
+
disabled = false,
|
|
102
|
+
readOnly = false,
|
|
103
|
+
onClose,
|
|
104
|
+
} = props;
|
|
105
|
+
|
|
106
|
+
const theme = useMantineTheme();
|
|
107
|
+
|
|
108
|
+
// V1/V2 compatibility
|
|
109
|
+
const v1v2 = useArchbaseV1V2Compatibility<string>(
|
|
110
|
+
'ArchbaseBarcodeScanner',
|
|
111
|
+
dataSource,
|
|
112
|
+
dataField,
|
|
113
|
+
propValue ?? '',
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const [isScanning, setIsScanning] = useState(false);
|
|
117
|
+
const [lastScannedCode, setLastScannedCode] = useState<string>('');
|
|
118
|
+
const [lastScannedFormat, setLastScannedFormat] = useState<string>('');
|
|
119
|
+
const [cameraError, setCameraError] = useState<string | null>(null);
|
|
120
|
+
const [torchEnabled, setTorchEnabled] = useState(false);
|
|
121
|
+
const [supportsBarcodeDetector, setSupportsBarcodeDetector] = useState(true);
|
|
122
|
+
|
|
123
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
124
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
125
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
126
|
+
const detectorRef = useRef<any>(null);
|
|
127
|
+
const animationFrameRef = useRef<number>(0);
|
|
128
|
+
const lastDetectionTimeRef = useRef<number>(0);
|
|
129
|
+
|
|
130
|
+
// DataSource listener setup
|
|
131
|
+
const dataSourceEventRef = useRef(v1v2.dataSourceEvent);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
dataSourceEventRef.current = v1v2.dataSourceEvent;
|
|
134
|
+
}, [v1v2.dataSourceEvent]);
|
|
135
|
+
|
|
136
|
+
const stableDataSourceEvent = useCallback((event: DataSourceEvent<T>) => {
|
|
137
|
+
dataSourceEventRef.current(event);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
v1v2.loadDataSourceFieldValue();
|
|
142
|
+
if (dataSource && dataField) {
|
|
143
|
+
dataSource.addListener(stableDataSourceEvent);
|
|
144
|
+
const hasFieldListener = typeof (dataSource as any).addFieldChangeListener === 'function';
|
|
145
|
+
if (hasFieldListener) {
|
|
146
|
+
(dataSource as any).addFieldChangeListener(dataField, v1v2.loadDataSourceFieldValue);
|
|
147
|
+
}
|
|
148
|
+
return () => {
|
|
149
|
+
dataSource.removeListener(stableDataSourceEvent);
|
|
150
|
+
if (hasFieldListener) {
|
|
151
|
+
(dataSource as any).removeFieldChangeListener(dataField, v1v2.loadDataSourceFieldValue);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}, [dataSource, dataField, stableDataSourceEvent]);
|
|
156
|
+
|
|
157
|
+
useArchbaseDidUpdate(() => {
|
|
158
|
+
v1v2.loadDataSourceFieldValue();
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
// Initialize BarcodeDetector
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (typeof window !== 'undefined' && window.BarcodeDetector) {
|
|
164
|
+
try {
|
|
165
|
+
detectorRef.current = new window.BarcodeDetector({ formats });
|
|
166
|
+
} catch {
|
|
167
|
+
setSupportsBarcodeDetector(false);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
setSupportsBarcodeDetector(false);
|
|
171
|
+
}
|
|
172
|
+
}, [formats]);
|
|
173
|
+
|
|
174
|
+
const handleCodeDetected = useCallback(
|
|
175
|
+
(code: string, format: string) => {
|
|
176
|
+
setLastScannedCode(code);
|
|
177
|
+
setLastScannedFormat(format);
|
|
178
|
+
v1v2.handleValueChange(code);
|
|
179
|
+
|
|
180
|
+
if (onChangeValue) {
|
|
181
|
+
onChangeValue(code);
|
|
182
|
+
}
|
|
183
|
+
if (onScan) {
|
|
184
|
+
onScan(code, format);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!continuous) {
|
|
188
|
+
stopCamera();
|
|
189
|
+
if (onClose) {
|
|
190
|
+
onClose();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[continuous, onScan, onChangeValue, onClose, v1v2.handleValueChange],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const detectBarcodes = useCallback(async () => {
|
|
198
|
+
if (!videoRef.current || !canvasRef.current || !detectorRef.current) return;
|
|
199
|
+
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
if (now - lastDetectionTimeRef.current < scanInterval) {
|
|
202
|
+
animationFrameRef.current = requestAnimationFrame(detectBarcodes);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
lastDetectionTimeRef.current = now;
|
|
206
|
+
|
|
207
|
+
const video = videoRef.current;
|
|
208
|
+
const canvas = canvasRef.current;
|
|
209
|
+
const ctx = canvas.getContext('2d');
|
|
210
|
+
if (!ctx || video.readyState < video.HAVE_ENOUGH_DATA) {
|
|
211
|
+
animationFrameRef.current = requestAnimationFrame(detectBarcodes);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
canvas.width = video.videoWidth;
|
|
216
|
+
canvas.height = video.videoHeight;
|
|
217
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const barcodes: DetectedBarcode[] = await detectorRef.current.detect(canvas);
|
|
221
|
+
if (barcodes && barcodes.length > 0) {
|
|
222
|
+
const barcode = barcodes[0];
|
|
223
|
+
handleCodeDetected(barcode.rawValue, barcode.format);
|
|
224
|
+
if (!continuous) return;
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// Silently continue on detection errors
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
animationFrameRef.current = requestAnimationFrame(detectBarcodes);
|
|
231
|
+
}, [scanInterval, continuous, handleCodeDetected]);
|
|
232
|
+
|
|
233
|
+
const startCamera = useCallback(async () => {
|
|
234
|
+
if (disabled || readOnly) return;
|
|
235
|
+
|
|
236
|
+
if (!supportsBarcodeDetector) {
|
|
237
|
+
const errorMsg = 'BarcodeDetector API nao e suportada neste navegador. Use Chrome 83+ ou Edge 83+.';
|
|
238
|
+
setCameraError(errorMsg);
|
|
239
|
+
if (onError) {
|
|
240
|
+
onError(new Error(errorMsg));
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
setCameraError(null);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const constraints: MediaStreamConstraints = {
|
|
249
|
+
video: {
|
|
250
|
+
facingMode: 'environment',
|
|
251
|
+
width: { ideal: width * 2 },
|
|
252
|
+
height: { ideal: height * 2 },
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
257
|
+
streamRef.current = stream;
|
|
258
|
+
|
|
259
|
+
if (videoRef.current) {
|
|
260
|
+
videoRef.current.srcObject = stream;
|
|
261
|
+
await videoRef.current.play();
|
|
262
|
+
setIsScanning(true);
|
|
263
|
+
animationFrameRef.current = requestAnimationFrame(detectBarcodes);
|
|
264
|
+
}
|
|
265
|
+
} catch (err: any) {
|
|
266
|
+
let errorMsg = 'Erro ao acessar a camera.';
|
|
267
|
+
if (err.name === 'NotAllowedError') {
|
|
268
|
+
errorMsg = 'Permissao para acessar a camera foi negada.';
|
|
269
|
+
} else if (err.name === 'NotFoundError') {
|
|
270
|
+
errorMsg = 'Nenhuma camera encontrada no dispositivo.';
|
|
271
|
+
} else if (err.name === 'NotReadableError') {
|
|
272
|
+
errorMsg = 'Camera esta em uso por outro aplicativo.';
|
|
273
|
+
}
|
|
274
|
+
setCameraError(errorMsg);
|
|
275
|
+
if (onError) {
|
|
276
|
+
onError(new Error(errorMsg));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}, [disabled, readOnly, width, height, supportsBarcodeDetector, detectBarcodes, onError]);
|
|
280
|
+
|
|
281
|
+
const stopCamera = useCallback(() => {
|
|
282
|
+
if (animationFrameRef.current) {
|
|
283
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
284
|
+
animationFrameRef.current = 0;
|
|
285
|
+
}
|
|
286
|
+
if (streamRef.current) {
|
|
287
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
288
|
+
streamRef.current = null;
|
|
289
|
+
}
|
|
290
|
+
if (videoRef.current) {
|
|
291
|
+
videoRef.current.srcObject = null;
|
|
292
|
+
}
|
|
293
|
+
setIsScanning(false);
|
|
294
|
+
setTorchEnabled(false);
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
const toggleTorch = useCallback(async () => {
|
|
298
|
+
if (!streamRef.current) return;
|
|
299
|
+
const track = streamRef.current.getVideoTracks()[0];
|
|
300
|
+
if (!track) return;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const capabilities = track.getCapabilities() as any;
|
|
304
|
+
if (capabilities.torch) {
|
|
305
|
+
const newTorchState = !torchEnabled;
|
|
306
|
+
await track.applyConstraints({
|
|
307
|
+
advanced: [{ torch: newTorchState } as any],
|
|
308
|
+
});
|
|
309
|
+
setTorchEnabled(newTorchState);
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// torch not supported on this device
|
|
313
|
+
}
|
|
314
|
+
}, [torchEnabled]);
|
|
315
|
+
|
|
316
|
+
// Cleanup on unmount
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
return () => {
|
|
319
|
+
stopCamera();
|
|
320
|
+
};
|
|
321
|
+
}, []);
|
|
322
|
+
|
|
323
|
+
const isReadOnly = () => {
|
|
324
|
+
let tmpReadOnly = readOnly;
|
|
325
|
+
if (dataSource && !readOnly) {
|
|
326
|
+
tmpReadOnly = dataSource.isBrowsing();
|
|
327
|
+
}
|
|
328
|
+
return tmpReadOnly;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<Stack gap="xs">
|
|
333
|
+
<Box
|
|
334
|
+
style={{
|
|
335
|
+
position: 'relative',
|
|
336
|
+
width,
|
|
337
|
+
height,
|
|
338
|
+
backgroundColor: theme.colors.dark[7],
|
|
339
|
+
borderRadius: theme.radius.sm,
|
|
340
|
+
overflow: 'hidden',
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<video
|
|
344
|
+
ref={videoRef}
|
|
345
|
+
style={{
|
|
346
|
+
width: '100%',
|
|
347
|
+
height: '100%',
|
|
348
|
+
objectFit: 'cover',
|
|
349
|
+
display: isScanning ? 'block' : 'none',
|
|
350
|
+
}}
|
|
351
|
+
playsInline
|
|
352
|
+
muted
|
|
353
|
+
/>
|
|
354
|
+
|
|
355
|
+
{!isScanning && (
|
|
356
|
+
<Stack
|
|
357
|
+
align="center"
|
|
358
|
+
justify="center"
|
|
359
|
+
style={{
|
|
360
|
+
width: '100%',
|
|
361
|
+
height: '100%',
|
|
362
|
+
position: 'absolute',
|
|
363
|
+
top: 0,
|
|
364
|
+
left: 0,
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
{cameraError ? (
|
|
368
|
+
<>
|
|
369
|
+
<IconCameraOff size={48} color={theme.colors.red[5]} />
|
|
370
|
+
<Text size="xs" c="red" ta="center" px="sm">
|
|
371
|
+
{cameraError}
|
|
372
|
+
</Text>
|
|
373
|
+
</>
|
|
374
|
+
) : (
|
|
375
|
+
<>
|
|
376
|
+
<IconCamera size={48} color={theme.colors.gray[5]} />
|
|
377
|
+
<Text size="xs" c="dimmed" ta="center" px="sm">
|
|
378
|
+
Clique em iniciar para ativar a camera
|
|
379
|
+
</Text>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
</Stack>
|
|
383
|
+
)}
|
|
384
|
+
|
|
385
|
+
{/* Scan overlay */}
|
|
386
|
+
{isScanning && !overlay && (
|
|
387
|
+
<Box
|
|
388
|
+
style={{
|
|
389
|
+
position: 'absolute',
|
|
390
|
+
top: '50%',
|
|
391
|
+
left: '50%',
|
|
392
|
+
transform: 'translate(-50%, -50%)',
|
|
393
|
+
width: '60%',
|
|
394
|
+
height: '60%',
|
|
395
|
+
border: `2px solid ${theme.colors[theme.primaryColor][5]}`,
|
|
396
|
+
borderRadius: theme.radius.sm,
|
|
397
|
+
pointerEvents: 'none',
|
|
398
|
+
opacity: 0.7,
|
|
399
|
+
}}
|
|
400
|
+
/>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{/* Custom overlay */}
|
|
404
|
+
{isScanning && overlay && (
|
|
405
|
+
<Box
|
|
406
|
+
style={{
|
|
407
|
+
position: 'absolute',
|
|
408
|
+
top: 0,
|
|
409
|
+
left: 0,
|
|
410
|
+
width: '100%',
|
|
411
|
+
height: '100%',
|
|
412
|
+
pointerEvents: 'none',
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
{overlay}
|
|
416
|
+
</Box>
|
|
417
|
+
)}
|
|
418
|
+
|
|
419
|
+
{/* Hidden canvas for frame capture */}
|
|
420
|
+
<canvas ref={canvasRef} style={{ display: 'none' }} />
|
|
421
|
+
</Box>
|
|
422
|
+
|
|
423
|
+
{/* Controls */}
|
|
424
|
+
<Group gap="xs">
|
|
425
|
+
{!isScanning ? (
|
|
426
|
+
<Button
|
|
427
|
+
size="xs"
|
|
428
|
+
leftSection={<IconScan size={16} />}
|
|
429
|
+
onClick={startCamera}
|
|
430
|
+
disabled={disabled || isReadOnly()}
|
|
431
|
+
>
|
|
432
|
+
Iniciar scanner
|
|
433
|
+
</Button>
|
|
434
|
+
) : (
|
|
435
|
+
<Button
|
|
436
|
+
size="xs"
|
|
437
|
+
variant="outline"
|
|
438
|
+
color="red"
|
|
439
|
+
leftSection={<IconCameraOff size={16} />}
|
|
440
|
+
onClick={stopCamera}
|
|
441
|
+
>
|
|
442
|
+
Parar scanner
|
|
443
|
+
</Button>
|
|
444
|
+
)}
|
|
445
|
+
{showTorchButton && isScanning && (
|
|
446
|
+
<Button size="xs" variant="light" onClick={toggleTorch}>
|
|
447
|
+
{torchEnabled ? 'Desligar lanterna' : 'Ligar lanterna'}
|
|
448
|
+
</Button>
|
|
449
|
+
)}
|
|
450
|
+
</Group>
|
|
451
|
+
|
|
452
|
+
{/* Last scanned code */}
|
|
453
|
+
{lastScannedCode && (
|
|
454
|
+
<Box
|
|
455
|
+
style={{
|
|
456
|
+
padding: theme.spacing.xs,
|
|
457
|
+
backgroundColor:
|
|
458
|
+
theme.colors[theme.primaryColor][0],
|
|
459
|
+
borderRadius: theme.radius.sm,
|
|
460
|
+
border: `1px solid ${theme.colors[theme.primaryColor][3]}`,
|
|
461
|
+
}}
|
|
462
|
+
>
|
|
463
|
+
<Text size="xs" c="dimmed">
|
|
464
|
+
Ultimo codigo ({lastScannedFormat}):
|
|
465
|
+
</Text>
|
|
466
|
+
<Text size="sm" fw={600}>
|
|
467
|
+
{lastScannedCode}
|
|
468
|
+
</Text>
|
|
469
|
+
</Box>
|
|
470
|
+
)}
|
|
471
|
+
</Stack>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function ArchbaseBarcodeScanner<T, ID>(props: ArchbaseBarcodeScannerProps<T, ID>) {
|
|
476
|
+
const {
|
|
477
|
+
asModal = false,
|
|
478
|
+
modalTitle = 'Escanear codigo',
|
|
479
|
+
buttonLabel = 'Escanear',
|
|
480
|
+
buttonIcon,
|
|
481
|
+
label,
|
|
482
|
+
description,
|
|
483
|
+
error,
|
|
484
|
+
required,
|
|
485
|
+
style,
|
|
486
|
+
className,
|
|
487
|
+
disabled = false,
|
|
488
|
+
readOnly = false,
|
|
489
|
+
dataSource,
|
|
490
|
+
} = props;
|
|
491
|
+
|
|
492
|
+
const [modalOpened, setModalOpened] = useState(false);
|
|
493
|
+
|
|
494
|
+
// Validation context
|
|
495
|
+
const validationContext = useValidationErrors();
|
|
496
|
+
const fieldKey = `${props.dataField}`;
|
|
497
|
+
const contextError = validationContext?.getError(fieldKey);
|
|
498
|
+
|
|
499
|
+
const [internalError, setInternalError] = useState<string | undefined>(error);
|
|
500
|
+
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (error !== undefined && error !== internalError) {
|
|
503
|
+
setInternalError(error);
|
|
504
|
+
}
|
|
505
|
+
}, [error]);
|
|
506
|
+
|
|
507
|
+
const displayError = internalError || contextError;
|
|
508
|
+
|
|
509
|
+
const isReadOnly = () => {
|
|
510
|
+
let tmpReadOnly = readOnly;
|
|
511
|
+
if (dataSource && !readOnly) {
|
|
512
|
+
tmpReadOnly = dataSource.isBrowsing();
|
|
513
|
+
}
|
|
514
|
+
return tmpReadOnly;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
if (asModal) {
|
|
518
|
+
return (
|
|
519
|
+
<Box style={style} className={className}>
|
|
520
|
+
{label && (
|
|
521
|
+
<Input.Label required={required}>{label}</Input.Label>
|
|
522
|
+
)}
|
|
523
|
+
{description && (
|
|
524
|
+
<Input.Description>{description}</Input.Description>
|
|
525
|
+
)}
|
|
526
|
+
<Button
|
|
527
|
+
leftSection={buttonIcon ?? <IconScan size={16} />}
|
|
528
|
+
onClick={() => setModalOpened(true)}
|
|
529
|
+
disabled={disabled || isReadOnly()}
|
|
530
|
+
variant="light"
|
|
531
|
+
mt={4}
|
|
532
|
+
>
|
|
533
|
+
{buttonLabel}
|
|
534
|
+
</Button>
|
|
535
|
+
{displayError && (
|
|
536
|
+
<Input.Error mt={4}>{displayError}</Input.Error>
|
|
537
|
+
)}
|
|
538
|
+
<Modal
|
|
539
|
+
opened={modalOpened}
|
|
540
|
+
onClose={() => setModalOpened(false)}
|
|
541
|
+
title={modalTitle}
|
|
542
|
+
centered
|
|
543
|
+
size="auto"
|
|
544
|
+
>
|
|
545
|
+
<ArchbaseBarcodeScannerInner<T, ID>
|
|
546
|
+
{...props}
|
|
547
|
+
onClose={() => setModalOpened(false)}
|
|
548
|
+
/>
|
|
549
|
+
</Modal>
|
|
550
|
+
</Box>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
<Box style={style} className={className}>
|
|
556
|
+
{label && (
|
|
557
|
+
<Input.Label required={required}>{label}</Input.Label>
|
|
558
|
+
)}
|
|
559
|
+
{description && (
|
|
560
|
+
<Input.Description>{description}</Input.Description>
|
|
561
|
+
)}
|
|
562
|
+
<ArchbaseBarcodeScannerInner<T, ID> {...props} />
|
|
563
|
+
{displayError && (
|
|
564
|
+
<Input.Error mt={4}>{displayError}</Input.Error>
|
|
565
|
+
)}
|
|
566
|
+
</Box>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
ArchbaseBarcodeScanner.displayName = 'ArchbaseBarcodeScanner';
|