@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.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/es/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/authRedirect.d.ts +5 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +3 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +8 -1
- package/es/index.mjs +190 -141
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +9 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +17 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +195 -146
- package/lib/locale/languageCodes.js +3 -1
- package/package.json +9 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/authRedirect.test.ts +137 -0
- package/src/__tests__/browserChecker.test.ts +185 -6
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +38 -37
- package/src/__tests__/settings-layout-root.test.tsx +260 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-shell.test.tsx +200 -0
- package/src/acl/ACLProvider.tsx +21 -0
- package/src/authRedirect.ts +72 -3
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
- package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +2 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +111 -16
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +78 -32
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +3 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +14 -1
- package/src/locale/languageCodes.ts +3 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +110 -70
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +238 -0
- package/src/settings-app/SettingsShell.tsx +167 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +104 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +301 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +122 -141
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats } from 'html5-qrcode';
|
|
11
|
-
import
|
|
11
|
+
import jsQR from 'jsqr';
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
12
13
|
import type { CodeFormatsToSupport } from './types';
|
|
13
14
|
|
|
14
15
|
type ScannerSize = {
|
|
@@ -20,12 +21,44 @@ type UseCodeScannerOptions = {
|
|
|
20
21
|
enabled: boolean;
|
|
21
22
|
elementId: string;
|
|
22
23
|
formatsToSupport?: CodeFormatsToSupport;
|
|
23
|
-
scanBoxSize?: ScannerSize;
|
|
24
24
|
onScannerSizeChanged?: (size: ScannerSize) => void;
|
|
25
25
|
onScanSuccess: (text: string) => void;
|
|
26
26
|
onScanFailure?: () => void;
|
|
27
|
+
onCameraStartFailure?: (error: unknown) => void;
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
type ImageDataVariant = {
|
|
31
|
+
imageData: ImageData;
|
|
32
|
+
maxSize: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type JsQRImageTransform = {
|
|
36
|
+
contrast?: number;
|
|
37
|
+
threshold?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type FocusMediaTrackCapabilities = MediaTrackCapabilities & {
|
|
41
|
+
focusMode?: string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type FocusMediaTrackConstraintSet = MediaTrackConstraintSet & {
|
|
45
|
+
focusMode?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const QR_SCAN_IMAGE_SIZES = [3200, 2400, 1600, 1000];
|
|
49
|
+
const LIVE_QR_SCAN_INTERVAL = 120;
|
|
50
|
+
const LIVE_QR_SCAN_MAX_WIDTH = 960;
|
|
51
|
+
const LIVE_QR_SCAN_MAX_HEIGHT = 540;
|
|
52
|
+
const QR_SCAN_IMAGE_TRANSFORMS: JsQRImageTransform[] = [
|
|
53
|
+
{},
|
|
54
|
+
{ contrast: 3, threshold: 105 },
|
|
55
|
+
{ contrast: 2, threshold: 105 },
|
|
56
|
+
{ contrast: 3, threshold: 120 },
|
|
57
|
+
{ contrast: 2, threshold: 120 },
|
|
58
|
+
{ contrast: 3, threshold: 90 },
|
|
59
|
+
{ contrast: 4, threshold: 105 },
|
|
60
|
+
];
|
|
61
|
+
|
|
29
62
|
export const DEFAULT_CODE_FORMATS: CodeFormatsToSupport = [
|
|
30
63
|
Html5QrcodeSupportedFormats.QR_CODE,
|
|
31
64
|
Html5QrcodeSupportedFormats.CODE_128,
|
|
@@ -43,8 +76,79 @@ export const DEFAULT_CODE_FORMATS: CodeFormatsToSupport = [
|
|
|
43
76
|
|
|
44
77
|
export function getCodeScanBoxSize(width: number, height: number) {
|
|
45
78
|
return {
|
|
46
|
-
width: Math.floor(Math.min(width *
|
|
47
|
-
height: Math.floor(Math.min(height *
|
|
79
|
+
width: Math.floor(Math.min((width * 90) / 100, 1152)),
|
|
80
|
+
height: Math.floor(Math.min((height * 70) / 100, 540)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function scanQrVideoFrame(video: HTMLVideoElement, canvas: HTMLCanvasElement) {
|
|
85
|
+
if (!video.videoWidth || !video.videoHeight || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const viewfinderWidth = video.clientWidth || video.videoWidth;
|
|
90
|
+
const viewfinderHeight = video.clientHeight || video.videoHeight;
|
|
91
|
+
const scanBoxSize = getCodeScanBoxSize(viewfinderWidth, viewfinderHeight);
|
|
92
|
+
const sourceWidth = Math.min(video.videoWidth, Math.floor(scanBoxSize.width * (video.videoWidth / viewfinderWidth)));
|
|
93
|
+
const sourceHeight = Math.min(
|
|
94
|
+
video.videoHeight,
|
|
95
|
+
Math.floor(scanBoxSize.height * (video.videoHeight / viewfinderHeight)),
|
|
96
|
+
);
|
|
97
|
+
const sourceX = Math.floor((video.videoWidth - sourceWidth) / 2);
|
|
98
|
+
const sourceY = Math.floor((video.videoHeight - sourceHeight) / 2);
|
|
99
|
+
const targetScale = Math.min(1, LIVE_QR_SCAN_MAX_WIDTH / sourceWidth, LIVE_QR_SCAN_MAX_HEIGHT / sourceHeight);
|
|
100
|
+
const targetWidth = Math.max(1, Math.floor(sourceWidth * targetScale));
|
|
101
|
+
const targetHeight = Math.max(1, Math.floor(sourceHeight * targetScale));
|
|
102
|
+
if (canvas.width !== targetWidth) {
|
|
103
|
+
canvas.width = targetWidth;
|
|
104
|
+
}
|
|
105
|
+
if (canvas.height !== targetHeight) {
|
|
106
|
+
canvas.height = targetHeight;
|
|
107
|
+
}
|
|
108
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
109
|
+
if (!context) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
context.drawImage(video, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, targetWidth, targetHeight);
|
|
114
|
+
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
|
115
|
+
return jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' })?.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function startLiveQrScan(elementId: string, onScanSuccess: (text: string) => void) {
|
|
119
|
+
const canvas = document.createElement('canvas');
|
|
120
|
+
let timer: number | undefined;
|
|
121
|
+
let stopped = false;
|
|
122
|
+
|
|
123
|
+
const scan = () => {
|
|
124
|
+
if (stopped) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const video = document.getElementById(elementId)?.querySelector('video');
|
|
128
|
+
if (video) {
|
|
129
|
+
const decodedText = scanQrVideoFrame(video, canvas);
|
|
130
|
+
if (decodedText) {
|
|
131
|
+
stopped = true;
|
|
132
|
+
onScanSuccess(decodedText);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
timer = window.setTimeout(scan, LIVE_QR_SCAN_INTERVAL);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
timer = window.setTimeout(scan, 0);
|
|
140
|
+
return () => {
|
|
141
|
+
stopped = true;
|
|
142
|
+
if (timer !== undefined) {
|
|
143
|
+
window.clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clampCodeScanBoxSize(scanBoxSize: ScannerSize, width: number, height: number) {
|
|
149
|
+
return {
|
|
150
|
+
width: Math.min(scanBoxSize.width, width),
|
|
151
|
+
height: Math.min(scanBoxSize.height, height),
|
|
48
152
|
};
|
|
49
153
|
}
|
|
50
154
|
|
|
@@ -62,39 +166,238 @@ async function stopScanner(scanner?: Html5Qrcode, options: { clear?: boolean } =
|
|
|
62
166
|
}
|
|
63
167
|
}
|
|
64
168
|
|
|
169
|
+
async function enableContinuousFocus(scanner: Html5Qrcode) {
|
|
170
|
+
try {
|
|
171
|
+
const capabilities = scanner.getRunningTrackCapabilities() as FocusMediaTrackCapabilities;
|
|
172
|
+
if (!capabilities.focusMode?.includes('continuous')) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const focusConstraints: FocusMediaTrackConstraintSet = { focusMode: 'continuous' };
|
|
176
|
+
await scanner.applyVideoConstraints({ advanced: [focusConstraints] });
|
|
177
|
+
} catch {
|
|
178
|
+
// Some browsers expose incomplete camera capability APIs. Scanning should continue without explicit focus control.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isSafariBrowser() {
|
|
183
|
+
const { userAgent, vendor } = navigator;
|
|
184
|
+
return /Apple/i.test(vendor) && /Safari/i.test(userAgent) && !/CriOS|FxiOS|EdgiOS|Chrome/i.test(userAgent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function loadImage(file: File) {
|
|
188
|
+
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
189
|
+
const image = new Image();
|
|
190
|
+
const url = URL.createObjectURL(file);
|
|
191
|
+
const cleanup = () => URL.revokeObjectURL(url);
|
|
192
|
+
|
|
193
|
+
image.onload = () => {
|
|
194
|
+
cleanup();
|
|
195
|
+
resolve(image);
|
|
196
|
+
};
|
|
197
|
+
image.onerror = (event) => {
|
|
198
|
+
cleanup();
|
|
199
|
+
reject(event);
|
|
200
|
+
};
|
|
201
|
+
image.onabort = (event) => {
|
|
202
|
+
cleanup();
|
|
203
|
+
reject(event);
|
|
204
|
+
};
|
|
205
|
+
image.src = url;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getScanImageSizes(naturalWidth: number, naturalHeight: number) {
|
|
210
|
+
const maxSize = Math.max(naturalWidth, naturalHeight);
|
|
211
|
+
const cappedMaxSize = Math.min(maxSize, QR_SCAN_IMAGE_SIZES[0]);
|
|
212
|
+
return Array.from(new Set([cappedMaxSize, ...QR_SCAN_IMAGE_SIZES.filter((size) => size < cappedMaxSize)])).sort(
|
|
213
|
+
(a, b) => b - a,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getImageDataFromImage(image: HTMLImageElement, maxSize: number): ImageDataVariant | undefined {
|
|
218
|
+
const naturalWidth = image.naturalWidth || image.width;
|
|
219
|
+
const naturalHeight = image.naturalHeight || image.height;
|
|
220
|
+
|
|
221
|
+
const scale = Math.min(1, maxSize / Math.max(naturalWidth, naturalHeight));
|
|
222
|
+
const width = Math.max(1, Math.round(naturalWidth * scale));
|
|
223
|
+
const height = Math.max(1, Math.round(naturalHeight * scale));
|
|
224
|
+
const canvas = document.createElement('canvas');
|
|
225
|
+
canvas.width = width;
|
|
226
|
+
canvas.height = height;
|
|
227
|
+
const context = canvas.getContext('2d');
|
|
228
|
+
if (!context) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
context.drawImage(image, 0, 0, width, height);
|
|
233
|
+
return {
|
|
234
|
+
imageData: context.getImageData(0, 0, width, height),
|
|
235
|
+
maxSize,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function getImageDataVariants(file: File) {
|
|
240
|
+
const image = await loadImage(file);
|
|
241
|
+
const naturalWidth = image.naturalWidth || image.width;
|
|
242
|
+
const naturalHeight = image.naturalHeight || image.height;
|
|
243
|
+
|
|
244
|
+
if (!naturalWidth || !naturalHeight) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return getScanImageSizes(naturalWidth, naturalHeight)
|
|
249
|
+
.map((maxSize) => getImageDataFromImage(image, maxSize))
|
|
250
|
+
.filter((variant): variant is ImageDataVariant => !!variant);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getTransformedImageData(imageData: ImageData, transform: JsQRImageTransform) {
|
|
254
|
+
const { contrast = 1, threshold } = transform;
|
|
255
|
+
if (contrast === 1 && threshold == null) {
|
|
256
|
+
return imageData.data;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
260
|
+
for (let index = 0; index < data.length; index += 4) {
|
|
261
|
+
let luminance = data[index] * 0.299 + data[index + 1] * 0.587 + data[index + 2] * 0.114;
|
|
262
|
+
luminance = Math.max(0, Math.min(255, (luminance - 128) * contrast + 128));
|
|
263
|
+
if (threshold != null) {
|
|
264
|
+
luminance = luminance < threshold ? 0 : 255;
|
|
265
|
+
}
|
|
266
|
+
data[index] = luminance;
|
|
267
|
+
data[index + 1] = luminance;
|
|
268
|
+
data[index + 2] = luminance;
|
|
269
|
+
}
|
|
270
|
+
return data;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function scanFileWithJsQR(file: File, formatsToSupport?: CodeFormatsToSupport) {
|
|
274
|
+
const formats = formatsToSupport?.length ? formatsToSupport : DEFAULT_CODE_FORMATS;
|
|
275
|
+
if (!formats.includes(Html5QrcodeSupportedFormats.QR_CODE)) {
|
|
276
|
+
throw new Error('QR_CODE is not included in the requested formats');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const variants = await getImageDataVariants(file);
|
|
280
|
+
if (!variants?.length) {
|
|
281
|
+
throw new Error('Failed to prepare uploaded image for QR decoding');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const { imageData } of variants) {
|
|
285
|
+
for (const transform of QR_SCAN_IMAGE_TRANSFORMS) {
|
|
286
|
+
const data = getTransformedImageData(imageData, transform);
|
|
287
|
+
const qrCode = jsQR(data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' });
|
|
288
|
+
if (qrCode?.data) {
|
|
289
|
+
return qrCode.data;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
throw new Error('No QR code decoded by jsQR');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function shouldScanQrWithJsQR(formatsToSupport?: CodeFormatsToSupport) {
|
|
298
|
+
const formats = formatsToSupport?.length ? formatsToSupport : DEFAULT_CODE_FORMATS;
|
|
299
|
+
return formats.includes(Html5QrcodeSupportedFormats.QR_CODE);
|
|
300
|
+
}
|
|
301
|
+
|
|
65
302
|
export function useCodeScanner({
|
|
66
303
|
enabled,
|
|
67
304
|
elementId,
|
|
68
305
|
formatsToSupport,
|
|
69
|
-
scanBoxSize,
|
|
70
306
|
onScannerSizeChanged,
|
|
71
307
|
onScanSuccess,
|
|
72
308
|
onScanFailure,
|
|
309
|
+
onCameraStartFailure,
|
|
73
310
|
}: UseCodeScannerOptions) {
|
|
74
311
|
const [scanner, setScanner] = useState<Html5Qrcode>();
|
|
312
|
+
const liveQrScanStopRef = useRef<() => void>();
|
|
313
|
+
const scanSucceededRef = useRef(false);
|
|
314
|
+
const scanSessionRef = useRef(0);
|
|
315
|
+
|
|
316
|
+
const stopLiveQrScan = useCallback(() => {
|
|
317
|
+
liveQrScanStopRef.current?.();
|
|
318
|
+
liveQrScanStopRef.current = undefined;
|
|
319
|
+
}, []);
|
|
320
|
+
|
|
321
|
+
const cancelActiveScan = useCallback(() => {
|
|
322
|
+
scanSessionRef.current += 1;
|
|
323
|
+
stopLiveQrScan();
|
|
324
|
+
}, [stopLiveQrScan]);
|
|
325
|
+
|
|
326
|
+
const reportScanSuccess = useCallback(
|
|
327
|
+
(text: string) => {
|
|
328
|
+
if (scanSucceededRef.current) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
scanSucceededRef.current = true;
|
|
332
|
+
stopLiveQrScan();
|
|
333
|
+
onScanSuccess(text);
|
|
334
|
+
},
|
|
335
|
+
[onScanSuccess, stopLiveQrScan],
|
|
336
|
+
);
|
|
75
337
|
|
|
76
338
|
const startScanCamera = useCallback(
|
|
77
339
|
async (scannerInstance: Html5Qrcode) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
340
|
+
cancelActiveScan();
|
|
341
|
+
const scanSession = scanSessionRef.current;
|
|
342
|
+
scanSucceededRef.current = false;
|
|
343
|
+
try {
|
|
344
|
+
await scannerInstance.start(
|
|
345
|
+
{ facingMode: 'environment' },
|
|
346
|
+
{
|
|
347
|
+
fps: 8,
|
|
348
|
+
disableFlip: false,
|
|
349
|
+
videoConstraints: {
|
|
350
|
+
facingMode: { ideal: 'environment' },
|
|
351
|
+
width: { ideal: 1920 },
|
|
352
|
+
height: { ideal: 1080 },
|
|
353
|
+
frameRate: { ideal: 30 },
|
|
354
|
+
},
|
|
355
|
+
qrbox(width, height) {
|
|
356
|
+
onScannerSizeChanged?.({ width, height });
|
|
357
|
+
return clampCodeScanBoxSize(getCodeScanBoxSize(width, height), width, height);
|
|
358
|
+
},
|
|
85
359
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
360
|
+
(decodedText) => {
|
|
361
|
+
reportScanSuccess(decodedText);
|
|
362
|
+
},
|
|
363
|
+
undefined,
|
|
364
|
+
);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (scanSession !== scanSessionRef.current) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
if (scanSession !== scanSessionRef.current) {
|
|
372
|
+
try {
|
|
373
|
+
await stopScanner(scannerInstance, { clear: true });
|
|
374
|
+
} catch {
|
|
375
|
+
// The scanner may already have been cleared by the canceled session cleanup.
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (shouldScanQrWithJsQR(formatsToSupport)) {
|
|
380
|
+
liveQrScanStopRef.current = startLiveQrScan(elementId, reportScanSuccess);
|
|
381
|
+
}
|
|
382
|
+
await enableContinuousFocus(scannerInstance);
|
|
92
383
|
},
|
|
93
|
-
[
|
|
384
|
+
[cancelActiveScan, elementId, formatsToSupport, onScannerSizeChanged, reportScanSuccess],
|
|
94
385
|
);
|
|
95
386
|
|
|
96
387
|
const startScanFile = useCallback(
|
|
97
388
|
async (file: File) => {
|
|
389
|
+
cancelActiveScan();
|
|
390
|
+
scanSucceededRef.current = false;
|
|
391
|
+
if (isSafariBrowser() && shouldScanQrWithJsQR(formatsToSupport)) {
|
|
392
|
+
try {
|
|
393
|
+
const decodedText = await scanFileWithJsQR(file, formatsToSupport);
|
|
394
|
+
reportScanSuccess(decodedText);
|
|
395
|
+
return;
|
|
396
|
+
} catch {
|
|
397
|
+
// Fall through to html5-qrcode so barcode uploads still work in Safari.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
98
401
|
if (!scanner) {
|
|
99
402
|
return;
|
|
100
403
|
}
|
|
@@ -102,13 +405,13 @@ export function useCodeScanner({
|
|
|
102
405
|
await stopScanner(scanner);
|
|
103
406
|
try {
|
|
104
407
|
const result = await scanner.scanFileV2(file, false);
|
|
105
|
-
|
|
106
|
-
} catch
|
|
408
|
+
reportScanSuccess(result.decodedText);
|
|
409
|
+
} catch {
|
|
107
410
|
onScanFailure?.();
|
|
108
411
|
await startScanCamera(scanner);
|
|
109
412
|
}
|
|
110
413
|
},
|
|
111
|
-
[onScanFailure,
|
|
414
|
+
[cancelActiveScan, formatsToSupport, onScanFailure, reportScanSuccess, scanner, startScanCamera],
|
|
112
415
|
);
|
|
113
416
|
|
|
114
417
|
useEffect(() => {
|
|
@@ -121,14 +424,19 @@ export function useCodeScanner({
|
|
|
121
424
|
verbose: false,
|
|
122
425
|
});
|
|
123
426
|
setScanner(scannerInstance);
|
|
124
|
-
startScanCamera(scannerInstance).catch(() => {
|
|
427
|
+
startScanCamera(scannerInstance).catch((error: unknown) => {
|
|
428
|
+
if (onCameraStartFailure) {
|
|
429
|
+
onCameraStartFailure(error);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
125
432
|
onScanFailure?.();
|
|
126
433
|
});
|
|
127
434
|
|
|
128
435
|
return () => {
|
|
436
|
+
cancelActiveScan();
|
|
129
437
|
stopScanner(scannerInstance, { clear: true }).catch(() => undefined);
|
|
130
438
|
};
|
|
131
|
-
}, [elementId, enabled, formatsToSupport, onScanFailure, startScanCamera]);
|
|
439
|
+
}, [cancelActiveScan, elementId, enabled, formatsToSupport, onCameraStartFailure, onScanFailure, startScanCamera]);
|
|
132
440
|
|
|
133
441
|
return {
|
|
134
442
|
startScanFile,
|