@pallasoft/ps-file-components 2.0.1 → 2.1.0
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/components/ps-document-scanner-loader.d.ts +1 -1
- package/dist/components/ps-document-scanner-sheet.d.ts +1 -1
- package/dist/components/ps-document-scanner-sheet.js +87 -2
- package/dist/components/ps-document-scanner.d.ts +1 -1
- package/dist/components/ps-document-scanner.js +2 -2
- package/dist/components/ps-image-viewer.js +35 -16
- package/dist/components/ps-pdf-viewer.js +18 -1
- package/dist/components/ps-scan-camera-view.js +2 -2
- package/dist/components/ps-scan-page-strip.d.ts +8 -0
- package/dist/components/ps-scan-page-strip.js +8 -0
- package/dist/components/ps-scan-preview.d.ts +3 -1
- package/dist/components/ps-scan-preview.js +5 -1
- package/dist/components/ps-scan-viewfinder-overlay.d.ts +4 -2
- package/dist/components/ps-scan-viewfinder-overlay.js +6 -6
- package/dist/constants/ps-file-viewer-config.d.ts +1 -0
- package/dist/constants/ps-file-viewer-config.js +1 -0
- package/dist/constants/ps-scan-config.d.ts +7 -3
- package/dist/constants/ps-scan-config.js +7 -3
- package/dist/constants/ps-scan-detect-profiles.d.ts +10 -0
- package/dist/constants/ps-scan-detect-profiles.js +8 -0
- package/dist/esm/components/ps-document-scanner-sheet.js +87 -2
- package/dist/esm/components/ps-document-scanner.js +2 -2
- package/dist/esm/components/ps-image-viewer.js +35 -16
- package/dist/esm/components/ps-pdf-viewer.js +18 -1
- package/dist/esm/components/ps-scan-camera-view.js +2 -2
- package/dist/esm/components/ps-scan-page-strip.js +4 -0
- package/dist/esm/components/ps-scan-preview.js +5 -1
- package/dist/esm/components/ps-scan-viewfinder-overlay.js +6 -6
- package/dist/esm/constants/ps-file-viewer-config.js +1 -0
- package/dist/esm/constants/ps-scan-config.js +7 -3
- package/dist/esm/constants/ps-scan-detect-profiles.js +5 -0
- package/dist/esm/hooks/use-ps-drag-pan.js +66 -0
- package/dist/esm/hooks/use-ps-scan-live-detection.js +47 -22
- package/dist/esm/hooks/use-ps-scan-quad-tween.js +34 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/models/ps-document-scanner-page-labels.js +1 -0
- package/dist/esm/models/ps-drag-pan-handlers.js +1 -0
- package/dist/esm/models/ps-drag-pan-options.js +1 -0
- package/dist/esm/models/ps-scan-detect-profile.js +1 -0
- package/dist/esm/models/ps-scan-multi-page.js +1 -0
- package/dist/esm/models/ps-scan-page.js +1 -0
- package/dist/esm/models/ps-scan-pdf-page.js +1 -0
- package/dist/esm/services/ps-pan-service.js +13 -0
- package/dist/esm/services/ps-scan-edge-service.js +7 -4
- package/dist/esm/services/ps-scan-file-name-service.js +3 -0
- package/dist/esm/services/ps-scan-frame-service.js +10 -0
- package/dist/esm/services/ps-scan-pdf-service.js +69 -0
- package/dist/esm/services/ps-scan-quad-tween-service.js +10 -0
- package/dist/hooks/use-ps-drag-pan.d.ts +3 -0
- package/dist/hooks/use-ps-drag-pan.js +70 -0
- package/dist/hooks/use-ps-scan-live-detection.js +46 -21
- package/dist/hooks/use-ps-scan-quad-tween.d.ts +3 -0
- package/dist/hooks/use-ps-scan-quad-tween.js +38 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/models/ps-document-scanner-page-labels.d.ts +6 -0
- package/dist/models/ps-document-scanner-page-labels.js +2 -0
- package/dist/models/ps-document-scanner-props.d.ts +3 -0
- package/dist/models/ps-document-scanner-sheet-props.d.ts +2 -0
- package/dist/models/ps-drag-pan-handlers.d.ts +8 -0
- package/dist/models/ps-drag-pan-handlers.js +2 -0
- package/dist/models/ps-drag-pan-options.d.ts +9 -0
- package/dist/models/ps-drag-pan-options.js +2 -0
- package/dist/models/ps-scan-detect-profile.d.ts +4 -0
- package/dist/models/ps-scan-detect-profile.js +2 -0
- package/dist/models/ps-scan-live-detection.d.ts +3 -1
- package/dist/models/ps-scan-multi-page.d.ts +9 -0
- package/dist/models/ps-scan-multi-page.js +2 -0
- package/dist/models/ps-scan-page.d.ts +4 -0
- package/dist/models/ps-scan-page.js +2 -0
- package/dist/models/ps-scan-pdf-page.d.ts +5 -0
- package/dist/models/ps-scan-pdf-page.js +2 -0
- package/dist/services/ps-pan-service.d.ts +6 -0
- package/dist/services/ps-pan-service.js +17 -0
- package/dist/services/ps-scan-edge-service.d.ts +2 -1
- package/dist/services/ps-scan-edge-service.js +7 -4
- package/dist/services/ps-scan-file-name-service.d.ts +1 -0
- package/dist/services/ps-scan-file-name-service.js +3 -0
- package/dist/services/ps-scan-frame-service.d.ts +3 -0
- package/dist/services/ps-scan-frame-service.js +14 -0
- package/dist/services/ps-scan-pdf-service.d.ts +10 -0
- package/dist/services/ps-scan-pdf-service.js +73 -0
- package/dist/services/ps-scan-quad-tween-service.d.ts +4 -0
- package/dist/services/ps-scan-quad-tween-service.js +14 -0
- package/package.json +1 -1
|
@@ -5,25 +5,32 @@ import { PallasoftViewportTier, PallasoftZIndex } from '@pallasoft/ps-design-sys
|
|
|
5
5
|
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
6
6
|
import { createPortal } from 'react-dom';
|
|
7
7
|
import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
|
|
8
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
8
9
|
import { PsScanStep } from '../constants/ps-scan-step';
|
|
9
10
|
import { usePsFocusTrap } from '../hooks/use-ps-focus-trap';
|
|
10
11
|
import { usePsScanCamera } from '../hooks/use-ps-scan-camera';
|
|
11
12
|
import { usePsScanLiveDetection } from '../hooks/use-ps-scan-live-detection';
|
|
12
13
|
import { usePsScanPhoto } from '../hooks/use-ps-scan-photo';
|
|
13
14
|
import { PsScanFileNameService } from '../services/ps-scan-file-name-service';
|
|
15
|
+
import { PsScanPdfService } from '../services/ps-scan-pdf-service';
|
|
14
16
|
import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
|
|
15
17
|
import { PsScanCameraView } from './ps-scan-camera-view';
|
|
16
18
|
import { PsScanCropEditor } from './ps-scan-crop-editor';
|
|
17
19
|
import { PsScanIconButton } from './ps-scan-icon-button';
|
|
20
|
+
import { PsScanPageStrip } from './ps-scan-page-strip';
|
|
18
21
|
import { PsScanPermissionView } from './ps-scan-permission-view';
|
|
19
22
|
import { PsScanPreview } from './ps-scan-preview';
|
|
20
|
-
export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, }) => {
|
|
23
|
+
export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }) => {
|
|
21
24
|
const tier = usePsViewport();
|
|
22
25
|
const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
|
|
23
26
|
const content = useRef(null);
|
|
24
27
|
const { clear, detectAgain, load, photo, setCorners } = usePsScanPhoto();
|
|
25
28
|
const [step, setStep] = useState(PsScanStep.Camera);
|
|
26
29
|
const [result, setResult] = useState(null);
|
|
30
|
+
const [pages, setPages] = useState([]);
|
|
31
|
+
const [isAddingPage, setIsAddingPage] = useState(false);
|
|
32
|
+
const pagesRef = useRef([]);
|
|
33
|
+
const nextPageId = useRef(1);
|
|
27
34
|
const [announcement, setAnnouncement] = useState('');
|
|
28
35
|
const [error, setError] = useState('');
|
|
29
36
|
const [isCapturing, setIsCapturing] = useState(false);
|
|
@@ -41,6 +48,12 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
|
|
|
41
48
|
if (result !== null)
|
|
42
49
|
URL.revokeObjectURL(result.url);
|
|
43
50
|
}, [result]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
pagesRef.current = pages;
|
|
53
|
+
}, [pages]);
|
|
54
|
+
useEffect(() => () => {
|
|
55
|
+
pagesRef.current.forEach((page) => URL.revokeObjectURL(page.url));
|
|
56
|
+
}, []);
|
|
44
57
|
const edgeMessage = (isDetected) => isDetected ? labels.edgesFound : labels.edgesNotFound;
|
|
45
58
|
const accept = async (source) => {
|
|
46
59
|
setError('');
|
|
@@ -94,19 +107,91 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
|
|
|
94
107
|
setError(labels.processFailed);
|
|
95
108
|
}
|
|
96
109
|
};
|
|
110
|
+
const multiPageDimension = Math.min(maxDimension, PsScanConfig.multiPageMaxDimension);
|
|
111
|
+
const renderPage = async () => {
|
|
112
|
+
if (photo === null)
|
|
113
|
+
return null;
|
|
114
|
+
const rendered = await PsScanPipelineService.render(photo.source, photo.corners, multiPageDimension, jpegQuality);
|
|
115
|
+
const id = nextPageId.current;
|
|
116
|
+
nextPageId.current += 1;
|
|
117
|
+
return { ...rendered, id, url: URL.createObjectURL(rendered.blob) };
|
|
118
|
+
};
|
|
119
|
+
const addPage = async () => {
|
|
120
|
+
if (result === null || isAddingPage)
|
|
121
|
+
return;
|
|
122
|
+
setIsAddingPage(true);
|
|
123
|
+
setError('');
|
|
124
|
+
try {
|
|
125
|
+
const page = await renderPage();
|
|
126
|
+
if (page === null)
|
|
127
|
+
return;
|
|
128
|
+
setPages((current) => [...current, page]);
|
|
129
|
+
clear();
|
|
130
|
+
setResult(null);
|
|
131
|
+
setStep(PsScanStep.Camera);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
setError(labels.processFailed);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
setIsAddingPage(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const removePage = (id) => {
|
|
141
|
+
const removed = pages.find((page) => page.id === id);
|
|
142
|
+
if (removed !== undefined)
|
|
143
|
+
URL.revokeObjectURL(removed.url);
|
|
144
|
+
setPages((current) => current.filter((page) => page.id !== id));
|
|
145
|
+
};
|
|
146
|
+
const useAsPdf = async () => {
|
|
147
|
+
if (isAddingPage)
|
|
148
|
+
return;
|
|
149
|
+
setIsAddingPage(true);
|
|
150
|
+
setError('');
|
|
151
|
+
try {
|
|
152
|
+
const last = await renderPage();
|
|
153
|
+
if (last === null)
|
|
154
|
+
return;
|
|
155
|
+
const pdf = await PsScanPdfService.build([...pages, last]);
|
|
156
|
+
URL.revokeObjectURL(last.url);
|
|
157
|
+
onComplete([
|
|
158
|
+
new File([pdf], PsScanFileNameService.buildPdf(fileName), { type: 'application/pdf' }),
|
|
159
|
+
]);
|
|
160
|
+
onClose();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
setError(labels.processFailed);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
setIsAddingPage(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
97
169
|
const use = () => {
|
|
98
170
|
if (result === null)
|
|
99
171
|
return;
|
|
172
|
+
if (pages.length > 0) {
|
|
173
|
+
void useAsPdf();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
100
176
|
onComplete([
|
|
101
177
|
new File([result.blob], PsScanFileNameService.build(fileName), { type: 'image/jpeg' }),
|
|
102
178
|
]);
|
|
103
179
|
onClose();
|
|
104
180
|
};
|
|
181
|
+
const multiPage = pageLabels === null
|
|
182
|
+
? undefined
|
|
183
|
+
: {
|
|
184
|
+
isBusy: isAddingPage,
|
|
185
|
+
labels: pageLabels,
|
|
186
|
+
onAddPage: addPage,
|
|
187
|
+
onRemovePage: removePage,
|
|
188
|
+
pages,
|
|
189
|
+
};
|
|
105
190
|
if (typeof document === 'undefined')
|
|
106
191
|
return null;
|
|
107
192
|
return createPortal(_jsx("div", { "aria-label": labels.title, "aria-modal": 'true', className: isDesktop
|
|
108
193
|
? 'fixed inset-0 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm animate-ps-fade-in'
|
|
109
194
|
: 'fixed inset-0 bg-black animate-ps-fade-in', role: 'dialog', style: { zIndex }, children: _jsx(PsLayer, { zIndex: zIndex, children: _jsxs("div", { className: isDesktop
|
|
110
195
|
? 'relative h-[min(52rem,calc(100dvh-3rem))] w-[min(76rem,calc(100vw-3rem))] overflow-hidden rounded-2xl bg-black text-white shadow-2xl ring-1 ring-white/10 animate-ps-scale-in'
|
|
111
|
-
: 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? (_jsx(PsScanCameraView, { canTorch: camera.canTorch, detection: detection, flashKey: flashKey, isBusy: isCapturing, isReady: isReady, isTorchOn: camera.isTorchOn, labels: labels, onCapture: capture, onClose: onClose, onFile: accept, onToggleTorch: camera.toggleTorch, videoRef: camera.videoRef, videoTarget: camera.attachVideo })) : null, isCameraStep && !showsCamera ? (_jsxs(_Fragment, { children: [_jsx(PsScanPermissionView, { labels: labels, onAllow: camera.allow, onFile: accept, status: camera.status }), _jsx("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: _jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }) })] })) : null, step === PsScanStep.Crop && photo !== null ? (_jsx(PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: setCorners, onContinue: flatten, onRetake: retake, photo: photo })) : null, step === PsScanStep.Preview ? (_jsx(PsScanPreview, { error: error, labels: labels, onAdjust: adjust, onRetake: retake, onUse: use, result: result })) : null, isCameraStep && error !== '' ? (_jsx("p", { className: 'absolute inset-x-4 bottom-36 mx-auto max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })) : null] }) }) }), document.body);
|
|
196
|
+
: 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? (_jsx(PsScanCameraView, { canTorch: camera.canTorch, detection: detection, flashKey: flashKey, isBusy: isCapturing, isReady: isReady, isTorchOn: camera.isTorchOn, labels: labels, onCapture: capture, onClose: onClose, onFile: accept, onToggleTorch: camera.toggleTorch, videoRef: camera.videoRef, videoTarget: camera.attachVideo })) : null, isCameraStep && !showsCamera ? (_jsxs(_Fragment, { children: [_jsx(PsScanPermissionView, { labels: labels, onAllow: camera.allow, onFile: accept, status: camera.status }), _jsx("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: _jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }) })] })) : null, step === PsScanStep.Crop && photo !== null ? (_jsx(PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: setCorners, onContinue: flatten, onRetake: retake, photo: photo })) : null, step === PsScanStep.Preview ? (_jsx(PsScanPreview, { error: error, labels: labels, multiPage: multiPage, onAdjust: adjust, onRetake: retake, onUse: use, result: result })) : null, isCameraStep && pageLabels !== null && pages.length > 0 ? (_jsx("div", { className: 'absolute inset-x-0 bottom-32 flex justify-center', children: _jsx(PsScanPageStrip, { labels: pageLabels, onRemove: removePage, pages: pages }) })) : null, isCameraStep && error !== '' ? (_jsx("p", { className: 'absolute inset-x-4 bottom-36 mx-auto max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })) : null] }) }) }), document.body);
|
|
112
197
|
};
|
|
@@ -4,9 +4,9 @@ import { usePsHydrated } from '@pallasoft/ps-core-components';
|
|
|
4
4
|
import { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
|
|
5
5
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
6
6
|
import { PsDocumentScannerLoader } from './ps-document-scanner-loader';
|
|
7
|
-
export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, onClose, onComplete, }) => {
|
|
7
|
+
export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, isMultiPage = false, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, onClose, onComplete, pageLabels, }) => {
|
|
8
8
|
const isHydrated = usePsHydrated();
|
|
9
9
|
if (!isOpen || !isHydrated)
|
|
10
10
|
return null;
|
|
11
|
-
return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, onClose: onClose, onComplete: onComplete }) }));
|
|
11
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, onClose: onClose, onComplete: onComplete, pageLabels: isMultiPage && pageLabels !== undefined ? pageLabels : null }) }));
|
|
12
12
|
};
|
|
@@ -1,29 +1,48 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { usePsDragPan } from '../hooks/use-ps-drag-pan';
|
|
4
|
+
import { PsPanService } from '../services/ps-pan-service';
|
|
3
5
|
export const PsImageViewer = ({ file, labels, rotation, zoom }) => {
|
|
4
6
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
5
7
|
const [hasFailed, setHasFailed] = useState(false);
|
|
6
|
-
const
|
|
8
|
+
const pan = useRef(null);
|
|
9
|
+
const base = useRef({ x: 0, y: 0 });
|
|
10
|
+
const frame = useRef(0);
|
|
11
|
+
const pending = useRef({ x: 0, y: 0 });
|
|
7
12
|
useEffect(() => {
|
|
8
13
|
setOffset({ x: 0, y: 0 });
|
|
9
14
|
setHasFailed(false);
|
|
10
15
|
}, [file.id, zoom === 1]);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
16
|
-
};
|
|
17
|
-
const onPointerMove = (event) => {
|
|
18
|
-
if (drag.current === null)
|
|
19
|
-
return;
|
|
20
|
-
setOffset({ x: event.clientX - drag.current.x, y: event.clientY - drag.current.y });
|
|
16
|
+
useEffect(() => () => cancelAnimationFrame(frame.current), []);
|
|
17
|
+
const write = (next) => {
|
|
18
|
+
if (pan.current !== null)
|
|
19
|
+
pan.current.style.transform = PsPanService.translate(next);
|
|
21
20
|
};
|
|
21
|
+
const handlers = usePsDragPan({
|
|
22
|
+
activationDistance: 0,
|
|
23
|
+
isEnabled: zoom > 1,
|
|
24
|
+
isMouseOnly: false,
|
|
25
|
+
onEnd: (delta) => {
|
|
26
|
+
cancelAnimationFrame(frame.current);
|
|
27
|
+
frame.current = 0;
|
|
28
|
+
const next = PsPanService.add(base.current, delta);
|
|
29
|
+
write(next);
|
|
30
|
+
setOffset(next);
|
|
31
|
+
},
|
|
32
|
+
onMove: (delta) => {
|
|
33
|
+
pending.current = PsPanService.add(base.current, delta);
|
|
34
|
+
if (frame.current !== 0)
|
|
35
|
+
return;
|
|
36
|
+
frame.current = requestAnimationFrame(() => {
|
|
37
|
+
frame.current = 0;
|
|
38
|
+
write(pending.current);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
onStart: () => {
|
|
42
|
+
base.current = offset;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
22
45
|
if (hasFailed)
|
|
23
46
|
return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
|
|
24
|
-
return (_jsx("div", { className: `
|
|
25
|
-
drag.current = null;
|
|
26
|
-
}, children: _jsx("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), src: file.url, style: {
|
|
27
|
-
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
|
|
28
|
-
} }) }));
|
|
47
|
+
return (_jsx("div", { className: `h-full w-full touch-none overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, ...handlers, children: _jsx("div", { className: 'flex h-full w-full items-center justify-center', ref: pan, style: { transform: PsPanService.translate(offset) }, children: _jsx("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), src: file.url, style: { transform: `scale(${zoom}) rotate(${rotation}deg)` } }) }) }));
|
|
29
48
|
};
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { PsSpinner, usePsElementSize } from '@pallasoft/ps-core-components';
|
|
4
4
|
import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
|
|
5
|
+
import { usePsDragPan } from '../hooks/use-ps-drag-pan';
|
|
5
6
|
import { PsPdfService } from '../services/ps-pdf-service';
|
|
6
7
|
import { PsPdfPage } from './ps-pdf-page';
|
|
7
8
|
import { PsPdfThumbnail } from './ps-pdf-thumbnail';
|
|
@@ -13,6 +14,22 @@ export const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorker
|
|
|
13
14
|
const [currentPage, setCurrentPage] = useState(1);
|
|
14
15
|
const [frame, frameSize] = usePsElementSize();
|
|
15
16
|
const scroller = useRef(null);
|
|
17
|
+
const scrollStart = useRef({ x: 0, y: 0 });
|
|
18
|
+
const handlers = usePsDragPan({
|
|
19
|
+
activationDistance: PsFileViewerConfig.dragActivationDistance,
|
|
20
|
+
isEnabled: true,
|
|
21
|
+
isMouseOnly: true,
|
|
22
|
+
onEnd: () => undefined,
|
|
23
|
+
onMove: (delta) => {
|
|
24
|
+
if (scroller.current === null)
|
|
25
|
+
return;
|
|
26
|
+
scroller.current.scrollLeft = scrollStart.current.x - delta.x;
|
|
27
|
+
scroller.current.scrollTop = scrollStart.current.y - delta.y;
|
|
28
|
+
},
|
|
29
|
+
onStart: (element) => {
|
|
30
|
+
scrollStart.current = { x: element.scrollLeft, y: element.scrollTop };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
16
33
|
useEffect(() => {
|
|
17
34
|
let isCurrent = true;
|
|
18
35
|
let opened = null;
|
|
@@ -83,7 +100,7 @@ export const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorker
|
|
|
83
100
|
return Math.max(0.1, fit * zoom);
|
|
84
101
|
}, [frameSize.width, pageWidth, zoom]);
|
|
85
102
|
const pages = document === null ? [] : Array.from({ length: document.numPages }, (_, index) => index + 1);
|
|
86
|
-
return (_jsxs("div", { className: 'flex h-full w-full min-w-0', children: [document !== null && document.numPages > 1 ? (_jsx("nav", { className: 'ps-scrollbar-hidden hidden w-24 shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-white/10 py-3 md:flex', children: pages.map((pageNumber) => (_jsx(PsPdfThumbnail, { document: document, hasMatches: matches.some((match) => match.pageNumber === pageNumber), isCurrent: pageNumber === currentPage, label: labels.page, onSelect: () => scrollToPage(pageNumber), pageNumber: pageNumber, rotation: rotation }, pageNumber))) })) : null, _jsx("div", { className: 'min-w-0 flex-1', ref: frame, children: hasFailed ? (_jsx("p", { className: 'flex h-full items-center justify-center text-sm text-white/70', children: labels.loadFailed })) : document === null ? (_jsx("div", { className: 'flex h-full items-center justify-center text-white', children: _jsx(PsSpinner, { color: 'current', size: 'large' }) })) : (_jsx("div", { className: 'flex h-full flex-col items-center gap-4 overflow-auto px-6 py-6', onScroll: (event) => {
|
|
103
|
+
return (_jsxs("div", { className: 'flex h-full w-full min-w-0', children: [document !== null && document.numPages > 1 ? (_jsx("nav", { className: 'ps-scrollbar-hidden hidden w-24 shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-white/10 py-3 md:flex', children: pages.map((pageNumber) => (_jsx(PsPdfThumbnail, { document: document, hasMatches: matches.some((match) => match.pageNumber === pageNumber), isCurrent: pageNumber === currentPage, label: labels.page, onSelect: () => scrollToPage(pageNumber), pageNumber: pageNumber, rotation: rotation }, pageNumber))) })) : null, _jsx("div", { className: 'min-w-0 flex-1', ref: frame, children: hasFailed ? (_jsx("p", { className: 'flex h-full items-center justify-center text-sm text-white/70', children: labels.loadFailed })) : document === null ? (_jsx("div", { className: 'flex h-full items-center justify-center text-white', children: _jsx(PsSpinner, { color: 'current', size: 'large' }) })) : (_jsx("div", { className: 'flex h-full cursor-grab flex-col items-center gap-4 overflow-auto px-6 py-6 active:cursor-grabbing', ...handlers, onScroll: (event) => {
|
|
87
104
|
const top = event.currentTarget.scrollTop + event.currentTarget.clientHeight / 3;
|
|
88
105
|
const visible = Array.from(event.currentTarget.querySelectorAll('[data-ps-pdf-page]')).find((node) => node.offsetTop + node.offsetHeight > top);
|
|
89
106
|
if (visible)
|
|
@@ -11,11 +11,11 @@ import { PsScanViewfinderOverlay } from './ps-scan-viewfinder-overlay';
|
|
|
11
11
|
export const PsScanCameraView = ({ canTorch, detection, flashKey, isBusy, isReady, isTorchOn, labels, onCapture, onClose, onFile, onToggleTorch, videoRef, videoTarget, }) => {
|
|
12
12
|
const [areaRef, area] = usePsElementSize();
|
|
13
13
|
const [video, setVideo] = useState({ height: 0, width: 0 });
|
|
14
|
-
const hint = detection.
|
|
14
|
+
const hint = detection.hasQuad ? labels.hintFound : labels.hintFind;
|
|
15
15
|
const measure = () => {
|
|
16
16
|
const element = videoRef.current;
|
|
17
17
|
if (element !== null)
|
|
18
18
|
setVideo({ height: element.videoHeight, width: element.videoWidth });
|
|
19
19
|
};
|
|
20
|
-
return (_jsxs("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [_jsxs("div", { className: 'absolute inset-0', ref: areaRef, children: [_jsx("video", { "aria-hidden": 'true', autoPlay: true, className: PallasoftClassNameService.merge('size-full object-cover transition-opacity duration-300', isReady ? 'opacity-100' : 'opacity-0'), muted: true, onLoadedMetadata: measure, onResize: measure, playsInline: true, ref: videoTarget }), isReady ? (_jsx(PsScanViewfinderOverlay, { area: area,
|
|
20
|
+
return (_jsxs("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [_jsxs("div", { className: 'absolute inset-0', ref: areaRef, children: [_jsx("video", { "aria-hidden": 'true', autoPlay: true, className: PallasoftClassNameService.merge('size-full object-cover transition-opacity duration-300', isReady ? 'opacity-100' : 'opacity-0'), muted: true, onLoadedMetadata: measure, onResize: measure, playsInline: true, ref: videoTarget }), isReady ? (_jsx(PsScanViewfinderOverlay, { area: area, hasQuad: detection.hasQuad, quadRef: detection.quadRef, video: video })) : null] }), isReady ? null : (_jsxs("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] })), flashKey > 0 ? (_jsx("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 bg-white animate-ps-fade-out' }, flashKey)) : null, _jsxs("div", { className: 'absolute inset-x-0 top-0 flex items-start justify-between gap-3 bg-gradient-to-b from-black/60 to-transparent px-4 pb-10 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }), _jsx("div", { className: 'flex items-center gap-2', children: canTorch && isReady ? (_jsx(PsScanIconButton, { iconKey: PsIconKey.Flash, isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch })) : null })] }), isReady ? (_jsx("div", { className: 'absolute inset-x-0 top-[calc(max(1rem,env(safe-area-inset-top))+3.75rem)] flex justify-center px-4', children: _jsx("p", { "aria-live": 'polite', className: PsScanChromeClasses.pill, children: hint }) })) : null, _jsxs("div", { className: 'absolute inset-x-0 bottom-0 grid grid-cols-[1fr_auto_1fr] items-center gap-4 bg-gradient-to-t from-black/80 to-transparent px-5 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-12', children: [_jsx("div", { className: 'flex justify-start', children: _jsx(PsScanPhotoButton, { isIconOnly: true, label: labels.choosePhoto, onFile: onFile }) }), _jsx(PsScanShutter, { disabled: !isReady || isBusy, label: labels.capture, onCapture: onCapture }), _jsx("span", { "aria-hidden": 'true' })] })] }));
|
|
21
21
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
3
|
+
import { PsScanTemplateService } from '../services/ps-scan-template-service';
|
|
4
|
+
export const PsScanPageStrip = ({ labels, onRemove, pages }) => (_jsx("ul", { className: 'ps-scrollbar-hidden flex min-w-0 items-center justify-center gap-3 overflow-x-auto px-4 py-2', children: pages.map((page, index) => (_jsxs("li", { className: 'relative shrink-0', children: [_jsx("img", { alt: PsScanTemplateService.fill(labels.pageLabel, { n: index + 1 }), className: 'h-16 w-auto rounded-sm bg-white object-contain ring-1 ring-white/25', src: page.url }), _jsx("button", { "aria-label": PsScanTemplateService.fill(labels.removePage, { n: index + 1 }), className: 'ps-focus-ring ps-control-square ps-control-compact absolute -right-2 -top-2 inline-flex items-center justify-center rounded-ps-control bg-black/80 text-white transition hover:bg-black', onClick: () => onRemove(page.id), type: 'button', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Close }) })] }, page.id))) }));
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { PsSpinner } from '@pallasoft/ps-core-components';
|
|
3
3
|
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
4
|
+
import { PsScanTemplateService } from '../services/ps-scan-template-service';
|
|
4
5
|
import { PsScanButton } from './ps-scan-button';
|
|
5
6
|
import { PsScanIconButton } from './ps-scan-icon-button';
|
|
6
|
-
|
|
7
|
+
import { PsScanPageStrip } from './ps-scan-page-strip';
|
|
8
|
+
export const PsScanPreview = ({ error, labels, multiPage, onAdjust, onRetake, onUse, result, }) => (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [_jsxs("header", { className: 'grid shrink-0 grid-cols-[2.75rem_1fr_2.75rem] items-center gap-3 px-4 pb-1 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Crop, label: labels.cropTitle, onClick: onAdjust }), _jsx("h2", { className: 'truncate text-center text-base font-semibold', children: labels.previewTitle }), _jsx("span", { "aria-hidden": 'true' })] }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-6 py-4', children: result === null ? (_jsxs("div", { className: 'flex flex-col items-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.preparing, size: 'small' }), _jsx("span", { children: labels.preparing })] })) : (_jsx("img", { alt: labels.previewAlt, className: 'max-h-full max-w-full rounded-sm bg-white object-contain shadow-[0_24px_60px_-20px_rgb(0_0_0/0.9)] animate-ps-fade-in', src: result.url })) }), error === '' ? null : (_jsx("p", { className: 'mx-auto mb-2 max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })), multiPage === undefined || multiPage.pages.length === 0 ? null : (_jsxs("div", { className: 'shrink-0', children: [_jsx(PsScanPageStrip, { labels: multiPage.labels, onRemove: multiPage.onRemovePage, pages: multiPage.pages }), _jsx("p", { className: 'text-center text-sm text-white/75', children: PsScanTemplateService.fill(multiPage.labels.pageCount, {
|
|
9
|
+
count: multiPage.pages.length + 1,
|
|
10
|
+
}) })] })), _jsxs("footer", { className: 'mx-auto grid w-full max-w-md shrink-0 grid-cols-2 gap-3 px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-2', children: [multiPage === undefined ? null : (_jsx(PsScanButton, { className: 'col-span-2', disabled: result === null, isLoading: multiPage.isBusy, isLarge: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Plus }), onClick: multiPage.onAddPage, children: multiPage.labels.addPage })), _jsx(PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), _jsx(PsScanButton, { disabled: result === null, isLarge: true, isPrimary: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Check }), onClick: onUse, children: labels.useScan })] })] }));
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
3
|
+
import { usePsScanQuadTween } from '../hooks/use-ps-scan-quad-tween';
|
|
3
4
|
import { PsScanGeometryService } from '../services/ps-scan-geometry-service';
|
|
4
|
-
export const PsScanViewfinderOverlay = ({ area,
|
|
5
|
-
if (area.width <= 0 || area.height <= 0)
|
|
6
|
-
return null;
|
|
5
|
+
export const PsScanViewfinderOverlay = ({ area, hasQuad, quadRef, video, }) => {
|
|
7
6
|
const box = PsScanGeometryService.cover(area, video);
|
|
8
7
|
const left = (area.width - box.width) / 2;
|
|
9
8
|
const top = (area.height - box.height) / 2;
|
|
9
|
+
const polygon = usePsScanQuadTween(quadRef, (quad) => quad.map((point) => `${left + point.x * box.width},${top + point.y * box.height}`).join(' '));
|
|
10
|
+
if (area.width <= 0 || area.height <= 0)
|
|
11
|
+
return null;
|
|
10
12
|
const frameHeight = Math.min(area.height * PsScanConfig.guideHeightRatio, (area.width * PsScanConfig.guideWidthRatio) / PsScanConfig.guideAspect);
|
|
11
13
|
const frameWidth = frameHeight * PsScanConfig.guideAspect;
|
|
12
14
|
const frameLeft = (area.width - frameWidth) / 2;
|
|
@@ -18,7 +20,5 @@ export const PsScanViewfinderOverlay = ({ area, quad, video }) => {
|
|
|
18
20
|
`M${frameLeft + frameWidth} ${frameTop + frameHeight - arm}V${frameTop + frameHeight}H${frameLeft + frameWidth - arm}`,
|
|
19
21
|
`M${frameLeft + arm} ${frameTop + frameHeight}H${frameLeft}V${frameTop + frameHeight - arm}`,
|
|
20
22
|
].join('');
|
|
21
|
-
return (_jsx("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', viewBox: `0 0 ${area.width} ${area.height}`, children:
|
|
22
|
-
.map((point) => `${left + point.x * box.width},${top + point.y * box.height}`)
|
|
23
|
-
.join(' '), strokeLinejoin: 'round', strokeWidth: '3' })) }));
|
|
23
|
+
return (_jsx("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', viewBox: `0 0 ${area.width} ${area.height}`, children: !hasQuad || box.width === 0 ? (_jsx("path", { className: 'stroke-white/75 animate-ps-fade-in', d: corners, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: '4' })) : (_jsx("polygon", { className: 'fill-primary/10 stroke-primary', ref: polygon, strokeLinejoin: 'round', strokeWidth: '3' })) }));
|
|
24
24
|
};
|
|
@@ -27,14 +27,18 @@ export const PsScanConfig = {
|
|
|
27
27
|
guideLift: 0.03,
|
|
28
28
|
guideWidthRatio: 0.76,
|
|
29
29
|
liveDeadzone: 0.006,
|
|
30
|
-
|
|
30
|
+
liveCloseRadii: [2],
|
|
31
|
+
liveDetectMinIntervalMs: 70,
|
|
31
32
|
liveDetectMaxDimension: 280,
|
|
32
|
-
liveMissLimit:
|
|
33
|
-
liveSmoothing: 0.
|
|
33
|
+
liveMissLimit: 1,
|
|
34
|
+
liveSmoothing: 0.68,
|
|
35
|
+
liveTweenMs: 55,
|
|
34
36
|
loupeOffset: 72,
|
|
35
37
|
loupeSize: 112,
|
|
36
38
|
loupeZoom: 2.5,
|
|
37
39
|
minCornerGap: 0.02,
|
|
40
|
+
multiPageMaxDimension: 2400,
|
|
41
|
+
pdfLongEdgePoints: 842,
|
|
38
42
|
polygonEpsilons: [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07],
|
|
39
43
|
torchConstraint: 'torch',
|
|
40
44
|
warpChunkPixels: 400000,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { PsPanService } from '../services/ps-pan-service';
|
|
3
|
+
export const usePsDragPan = ({ activationDistance, isEnabled, isMouseOnly, onEnd, onMove, onStart, }) => {
|
|
4
|
+
const origin = useRef(null);
|
|
5
|
+
const pointerId = useRef(-1);
|
|
6
|
+
const isActive = useRef(false);
|
|
7
|
+
const last = useRef({ x: 0, y: 0 });
|
|
8
|
+
const activate = (element) => {
|
|
9
|
+
isActive.current = true;
|
|
10
|
+
last.current = { x: 0, y: 0 };
|
|
11
|
+
if (typeof element.setPointerCapture === 'function')
|
|
12
|
+
element.setPointerCapture(pointerId.current);
|
|
13
|
+
element.style.userSelect = 'none';
|
|
14
|
+
window.getSelection()?.removeAllRanges();
|
|
15
|
+
onStart(element);
|
|
16
|
+
};
|
|
17
|
+
const finish = (element) => {
|
|
18
|
+
if (origin.current === null)
|
|
19
|
+
return;
|
|
20
|
+
const wasActive = isActive.current;
|
|
21
|
+
origin.current = null;
|
|
22
|
+
isActive.current = false;
|
|
23
|
+
element.style.userSelect = '';
|
|
24
|
+
if (wasActive)
|
|
25
|
+
onEnd(last.current);
|
|
26
|
+
};
|
|
27
|
+
const onPointerDown = (event) => {
|
|
28
|
+
if (!isEnabled || origin.current !== null)
|
|
29
|
+
return;
|
|
30
|
+
if (isMouseOnly && event.pointerType === 'touch')
|
|
31
|
+
return;
|
|
32
|
+
if (event.pointerType === 'mouse' && event.button !== 0)
|
|
33
|
+
return;
|
|
34
|
+
if (PsPanService.isOnScrollbar(event.currentTarget, event.clientX, event.clientY))
|
|
35
|
+
return;
|
|
36
|
+
origin.current = { x: event.clientX, y: event.clientY };
|
|
37
|
+
pointerId.current = event.pointerId;
|
|
38
|
+
isActive.current = false;
|
|
39
|
+
if (activationDistance <= 0)
|
|
40
|
+
activate(event.currentTarget);
|
|
41
|
+
};
|
|
42
|
+
const onPointerMove = (event) => {
|
|
43
|
+
if (origin.current === null || event.pointerId !== pointerId.current)
|
|
44
|
+
return;
|
|
45
|
+
const delta = { x: event.clientX - origin.current.x, y: event.clientY - origin.current.y };
|
|
46
|
+
if (!isActive.current) {
|
|
47
|
+
if (Math.hypot(delta.x, delta.y) < activationDistance)
|
|
48
|
+
return;
|
|
49
|
+
activate(event.currentTarget);
|
|
50
|
+
}
|
|
51
|
+
last.current = delta;
|
|
52
|
+
onMove(delta);
|
|
53
|
+
};
|
|
54
|
+
const onPointerEnd = (event) => {
|
|
55
|
+
if (event.pointerId !== pointerId.current)
|
|
56
|
+
return;
|
|
57
|
+
finish(event.currentTarget);
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
onLostPointerCapture: onPointerEnd,
|
|
61
|
+
onPointerCancel: onPointerEnd,
|
|
62
|
+
onPointerDown,
|
|
63
|
+
onPointerMove,
|
|
64
|
+
onPointerUp: onPointerEnd,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -1,41 +1,66 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
3
|
+
import { PsScanDetectProfiles } from '../constants/ps-scan-detect-profiles';
|
|
3
4
|
import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
|
|
4
5
|
import { PsScanEdgeService } from '../services/ps-scan-edge-service';
|
|
6
|
+
import { PsScanFrameService } from '../services/ps-scan-frame-service';
|
|
5
7
|
import { PsScanStabilityService } from '../services/ps-scan-stability-service';
|
|
6
8
|
export const usePsScanLiveDetection = (videoRef, isActive) => {
|
|
7
|
-
const [
|
|
9
|
+
const [hasQuad, setHasQuad] = useState(false);
|
|
10
|
+
const quadRef = useRef(null);
|
|
8
11
|
useEffect(() => {
|
|
12
|
+
quadRef.current = null;
|
|
9
13
|
if (!isActive) {
|
|
10
|
-
|
|
14
|
+
setHasQuad(false);
|
|
11
15
|
return;
|
|
12
16
|
}
|
|
13
17
|
let isCancelled = false;
|
|
18
|
+
let isRunning = false;
|
|
19
|
+
let lastStart = 0;
|
|
14
20
|
let state = PsScanStabilityService.initial();
|
|
15
|
-
let
|
|
16
|
-
const
|
|
21
|
+
let cancelNext = () => undefined;
|
|
22
|
+
const schedule = () => {
|
|
23
|
+
if (!isCancelled)
|
|
24
|
+
cancelNext = PsScanFrameService.schedule(videoRef.current, run);
|
|
25
|
+
};
|
|
26
|
+
const run = async () => {
|
|
17
27
|
const video = videoRef.current;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
catch {
|
|
27
|
-
state = PsScanStabilityService.initial();
|
|
28
|
-
}
|
|
29
|
-
setDetection((current) => (current.quad === state.quad ? current : { quad: state.quad }));
|
|
28
|
+
const now = performance.now();
|
|
29
|
+
if (isRunning ||
|
|
30
|
+
video === null ||
|
|
31
|
+
video.videoWidth === 0 ||
|
|
32
|
+
document.visibilityState !== 'visible' ||
|
|
33
|
+
now - lastStart < PsScanConfig.liveDetectMinIntervalMs) {
|
|
34
|
+
schedule();
|
|
35
|
+
return;
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
isRunning = true;
|
|
38
|
+
lastStart = now;
|
|
39
|
+
try {
|
|
40
|
+
const raster = PsScanCanvasService.sample(video, PsScanConfig.liveDetectMaxDimension);
|
|
41
|
+
const quad = await PsScanEdgeService.detect(raster, PsScanConfig.liveDetectMaxDimension, PsScanDetectProfiles.live);
|
|
42
|
+
if (isCancelled)
|
|
43
|
+
return;
|
|
44
|
+
state = PsScanStabilityService.next(state, quad);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
state = PsScanStabilityService.initial();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
isRunning = false;
|
|
51
|
+
}
|
|
52
|
+
if (isCancelled)
|
|
53
|
+
return;
|
|
54
|
+
quadRef.current = state.quad;
|
|
55
|
+
setHasQuad(state.quad !== null);
|
|
56
|
+
schedule();
|
|
33
57
|
};
|
|
34
|
-
|
|
58
|
+
schedule();
|
|
35
59
|
return () => {
|
|
36
60
|
isCancelled = true;
|
|
37
|
-
|
|
61
|
+
cancelNext();
|
|
62
|
+
quadRef.current = null;
|
|
38
63
|
};
|
|
39
64
|
}, [isActive]);
|
|
40
|
-
return
|
|
65
|
+
return { hasQuad, quadRef };
|
|
41
66
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { PsScanQuadTweenService } from '../services/ps-scan-quad-tween-service';
|
|
3
|
+
export const usePsScanQuadTween = (quadRef, format) => {
|
|
4
|
+
const polygon = useRef(null);
|
|
5
|
+
const formatRef = useRef(format);
|
|
6
|
+
formatRef.current = format;
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let displayed = null;
|
|
9
|
+
let lastTime = performance.now();
|
|
10
|
+
let lastPoints = '';
|
|
11
|
+
let handle = 0;
|
|
12
|
+
const frame = (time) => {
|
|
13
|
+
const target = quadRef.current;
|
|
14
|
+
const elapsed = time - lastTime;
|
|
15
|
+
lastTime = time;
|
|
16
|
+
if (target === null)
|
|
17
|
+
displayed = null;
|
|
18
|
+
else
|
|
19
|
+
displayed =
|
|
20
|
+
displayed === null ? target : PsScanQuadTweenService.step(displayed, target, elapsed);
|
|
21
|
+
if (displayed !== null && polygon.current !== null) {
|
|
22
|
+
const points = formatRef.current(displayed);
|
|
23
|
+
if (points !== lastPoints) {
|
|
24
|
+
lastPoints = points;
|
|
25
|
+
polygon.current.setAttribute('points', points);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
handle = requestAnimationFrame(frame);
|
|
29
|
+
};
|
|
30
|
+
handle = requestAnimationFrame(frame);
|
|
31
|
+
return () => cancelAnimationFrame(handle);
|
|
32
|
+
}, [quadRef]);
|
|
33
|
+
return polygon;
|
|
34
|
+
};
|
package/dist/esm/index.js
CHANGED
|
@@ -33,6 +33,8 @@ export * from './constants/ps-upload-messages';
|
|
|
33
33
|
export * from './constants/ps-upload-queue-config';
|
|
34
34
|
export * from './constants/ps-upload-status-tones';
|
|
35
35
|
export * from './constants/ps-upload-status';
|
|
36
|
+
export * from './models/ps-drag-pan-handlers';
|
|
37
|
+
export * from './models/ps-drag-pan-options';
|
|
36
38
|
export * from './models/ps-file-icon-button-tone';
|
|
37
39
|
export * from './models/ps-file-rejection';
|
|
38
40
|
export * from './models/ps-file-selection-rules';
|
|
@@ -66,10 +68,12 @@ export * from './services/ps-text-search-service';
|
|
|
66
68
|
export * from './services/ps-upload-queue-service';
|
|
67
69
|
export * from './services/ps-xhr-upload-service';
|
|
68
70
|
export * from './hooks/use-ps-active-match-scroll';
|
|
71
|
+
export * from './hooks/use-ps-drag-pan';
|
|
69
72
|
export * from './hooks/use-ps-file-text';
|
|
70
73
|
export * from './hooks/use-ps-in-view';
|
|
71
74
|
export * from './hooks/use-ps-upload-queue';
|
|
72
75
|
export * from './components/ps-document-scanner';
|
|
73
76
|
export * from './constants/ps-document-scanner-labels';
|
|
77
|
+
export * from './models/ps-document-scanner-page-labels';
|
|
74
78
|
export * from './models/ps-document-scanner-props';
|
|
75
79
|
export * from './models/ps-file-picker-scan';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|