@pallasoft/ps-file-components 1.2.4 → 1.4.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 -0
- package/dist/components/ps-document-scanner-loader.js +40 -0
- package/dist/components/ps-document-scanner-sheet.d.ts +2 -0
- package/dist/components/ps-document-scanner-sheet.js +138 -0
- package/dist/components/ps-document-scanner.d.ts +2 -0
- package/dist/components/ps-document-scanner.js +17 -0
- package/dist/components/ps-file-picker.d.ts +2 -1
- package/dist/components/ps-file-picker.js +2 -2
- package/dist/components/ps-file-viewer-pane.d.ts +2 -0
- package/dist/components/ps-file-viewer-pane.js +39 -0
- package/dist/components/ps-scan-camera-view.d.ts +17 -0
- package/dist/components/ps-scan-camera-view.js +27 -0
- package/dist/components/ps-scan-crop-editor.d.ts +13 -0
- package/dist/components/ps-scan-crop-editor.js +53 -0
- package/dist/components/ps-scan-icon-button.d.ts +10 -0
- package/dist/components/ps-scan-icon-button.js +7 -0
- package/dist/components/ps-scan-page-list.d.ts +12 -0
- package/dist/components/ps-scan-page-list.js +23 -0
- package/dist/components/ps-scan-review.d.ts +23 -0
- package/dist/components/ps-scan-review.js +20 -0
- package/dist/constants/ps-document-scanner-labels.d.ts +50 -0
- package/dist/constants/ps-document-scanner-labels.js +53 -0
- package/dist/constants/ps-file-picker-labels.d.ts +1 -0
- package/dist/constants/ps-file-picker-labels.js +1 -0
- package/dist/constants/ps-scan-camera-status.d.ts +7 -0
- package/dist/constants/ps-scan-camera-status.js +9 -0
- package/dist/constants/ps-scan-config.d.ts +31 -0
- package/dist/constants/ps-scan-config.js +34 -0
- package/dist/constants/ps-scan-corner-order.d.ts +1 -0
- package/dist/constants/ps-scan-corner-order.js +4 -0
- package/dist/constants/ps-scan-filter.d.ts +7 -0
- package/dist/constants/ps-scan-filter.js +9 -0
- package/dist/constants/ps-scan-output-format.d.ts +5 -0
- package/dist/constants/ps-scan-output-format.js +7 -0
- package/dist/constants/ps-scan-step.d.ts +6 -0
- package/dist/constants/ps-scan-step.js +8 -0
- package/dist/esm/components/ps-document-scanner-loader.js +4 -0
- package/dist/esm/components/ps-document-scanner-sheet.js +134 -0
- package/dist/esm/components/ps-document-scanner.js +13 -0
- package/dist/esm/components/ps-file-picker.js +3 -3
- package/dist/esm/components/ps-file-viewer-pane.js +35 -0
- package/dist/esm/components/ps-scan-camera-view.js +23 -0
- package/dist/esm/components/ps-scan-crop-editor.js +49 -0
- package/dist/esm/components/ps-scan-icon-button.js +3 -0
- package/dist/esm/components/ps-scan-page-list.js +19 -0
- package/dist/esm/components/ps-scan-review.js +16 -0
- package/dist/esm/constants/ps-document-scanner-labels.js +50 -0
- package/dist/esm/constants/ps-file-picker-labels.js +1 -0
- package/dist/esm/constants/ps-scan-camera-status.js +6 -0
- package/dist/esm/constants/ps-scan-config.js +31 -0
- package/dist/esm/constants/ps-scan-corner-order.js +1 -0
- package/dist/esm/constants/ps-scan-filter.js +6 -0
- package/dist/esm/constants/ps-scan-output-format.js +4 -0
- package/dist/esm/constants/ps-scan-step.js +5 -0
- package/dist/esm/hooks/use-ps-focus-trap.js +45 -0
- package/dist/esm/hooks/use-ps-scan-camera.js +62 -0
- package/dist/esm/hooks/use-ps-scan-pages.js +62 -0
- package/dist/esm/hooks/use-ps-scan-previews.js +57 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/models/ps-document-scanner-props.js +1 -0
- package/dist/esm/models/ps-document-scanner-sheet-props.js +1 -0
- package/dist/esm/models/ps-file-viewer-pane-props.js +1 -0
- package/dist/esm/models/ps-scan-added-page.js +1 -0
- package/dist/esm/models/ps-scan-camera-session.js +1 -0
- package/dist/esm/models/ps-scan-decoded-image.js +1 -0
- package/dist/esm/models/ps-scan-levels.js +1 -0
- package/dist/esm/models/ps-scan-output-options.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/models/ps-scan-prepared.js +1 -0
- package/dist/esm/models/ps-scan-quad.js +1 -0
- package/dist/esm/models/ps-scan-raster.js +1 -0
- package/dist/esm/models/ps-scan-rendered-page.js +1 -0
- package/dist/esm/services/ps-scan-camera-service.js +50 -0
- package/dist/esm/services/ps-scan-canvas-service.js +46 -0
- package/dist/esm/services/ps-scan-edge-service.js +156 -0
- package/dist/esm/services/ps-scan-enhance-service.js +97 -0
- package/dist/esm/services/ps-scan-file-name-service.js +25 -0
- package/dist/esm/services/ps-scan-geometry-service.js +96 -0
- package/dist/esm/services/ps-scan-homography-service.js +46 -0
- package/dist/esm/services/ps-scan-hull-service.js +59 -0
- package/dist/esm/services/ps-scan-image-filter-service.js +52 -0
- package/dist/esm/services/ps-scan-order-service.js +13 -0
- package/dist/esm/services/ps-scan-output-service.js +24 -0
- package/dist/esm/services/ps-scan-pdf-service.js +56 -0
- package/dist/esm/services/ps-scan-pipeline-service.js +40 -0
- package/dist/esm/services/ps-scan-raster-service.js +67 -0
- package/dist/esm/services/ps-scan-template-service.js +5 -0
- package/dist/esm/services/ps-scan-warp-service.js +35 -0
- package/dist/hooks/use-ps-focus-trap.d.ts +2 -0
- package/dist/hooks/use-ps-focus-trap.js +49 -0
- package/dist/hooks/use-ps-scan-camera.d.ts +9 -0
- package/dist/hooks/use-ps-scan-camera.js +66 -0
- package/dist/hooks/use-ps-scan-pages.d.ts +12 -0
- package/dist/hooks/use-ps-scan-pages.js +66 -0
- package/dist/hooks/use-ps-scan-previews.d.ts +2 -0
- package/dist/hooks/use-ps-scan-previews.js +61 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/models/ps-document-scanner-props.d.ts +15 -0
- package/dist/models/ps-document-scanner-props.js +2 -0
- package/dist/models/ps-document-scanner-sheet-props.d.ts +13 -0
- package/dist/models/ps-document-scanner-sheet-props.js +2 -0
- package/dist/models/ps-file-viewer-pane-props.d.ts +12 -0
- package/dist/models/ps-file-viewer-pane-props.js +2 -0
- package/dist/models/ps-scan-added-page.d.ts +5 -0
- package/dist/models/ps-scan-added-page.js +2 -0
- package/dist/models/ps-scan-camera-session.d.ts +5 -0
- package/dist/models/ps-scan-camera-session.js +2 -0
- package/dist/models/ps-scan-decoded-image.d.ts +6 -0
- package/dist/models/ps-scan-decoded-image.js +2 -0
- package/dist/models/ps-scan-levels.d.ts +4 -0
- package/dist/models/ps-scan-levels.js +2 -0
- package/dist/models/ps-scan-output-options.d.ts +7 -0
- package/dist/models/ps-scan-output-options.js +2 -0
- package/dist/models/ps-scan-page.d.ts +12 -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/models/ps-scan-prepared.d.ts +7 -0
- package/dist/models/ps-scan-prepared.js +2 -0
- package/dist/models/ps-scan-quad.d.ts +2 -0
- package/dist/models/ps-scan-quad.js +2 -0
- package/dist/models/ps-scan-raster.d.ts +5 -0
- package/dist/models/ps-scan-raster.js +2 -0
- package/dist/models/ps-scan-rendered-page.d.ts +5 -0
- package/dist/models/ps-scan-rendered-page.js +2 -0
- package/dist/services/ps-scan-camera-service.d.ts +10 -0
- package/dist/services/ps-scan-camera-service.js +54 -0
- package/dist/services/ps-scan-canvas-service.d.ts +8 -0
- package/dist/services/ps-scan-canvas-service.js +50 -0
- package/dist/services/ps-scan-edge-service.d.ts +9 -0
- package/dist/services/ps-scan-edge-service.js +160 -0
- package/dist/services/ps-scan-enhance-service.d.ts +9 -0
- package/dist/services/ps-scan-enhance-service.js +101 -0
- package/dist/services/ps-scan-file-name-service.d.ts +6 -0
- package/dist/services/ps-scan-file-name-service.js +29 -0
- package/dist/services/ps-scan-geometry-service.d.ts +18 -0
- package/dist/services/ps-scan-geometry-service.js +100 -0
- package/dist/services/ps-scan-homography-service.d.ts +7 -0
- package/dist/services/ps-scan-homography-service.js +50 -0
- package/dist/services/ps-scan-hull-service.d.ts +6 -0
- package/dist/services/ps-scan-hull-service.js +63 -0
- package/dist/services/ps-scan-image-filter-service.d.ts +6 -0
- package/dist/services/ps-scan-image-filter-service.js +56 -0
- package/dist/services/ps-scan-order-service.d.ts +4 -0
- package/dist/services/ps-scan-order-service.js +17 -0
- package/dist/services/ps-scan-output-service.d.ts +5 -0
- package/dist/services/ps-scan-output-service.js +28 -0
- package/dist/services/ps-scan-pdf-service.d.ts +5 -0
- package/dist/services/ps-scan-pdf-service.js +60 -0
- package/dist/services/ps-scan-pipeline-service.d.ts +10 -0
- package/dist/services/ps-scan-pipeline-service.js +44 -0
- package/dist/services/ps-scan-raster-service.d.ts +6 -0
- package/dist/services/ps-scan-raster-service.js +71 -0
- package/dist/services/ps-scan-template-service.d.ts +3 -0
- package/dist/services/ps-scan-template-service.js +9 -0
- package/dist/services/ps-scan-warp-service.d.ts +6 -0
- package/dist/services/ps-scan-warp-service.js +39 -0
- package/package.json +9 -6
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PsDocumentScannerLabels = void 0;
|
|
4
|
+
exports.PsDocumentScannerLabels = {
|
|
5
|
+
addPage: 'Add page',
|
|
6
|
+
autoDetect: 'Auto-detect edges',
|
|
7
|
+
cameraDenied: 'Camera access was blocked. Allow it in your browser settings, or take a photo or choose existing photos instead.',
|
|
8
|
+
cameraStarting: 'Starting camera',
|
|
9
|
+
cameraUnavailable: 'No camera is available here. Take a photo or choose existing photos instead.',
|
|
10
|
+
backToPages: 'Back to pages',
|
|
11
|
+
capture: 'Capture page',
|
|
12
|
+
choosePhotos: 'Choose photos',
|
|
13
|
+
close: 'Close scanner',
|
|
14
|
+
cornerBottomLeft: 'Bottom left corner',
|
|
15
|
+
cornerBottomRight: 'Bottom right corner',
|
|
16
|
+
cornerTopLeft: 'Top left corner',
|
|
17
|
+
cornerTopRight: 'Top right corner',
|
|
18
|
+
cropApply: 'Use crop',
|
|
19
|
+
cropHint: 'Drag the corners to fit the page, or use the arrow keys on a focused corner.',
|
|
20
|
+
cropTitle: 'Adjust edges',
|
|
21
|
+
deletePage: 'Delete page',
|
|
22
|
+
dragPage: 'Drag {page} to reorder',
|
|
23
|
+
editCrop: 'Edit edges',
|
|
24
|
+
edgesFound: 'Page edges found.',
|
|
25
|
+
edgesNotFound: 'Edges not found. Adjust the corners.',
|
|
26
|
+
filterColour: 'Colour',
|
|
27
|
+
filterDocument: 'Black and white',
|
|
28
|
+
filterGreyscale: 'Greyscale',
|
|
29
|
+
filterOriginal: 'Original',
|
|
30
|
+
filters: 'Enhance',
|
|
31
|
+
finish: 'Use scan',
|
|
32
|
+
finishing: 'Preparing your scan',
|
|
33
|
+
maxPages: 'You reached the page limit.',
|
|
34
|
+
moveDown: 'Move {page} down',
|
|
35
|
+
moveUp: 'Move {page} up',
|
|
36
|
+
page: 'Page',
|
|
37
|
+
pageAdded: 'Page {position} added.',
|
|
38
|
+
pageDeleted: 'Page deleted.',
|
|
39
|
+
pageMoved: '{page} moved to position {position} of {count}.',
|
|
40
|
+
pages: 'Pages',
|
|
41
|
+
processFailed: 'The scan could not be prepared. Try again.',
|
|
42
|
+
reorderCancelled: '{page} stays where it was.',
|
|
43
|
+
reorderDropped: '{page} dropped at {position} of {count}.',
|
|
44
|
+
reorderInstructions: 'Press space to lift a page, the arrow keys to move it and space to drop it.',
|
|
45
|
+
reorderLifted: '{page} lifted from {position} of {count}.',
|
|
46
|
+
reorderMoved: '{page} is now {position} of {count}.',
|
|
47
|
+
retake: 'Retake',
|
|
48
|
+
rotate: 'Rotate',
|
|
49
|
+
takePhoto: 'Take photo',
|
|
50
|
+
title: 'Scan document',
|
|
51
|
+
torchOff: 'Turn light off',
|
|
52
|
+
torchOn: 'Turn light on',
|
|
53
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const PsFilePickerLabels: {
|
|
2
2
|
readonly browse: "or click to browse your files";
|
|
3
|
+
readonly browseTouch: "or tap to choose from your device";
|
|
3
4
|
readonly dropActive: "Drop to upload";
|
|
4
5
|
readonly formats: "Accepted formats:";
|
|
5
6
|
readonly invalidType: "is not a supported file type.";
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PsFilePickerLabels = void 0;
|
|
4
4
|
exports.PsFilePickerLabels = {
|
|
5
5
|
browse: 'or click to browse your files',
|
|
6
|
+
browseTouch: 'or tap to choose from your device',
|
|
6
7
|
dropActive: 'Drop to upload',
|
|
7
8
|
formats: 'Accepted formats:',
|
|
8
9
|
invalidType: 'is not a supported file type.',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const PsScanCameraStatus: {
|
|
2
|
+
readonly Denied: "denied";
|
|
3
|
+
readonly Ready: "ready";
|
|
4
|
+
readonly Starting: "starting";
|
|
5
|
+
readonly Unavailable: "unavailable";
|
|
6
|
+
};
|
|
7
|
+
export type PsScanCameraStatus = (typeof PsScanCameraStatus)[keyof typeof PsScanCameraStatus];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const PsScanConfig: {
|
|
2
|
+
readonly adaptiveRatio: 0.92;
|
|
3
|
+
readonly adaptiveWindowDivisor: 16;
|
|
4
|
+
readonly adaptiveWindowMin: 15;
|
|
5
|
+
readonly capturedJpegQuality: 0.92;
|
|
6
|
+
readonly cornerNudge: 0.005;
|
|
7
|
+
readonly cornerNudgeLarge: 0.025;
|
|
8
|
+
readonly decodeMaxDimension: 3000;
|
|
9
|
+
readonly defaultFileName: "scan";
|
|
10
|
+
readonly defaultJpegQuality: 0.82;
|
|
11
|
+
readonly defaultMaxDimension: 2000;
|
|
12
|
+
readonly defaultMaxPages: 30;
|
|
13
|
+
readonly detectDilations: 2;
|
|
14
|
+
readonly detectMaxDimension: 320;
|
|
15
|
+
readonly detectMinAreaRatio: 0.12;
|
|
16
|
+
readonly detectMaxAreaRatio: 0.995;
|
|
17
|
+
readonly detectMinEdgeStrength: 24;
|
|
18
|
+
readonly detectMinHullFill: 0.85;
|
|
19
|
+
readonly detectPercentile: 0.98;
|
|
20
|
+
readonly detectRelativeStrength: 0.3;
|
|
21
|
+
readonly hullCandidateLimit: 18;
|
|
22
|
+
readonly levelHighPercentile: 0.99;
|
|
23
|
+
readonly levelLowPercentile: 0.01;
|
|
24
|
+
readonly minCornerGap: 0.02;
|
|
25
|
+
readonly pdfDpi: 150;
|
|
26
|
+
readonly pdfPointsPerInch: 72;
|
|
27
|
+
readonly previewDebounceMs: 120;
|
|
28
|
+
readonly previewMaxDimension: 1400;
|
|
29
|
+
readonly thumbnailHeight: 56;
|
|
30
|
+
readonly torchConstraint: "torch";
|
|
31
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PsScanConfig = void 0;
|
|
4
|
+
exports.PsScanConfig = {
|
|
5
|
+
adaptiveRatio: 0.92,
|
|
6
|
+
adaptiveWindowDivisor: 16,
|
|
7
|
+
adaptiveWindowMin: 15,
|
|
8
|
+
capturedJpegQuality: 0.92,
|
|
9
|
+
cornerNudge: 0.005,
|
|
10
|
+
cornerNudgeLarge: 0.025,
|
|
11
|
+
decodeMaxDimension: 3000,
|
|
12
|
+
defaultFileName: 'scan',
|
|
13
|
+
defaultJpegQuality: 0.82,
|
|
14
|
+
defaultMaxDimension: 2000,
|
|
15
|
+
defaultMaxPages: 30,
|
|
16
|
+
detectDilations: 2,
|
|
17
|
+
detectMaxDimension: 320,
|
|
18
|
+
detectMinAreaRatio: 0.12,
|
|
19
|
+
detectMaxAreaRatio: 0.995,
|
|
20
|
+
detectMinEdgeStrength: 24,
|
|
21
|
+
detectMinHullFill: 0.85,
|
|
22
|
+
detectPercentile: 0.98,
|
|
23
|
+
detectRelativeStrength: 0.3,
|
|
24
|
+
hullCandidateLimit: 18,
|
|
25
|
+
levelHighPercentile: 0.99,
|
|
26
|
+
levelLowPercentile: 0.01,
|
|
27
|
+
minCornerGap: 0.02,
|
|
28
|
+
pdfDpi: 150,
|
|
29
|
+
pdfPointsPerInch: 72,
|
|
30
|
+
previewDebounceMs: 120,
|
|
31
|
+
previewMaxDimension: 1400,
|
|
32
|
+
thumbnailHeight: 56,
|
|
33
|
+
torchConstraint: 'torch',
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PsScanCornerOrder: readonly ["topLeft", "topRight", "bottomRight", "bottomLeft"];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import { PsLayer, usePsLayerZIndex, usePsRestoreFocus, usePsScrollLock, usePsViewport, } from '@pallasoft/ps-core-components';
|
|
4
|
+
import { PallasoftViewportTier, PallasoftZIndex } from '@pallasoft/ps-design-system';
|
|
5
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
6
|
+
import { PsModal } from '@pallasoft/ps-popovers';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
import { PsScanStep } from '../constants/ps-scan-step';
|
|
9
|
+
import { usePsFocusTrap } from '../hooks/use-ps-focus-trap';
|
|
10
|
+
import { usePsScanCamera } from '../hooks/use-ps-scan-camera';
|
|
11
|
+
import { usePsScanPages } from '../hooks/use-ps-scan-pages';
|
|
12
|
+
import { usePsScanPreviews } from '../hooks/use-ps-scan-previews';
|
|
13
|
+
import { PsScanOutputService } from '../services/ps-scan-output-service';
|
|
14
|
+
import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
|
|
15
|
+
import { PsScanTemplateService } from '../services/ps-scan-template-service';
|
|
16
|
+
import { PsScanCameraView } from './ps-scan-camera-view';
|
|
17
|
+
import { PsScanCropEditor } from './ps-scan-crop-editor';
|
|
18
|
+
import { PsScanIconButton } from './ps-scan-icon-button';
|
|
19
|
+
import { PsScanReview } from './ps-scan-review';
|
|
20
|
+
export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, maxPages, onClose, onComplete, outputFormat, }) => {
|
|
21
|
+
const tier = usePsViewport();
|
|
22
|
+
const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
|
|
23
|
+
const content = useRef(null);
|
|
24
|
+
const { add, move, pages, remove, replace, setCorners, update } = usePsScanPages();
|
|
25
|
+
const [step, setStep] = useState(PsScanStep.Camera);
|
|
26
|
+
const [activeId, setActiveId] = useState('');
|
|
27
|
+
const [retakeId, setRetakeId] = useState('');
|
|
28
|
+
const [announcement, setAnnouncement] = useState('');
|
|
29
|
+
const [error, setError] = useState('');
|
|
30
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
31
|
+
const camera = usePsScanCamera(step === PsScanStep.Camera);
|
|
32
|
+
const previews = usePsScanPreviews(pages);
|
|
33
|
+
const page = pages.find((item) => item.id === activeId);
|
|
34
|
+
const isDesktop = tier === PallasoftViewportTier.Desktop;
|
|
35
|
+
usePsScrollLock(!isDesktop);
|
|
36
|
+
usePsRestoreFocus(true);
|
|
37
|
+
usePsFocusTrap(content, true, onClose);
|
|
38
|
+
const edgeMessage = (isDetected) => isDetected ? labels.edgesFound : labels.edgesNotFound;
|
|
39
|
+
const addFiles = async (files) => {
|
|
40
|
+
setError('');
|
|
41
|
+
try {
|
|
42
|
+
const images = files.filter((file) => file.type === '' || file.type.startsWith('image/'));
|
|
43
|
+
if (retakeId !== '' && images.length > 0) {
|
|
44
|
+
const added = await replace(retakeId, images[0]);
|
|
45
|
+
setActiveId(retakeId);
|
|
46
|
+
setRetakeId('');
|
|
47
|
+
setStep(PsScanStep.Crop);
|
|
48
|
+
setAnnouncement(edgeMessage(added.isDetected));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const room = Math.max(0, maxPages - pages.length);
|
|
52
|
+
if (images.length > room)
|
|
53
|
+
setError(labels.maxPages);
|
|
54
|
+
const added = await add(images.slice(0, room));
|
|
55
|
+
if (added.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
setActiveId(added[0].page.id);
|
|
58
|
+
setStep(added.length === 1 ? PsScanStep.Crop : PsScanStep.Review);
|
|
59
|
+
setAnnouncement(added.length === 1
|
|
60
|
+
? edgeMessage(added[0].isDetected)
|
|
61
|
+
: PsScanTemplateService.fill(labels.pageAdded, { position: pages.length + added.length }));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
setError(labels.processFailed);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const capture = async () => {
|
|
68
|
+
const blob = await camera.capture();
|
|
69
|
+
if (blob !== null)
|
|
70
|
+
await addFiles([blob]);
|
|
71
|
+
};
|
|
72
|
+
const autoDetect = async () => {
|
|
73
|
+
if (page === undefined)
|
|
74
|
+
return;
|
|
75
|
+
const prepared = await PsScanPipelineService.prepare(page.source);
|
|
76
|
+
setCorners(page.id, prepared.corners);
|
|
77
|
+
setAnnouncement(edgeMessage(prepared.isDetected));
|
|
78
|
+
};
|
|
79
|
+
const retake = () => {
|
|
80
|
+
if (page === undefined)
|
|
81
|
+
return;
|
|
82
|
+
setRetakeId(page.id);
|
|
83
|
+
setStep(PsScanStep.Camera);
|
|
84
|
+
};
|
|
85
|
+
const deletePage = () => {
|
|
86
|
+
if (page === undefined)
|
|
87
|
+
return;
|
|
88
|
+
const index = pages.findIndex((item) => item.id === page.id);
|
|
89
|
+
const rest = pages.filter((item) => item.id !== page.id);
|
|
90
|
+
remove(page.id);
|
|
91
|
+
setAnnouncement(labels.pageDeleted);
|
|
92
|
+
if (rest.length === 0) {
|
|
93
|
+
setActiveId('');
|
|
94
|
+
setStep(PsScanStep.Camera);
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
setActiveId(rest[Math.min(index, rest.length - 1)].id);
|
|
98
|
+
};
|
|
99
|
+
const finish = async () => {
|
|
100
|
+
setIsBusy(true);
|
|
101
|
+
setError('');
|
|
102
|
+
try {
|
|
103
|
+
const files = await PsScanOutputService.build(pages, {
|
|
104
|
+
fileName,
|
|
105
|
+
format: outputFormat,
|
|
106
|
+
jpegQuality,
|
|
107
|
+
maxDimension,
|
|
108
|
+
});
|
|
109
|
+
onComplete(files);
|
|
110
|
+
onClose();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
setError(labels.processFailed);
|
|
114
|
+
setIsBusy(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const moveTo = useCallback((from, to) => {
|
|
118
|
+
move(from, to);
|
|
119
|
+
setAnnouncement(PsScanTemplateService.fill(labels.pageMoved, {
|
|
120
|
+
count: pages.length,
|
|
121
|
+
page: `${labels.page} ${from + 1}`,
|
|
122
|
+
position: to + 1,
|
|
123
|
+
}));
|
|
124
|
+
}, [move, pages.length, labels]);
|
|
125
|
+
const body = (_jsxs("div", { className: 'relative h-full min-h-0 bg-surface text-ink', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), step === PsScanStep.Camera ? (_jsx(PsScanCameraView, { canGoBack: pages.length > 0, canTorch: camera.canTorch, isTorchOn: camera.isTorchOn, labels: labels, onBack: () => {
|
|
126
|
+
setRetakeId('');
|
|
127
|
+
setStep(PsScanStep.Review);
|
|
128
|
+
}, onCapture: capture, onFiles: addFiles, onToggleTorch: camera.toggleTorch, status: camera.status, videoRef: camera.videoRef })) : null, step === PsScanStep.Crop && page !== undefined ? (_jsx(PsScanCropEditor, { labels: labels, onApply: () => setStep(PsScanStep.Review), onAutoDetect: autoDetect, onChange: (corners) => setCorners(page.id, corners), onRetake: retake, page: page })) : null, step === PsScanStep.Review && page !== undefined ? (_jsx(PsScanReview, { canAddPage: pages.length < maxPages, error: error, isBusy: isBusy, labels: labels, onAddPage: () => setStep(PsScanStep.Camera), onDelete: deletePage, onEditCrop: () => setStep(PsScanStep.Crop), onFilter: (filter) => update(page.id, { filter }), onFinish: finish, onMove: moveTo, onRetake: retake, onRotate: () => update(page.id, { rotation: (page.rotation + 90) % 360 }), onSelect: setActiveId, page: page, pages: pages, previews: previews })) : null, step === PsScanStep.Camera && error !== '' ? (_jsx("p", { className: 'absolute inset-x-4 top-3 rounded-ps-control bg-danger px-3 py-2 text-sm text-white', role: 'alert', children: error })) : null] }));
|
|
129
|
+
if (isDesktop)
|
|
130
|
+
return (_jsx(PsModal, { isOpen: true, onClose: onClose, size: 'xl', title: labels.title, children: _jsx("div", { className: '-m-4 h-[min(70dvh,38rem)] overflow-hidden sm:-m-5', children: body }) }));
|
|
131
|
+
if (typeof document === 'undefined')
|
|
132
|
+
return null;
|
|
133
|
+
return createPortal(_jsx("div", { "aria-label": labels.title, "aria-modal": 'true', className: 'fixed inset-0 flex flex-col bg-surface text-ink animate-ps-fade-in', role: 'dialog', style: { zIndex }, children: _jsxs(PsLayer, { zIndex: zIndex, children: [_jsxs("header", { className: 'flex shrink-0 items-center justify-between gap-3 border-b border-line px-4 pt-[max(0.5rem,env(safe-area-inset-top))] pb-2', children: [_jsx("h2", { className: 'truncate text-base font-semibold', children: labels.title }), _jsx(PsScanIconButton, { label: labels.close, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Close }) })] }), _jsx("div", { className: 'min-h-0 flex-1', children: body })] }) }), document.body);
|
|
134
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { usePsHydrated } from '@pallasoft/ps-core-components';
|
|
4
|
+
import { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
|
|
5
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
6
|
+
import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
|
|
7
|
+
import { PsDocumentScannerLoader } from './ps-document-scanner-loader';
|
|
8
|
+
export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, maxPages = PsScanConfig.defaultMaxPages, onClose, onComplete, outputFormat = PsScanOutputFormat.Pdf, }) => {
|
|
9
|
+
const isHydrated = usePsHydrated();
|
|
10
|
+
if (!isOpen || !isHydrated)
|
|
11
|
+
return null;
|
|
12
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, maxPages: maxPages, onClose: onClose, onComplete: onComplete, outputFormat: outputFormat }) }));
|
|
13
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
4
4
|
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
5
5
|
import { PsFilePickerLabels } from '../constants/ps-file-picker-labels';
|
|
6
6
|
import { PsFileSelectionService } from '../services/ps-file-selection-service';
|
|
7
|
-
export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false, dropLabel, isLoading = false, labels, maxSizeBytes, maxSizeLabel, multiple = false, onError, onFileLoad, onFilesSelect, }) => {
|
|
7
|
+
export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false, dropLabel, isLoading = false, labels, maxSizeBytes, maxSizeLabel, multiple = false, onError, onFileLoad, onFilesSelect, touchLabel, }) => {
|
|
8
8
|
const [isDragging, setIsDragging] = useState(false);
|
|
9
9
|
const acceptAttr = useMemo(() => accept.join(','), [accept]);
|
|
10
10
|
const text = { ...PsFilePickerLabels, ...labels };
|
|
@@ -40,5 +40,5 @@ export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false,
|
|
|
40
40
|
return (_jsxs("div", { className: PallasoftClassNameService.merge('relative flex h-48 w-full flex-col items-center justify-center rounded-ps-control border-2 border-dashed border-line p-4 transition-all hover:border-primary/50 hover:bg-primary/5 sm:h-64', isDragging ? 'border-primary bg-primary/10' : '', disabled ? 'pointer-events-none opacity-60' : '', className), onDragEnter: (event) => onDrag(event, true), onDragLeave: (event) => onDrag(event, false), onDragOver: (event) => onDrag(event, true), onDrop: (event) => {
|
|
41
41
|
onDrag(event, false);
|
|
42
42
|
select(event.dataTransfer.files);
|
|
43
|
-
}, children: [_jsxs("div", { className: 'pointer-events-none flex flex-col items-center gap-3 text-center', children: [_jsx(PsIcon, { className: 'h-8 w-8 text-primary', iconKey: PsIconKey.Upload }), _jsxs("div", { children: [_jsx("p", { className: 'text-body-2 font-medium text-ink sm:text-body-1', children: isDragging ? text.dropActive : dropLabel }), _jsx("p", { className: 'mt-1 text-body-3 text-ink/60 sm:text-body-2', children: text.browse }), maxSizeLabel ? _jsx("p", { className: 'mt-3 text-body-3 text-ink/60', children: maxSizeLabel }) : null, _jsxs("p", { className: 'text-body-3 text-ink/60', children: [text.formats, " ", acceptLabel] })] })] }), _jsx("input", { accept: acceptAttr, "aria-disabled": disabled, "aria-label": dropLabel, className: 'absolute inset-0 h-full w-full cursor-pointer opacity-0', disabled: disabled, multiple: multiple, onChange: onChange, type: 'file' })] }));
|
|
43
|
+
}, children: [_jsxs("div", { className: 'pointer-events-none flex flex-col items-center gap-3 text-center', children: [_jsx(PsIcon, { className: 'h-8 w-8 text-primary', iconKey: PsIconKey.Upload }), _jsxs("div", { children: [_jsx("p", { className: 'text-body-2 font-medium text-ink sm:text-body-1', children: isDragging ? (text.dropActive) : (_jsxs(_Fragment, { children: [_jsx("span", { className: touchLabel === undefined ? '' : '[@media(pointer:coarse)]:hidden', children: dropLabel }), touchLabel === undefined ? null : (_jsx("span", { className: 'hidden [@media(pointer:coarse)]:inline', children: touchLabel }))] })) }), _jsxs("p", { className: 'mt-1 text-body-3 text-ink/60 sm:text-body-2', children: [_jsx("span", { className: '[@media(pointer:coarse)]:hidden', children: text.browse }), _jsx("span", { className: 'hidden [@media(pointer:coarse)]:inline', children: text.browseTouch })] }), maxSizeLabel ? _jsx("p", { className: 'mt-3 text-body-3 text-ink/60', children: maxSizeLabel }) : null, _jsxs("p", { className: 'text-body-3 text-ink/60', children: [text.formats, " ", acceptLabel] })] })] }), _jsx("input", { accept: acceptAttr, "aria-disabled": disabled, "aria-label": dropLabel, className: 'absolute inset-0 h-full w-full cursor-pointer opacity-0', disabled: disabled, multiple: multiple, onChange: onChange, type: 'file' })] }));
|
|
44
44
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
4
|
+
import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
|
|
5
|
+
import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
|
|
6
|
+
import { PsFileViewerLabels } from '../constants/ps-file-viewer-labels';
|
|
7
|
+
import { PsFileTypeService } from '../services/ps-file-type-service';
|
|
8
|
+
import { PsFileViewerButton } from './ps-file-viewer-button';
|
|
9
|
+
import { PsFileViewerContent } from './ps-file-viewer-content';
|
|
10
|
+
export const PsFileViewerPane = ({ contentType, headerActions, id, labels = PsFileViewerLabels, name, pdfWorkerSrc, url, }) => {
|
|
11
|
+
const [zoom, setZoom] = useState(1);
|
|
12
|
+
const [rotation, setRotation] = useState(0);
|
|
13
|
+
const kind = PsFileTypeService.kindOf(contentType, name);
|
|
14
|
+
const canZoom = PsFileViewerKinds.zoomable.includes(kind);
|
|
15
|
+
const canRotate = PsFileViewerKinds.rotatable.includes(kind);
|
|
16
|
+
const changeZoom = (step) => setZoom((current) => Math.min(PsFileViewerConfig.maxZoom, Math.max(PsFileViewerConfig.minZoom, current + step)));
|
|
17
|
+
const fileKey = id === undefined ? url : id;
|
|
18
|
+
const rotate = () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360);
|
|
19
|
+
const onKeyDown = (event) => {
|
|
20
|
+
if (event.metaKey || event.ctrlKey || event.altKey)
|
|
21
|
+
return;
|
|
22
|
+
if (canZoom && (event.key === '+' || event.key === '='))
|
|
23
|
+
changeZoom(PsFileViewerConfig.zoomStep);
|
|
24
|
+
else if (canZoom && (event.key === '-' || event.key === '_'))
|
|
25
|
+
changeZoom(-PsFileViewerConfig.zoomStep);
|
|
26
|
+
else if (canZoom && event.key === '0')
|
|
27
|
+
setZoom(1);
|
|
28
|
+
else if (canRotate && event.key.toLowerCase() === 'r')
|
|
29
|
+
rotate();
|
|
30
|
+
else
|
|
31
|
+
return;
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { className: 'flex h-full min-h-0 min-w-0 flex-col overflow-hidden rounded-ps-panel bg-black/85', children: [_jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-between gap-x-2 border-b border-white/10 px-2 py-1 text-white', children: [_jsx("p", { className: 'min-w-0 flex-1 basis-32 truncate px-1 text-xs text-white/70', title: name, children: name }), _jsxs("div", { className: 'flex items-center gap-0.5', role: 'toolbar', children: [canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { disabled: zoom <= PsFileViewerConfig.minZoom, label: labels.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: labels.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { disabled: zoom >= PsFileViewerConfig.maxZoom, label: labels.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: labels.rotate, onClick: rotate, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Rotate }) })) : null, headerActions] })] }), _jsx("div", { className: 'ps-focus-ring flex min-h-0 flex-1 p-2', onKeyDown: onKeyDown, tabIndex: 0, children: _jsx(PsFileViewerContent, { activeMatch: 0, file: { contentType, id: fileKey, name, url }, labels: labels, onMatchCount: () => undefined, pdfWorkerSrc: pdfWorkerSrc, query: '', rotation: rotation, zoom: zoom }, fileKey) })] }));
|
|
35
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { PsButton, PsSpinner } from '@pallasoft/ps-core-components';
|
|
4
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
5
|
+
import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
|
|
6
|
+
import { PsScanIconButton } from './ps-scan-icon-button';
|
|
7
|
+
export const PsScanCameraView = ({ canGoBack, canTorch, isTorchOn, labels, onBack, onCapture, onFiles, onToggleTorch, status, videoRef, }) => {
|
|
8
|
+
const captureInput = useRef(null);
|
|
9
|
+
const libraryInput = useRef(null);
|
|
10
|
+
const isReady = status === PsScanCameraStatus.Ready;
|
|
11
|
+
const message = status === PsScanCameraStatus.Denied
|
|
12
|
+
? labels.cameraDenied
|
|
13
|
+
: status === PsScanCameraStatus.Unavailable
|
|
14
|
+
? labels.cameraUnavailable
|
|
15
|
+
: '';
|
|
16
|
+
const onChange = (event) => {
|
|
17
|
+
const files = Array.from(event.target.files === null ? [] : event.target.files);
|
|
18
|
+
event.target.value = '';
|
|
19
|
+
if (files.length > 0)
|
|
20
|
+
onFiles(files);
|
|
21
|
+
};
|
|
22
|
+
return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-slate-950 text-white', children: [_jsxs("div", { className: 'relative min-h-0 flex-1', children: [_jsx("video", { "aria-label": labels.title, autoPlay: true, className: `size-full object-contain ${isReady ? '' : 'invisible'}`, muted: true, playsInline: true, ref: videoRef }), status === PsScanCameraStatus.Starting ? (_jsxs("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/80', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] })) : null, message === '' ? null : (_jsx("div", { className: 'absolute inset-0 flex items-center justify-center p-6 text-center text-sm text-white/85', role: 'status', children: _jsx("p", { className: 'max-w-sm', children: message }) }))] }), _jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-center gap-3 px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]', children: [canGoBack ? (_jsx(PsButton, { onClick: onBack, variant: 'surface', children: labels.backToPages })) : null, _jsx(PsButton, { leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Image }), onClick: () => libraryInput.current?.click(), variant: 'surface', children: labels.choosePhotos }), isReady ? (_jsx("button", { "aria-label": labels.capture, className: 'ps-focus-ring flex size-[4.5rem] shrink-0 items-center justify-center rounded-full border-4 border-white bg-white/20 transition active:scale-95', onClick: onCapture, title: labels.capture, type: 'button', children: _jsx("span", { className: 'size-12 rounded-full bg-white' }) })) : (_jsx(PsButton, { onClick: () => captureInput.current?.click(), variant: 'primary', children: labels.takePhoto })), canTorch && isReady ? (_jsx(PsScanIconButton, { isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Spark }) })) : null] }), _jsx("input", { accept: 'image/*', capture: 'environment', className: 'sr-only', onChange: onChange, ref: captureInput, tabIndex: -1, type: 'file' }), _jsx("input", { accept: 'image/*', className: 'sr-only', multiple: true, onChange: onChange, ref: libraryInput, tabIndex: -1, type: 'file' })] }));
|
|
23
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { PsButton, usePsElementSize } from '@pallasoft/ps-core-components';
|
|
4
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
5
|
+
import { PsScanCornerOrder } from '../constants/ps-scan-corner-order';
|
|
6
|
+
import { PsScanGeometryService } from '../services/ps-scan-geometry-service';
|
|
7
|
+
export const PsScanCropEditor = ({ labels, onApply, onAutoDetect, onChange, onRetake, page, }) => {
|
|
8
|
+
const [areaRef, area] = usePsElementSize();
|
|
9
|
+
const stageRef = useRef(null);
|
|
10
|
+
const hintId = 'ps-scan-crop-hint';
|
|
11
|
+
const size = PsScanGeometryService.fit(area, { height: page.height, width: page.width });
|
|
12
|
+
const cornerLabels = [
|
|
13
|
+
labels.cornerTopLeft,
|
|
14
|
+
labels.cornerTopRight,
|
|
15
|
+
labels.cornerBottomRight,
|
|
16
|
+
labels.cornerBottomLeft,
|
|
17
|
+
];
|
|
18
|
+
const move = (index, event) => {
|
|
19
|
+
const stage = stageRef.current;
|
|
20
|
+
if (stage === null || !event.currentTarget.hasPointerCapture(event.pointerId))
|
|
21
|
+
return;
|
|
22
|
+
const rect = stage.getBoundingClientRect();
|
|
23
|
+
const point = PsScanGeometryService.clamp({
|
|
24
|
+
x: (event.clientX - rect.left) / rect.width,
|
|
25
|
+
y: (event.clientY - rect.top) / rect.height,
|
|
26
|
+
});
|
|
27
|
+
const next = page.corners.map((corner, at) => (at === index ? point : corner));
|
|
28
|
+
if (PsScanGeometryService.isUsable(next))
|
|
29
|
+
onChange(next);
|
|
30
|
+
};
|
|
31
|
+
const nudge = (index, event) => {
|
|
32
|
+
const step = event.shiftKey ? PsScanConfig.cornerNudgeLarge : PsScanConfig.cornerNudge;
|
|
33
|
+
const deltas = {
|
|
34
|
+
ArrowDown: [0, step],
|
|
35
|
+
ArrowLeft: [-step, 0],
|
|
36
|
+
ArrowRight: [step, 0],
|
|
37
|
+
ArrowUp: [0, -step],
|
|
38
|
+
};
|
|
39
|
+
const delta = deltas[event.key];
|
|
40
|
+
if (delta === undefined)
|
|
41
|
+
return;
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
onChange(PsScanGeometryService.nudge(page.corners, index, delta[0], delta[1]));
|
|
44
|
+
};
|
|
45
|
+
return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-slate-950 text-white', children: [_jsx("p", { className: 'shrink-0 px-4 pt-3 text-center text-sm text-white/80', id: hintId, children: labels.cropHint }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center p-6', ref: areaRef, children: _jsxs("div", { className: 'relative touch-none select-none', ref: stageRef, style: { height: size.height, width: size.width }, children: [_jsx("img", { alt: '', className: 'absolute inset-0 size-full', draggable: false, src: page.sourceUrl }), _jsx("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', preserveAspectRatio: 'none', viewBox: '0 0 1 1', children: _jsx("polygon", { fill: 'rgba(255,255,255,0.12)', points: page.corners.map((point) => `${point.x},${point.y}`).join(' '), stroke: 'white', strokeWidth: 2, vectorEffect: 'non-scaling-stroke' }) }), PsScanCornerOrder.map((key, index) => (_jsx("button", { "aria-describedby": hintId, "aria-label": cornerLabels[index], className: 'ps-focus-ring absolute flex size-11 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center rounded-full active:cursor-grabbing', onKeyDown: (event) => nudge(index, event), onPointerCancel: (event) => event.currentTarget.releasePointerCapture(event.pointerId), onPointerDown: (event) => event.currentTarget.setPointerCapture(event.pointerId), onPointerMove: (event) => move(index, event), onPointerUp: (event) => event.currentTarget.releasePointerCapture(event.pointerId), style: {
|
|
46
|
+
left: `${page.corners[index].x * 100}%`,
|
|
47
|
+
top: `${page.corners[index].y * 100}%`,
|
|
48
|
+
}, type: 'button', children: _jsx("span", { className: 'size-5 rounded-full border-2 border-white bg-primary shadow' }) }, key)))] }) }), _jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-center gap-3 px-4 pt-2 pb-[max(0.75rem,env(safe-area-inset-bottom))]', children: [_jsx(PsButton, { onClick: onRetake, variant: 'surface', children: labels.retake }), _jsx(PsButton, { onClick: onAutoDetect, variant: 'surface', children: labels.autoDetect }), _jsx(PsButton, { onClick: onApply, variant: 'primary', children: labels.cropApply })] })] }));
|
|
49
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
export const PsScanIconButton = ({ children, className, disabled, isPressed, label, onClick, }) => (_jsx("button", { "aria-label": label, "aria-pressed": isPressed, className: PallasoftClassNameService.merge('ps-focus-ring inline-flex size-11 min-h-11 min-w-11 shrink-0 items-center justify-center rounded-ps-control border border-line bg-surface text-ink transition hover:border-primary/40 hover:text-primary disabled:pointer-events-none disabled:opacity-40', isPressed ? 'border-primary bg-primary-soft text-primary' : '', className), disabled: disabled, onClick: onClick, title: label, type: 'button', children: children }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsSortableList } from '@pallasoft/ps-core-components';
|
|
3
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
4
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
5
|
+
import { PsScanTemplateService } from '../services/ps-scan-template-service';
|
|
6
|
+
import { PsScanIconButton } from './ps-scan-icon-button';
|
|
7
|
+
export const PsScanPageList = ({ activeId, labels, onMove, onSelect, pages, previews, }) => {
|
|
8
|
+
const items = pages.map((page, index) => ({
|
|
9
|
+
id: page.id,
|
|
10
|
+
label: `${labels.page} ${index + 1}`,
|
|
11
|
+
}));
|
|
12
|
+
const say = (template, label, position) => PsScanTemplateService.fill(template, { count: pages.length, page: label, position });
|
|
13
|
+
return (_jsx(PsSortableList, { announcements: {
|
|
14
|
+
cancelled: (label) => say(labels.reorderCancelled, label, 0),
|
|
15
|
+
dropped: (label, position) => say(labels.reorderDropped, label, position),
|
|
16
|
+
lifted: (label, position) => say(labels.reorderLifted, label, position),
|
|
17
|
+
moved: (label, position) => say(labels.reorderMoved, label, position),
|
|
18
|
+
}, className: 'flex flex-col gap-2', getHandleLabel: (label) => PsScanTemplateService.fill(labels.dragPage, { page: label }), instructions: labels.reorderInstructions, items: items, onMove: (_id, from, to) => onMove(from, to), renderItem: ({ handle, index, isDragging, item }) => (_jsxs("div", { className: PallasoftClassNameService.merge('flex items-center gap-2 rounded-ps-control border bg-surface p-1.5', item.id === activeId ? 'border-primary' : 'border-line', isDragging ? 'opacity-80 shadow-lg' : ''), children: [handle, _jsxs("button", { "aria-current": item.id === activeId, "aria-label": item.label, className: 'ps-focus-ring flex min-h-11 min-w-0 flex-1 items-center gap-3 rounded-ps-control text-left', onClick: () => onSelect(item.id), type: 'button', children: [previews[item.id] === undefined ? (_jsx("span", { className: 'ps-skeleton h-14 w-10 shrink-0 rounded' })) : (_jsx("img", { alt: '', className: 'h-14 w-10 shrink-0 rounded border border-line bg-white object-cover object-top', src: previews[item.id] })), _jsx("span", { className: 'truncate text-sm font-medium text-ink', children: item.label })] }), _jsx(PsScanIconButton, { disabled: index === 0, label: PsScanTemplateService.fill(labels.moveUp, { page: item.label }), onClick: () => onMove(index, index - 1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5 -rotate-90', iconKey: PsIconKey.ChevronRight }) }), _jsx(PsScanIconButton, { disabled: index === pages.length - 1, label: PsScanTemplateService.fill(labels.moveDown, { page: item.label }), onClick: () => onMove(index, index + 1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5 rotate-90', iconKey: PsIconKey.ChevronRight }) })] })), role: 'list' }));
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsButton } from '@pallasoft/ps-core-components';
|
|
3
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
4
|
+
import { PsScanFilter } from '../constants/ps-scan-filter';
|
|
5
|
+
import { PsScanIconButton } from './ps-scan-icon-button';
|
|
6
|
+
import { PsScanPageList } from './ps-scan-page-list';
|
|
7
|
+
export const PsScanReview = ({ canAddPage, error, isBusy, labels, onAddPage, onDelete, onEditCrop, onFilter, onFinish, onMove, onRetake, onRotate, onSelect, page, pages, previews, }) => {
|
|
8
|
+
const filters = [
|
|
9
|
+
{ key: PsScanFilter.Original, label: labels.filterOriginal },
|
|
10
|
+
{ key: PsScanFilter.Colour, label: labels.filterColour },
|
|
11
|
+
{ key: PsScanFilter.Greyscale, label: labels.filterGreyscale },
|
|
12
|
+
{ key: PsScanFilter.Document, label: labels.filterDocument },
|
|
13
|
+
];
|
|
14
|
+
const preview = previews[page.id];
|
|
15
|
+
return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col lg:flex-row', children: [_jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center bg-slate-950 p-3', children: preview === undefined ? (_jsx("span", { "aria-hidden": 'true', className: 'ps-skeleton h-full w-2/3 rounded' })) : (_jsx("img", { alt: labels.page, className: 'max-h-full max-w-full object-contain', src: preview })) }), _jsxs("div", { className: 'flex max-h-[55%] min-h-0 shrink-0 flex-col gap-3 overflow-y-auto bg-surface px-4 pt-3 lg:max-h-none lg:w-80 lg:border-l lg:border-line', children: [_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [_jsx(PsScanIconButton, { label: labels.rotate, onClick: onRotate, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Rotate }) }), _jsx(PsScanIconButton, { label: labels.editCrop, onClick: onEditCrop, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Pencil }) }), _jsx(PsScanIconButton, { label: labels.retake, onClick: onRetake, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Refresh }) }), _jsx(PsScanIconButton, { label: labels.deletePage, onClick: onDelete, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Trash }) })] }), _jsx("div", { "aria-label": labels.filters, className: 'flex flex-wrap gap-2', role: 'group', children: filters.map((filter) => (_jsx("button", { "aria-pressed": page.filter === filter.key, className: `ps-focus-ring min-h-11 rounded-ps-control border px-3 text-sm font-medium transition ${page.filter === filter.key ? 'border-primary bg-primary-soft text-primary' : 'border-line bg-surface text-ink hover:border-primary/40'}`, onClick: () => onFilter(filter.key), type: 'button', children: filter.label }, filter.key))) }), _jsxs("section", { "aria-label": labels.pages, className: 'min-h-0', children: [_jsxs("h3", { className: 'mb-2 text-sm font-semibold text-ink', children: [labels.pages, " (", pages.length, ")"] }), _jsx(PsScanPageList, { activeId: page.id, labels: labels, onMove: onMove, onSelect: onSelect, pages: pages, previews: previews })] }), error === '' ? null : (_jsx("p", { className: 'text-sm text-danger', role: 'alert', children: error })), _jsxs("div", { className: 'sticky bottom-0 mt-auto flex flex-wrap gap-3 bg-surface pt-2 pb-[max(0.75rem,env(safe-area-inset-bottom))]', children: [_jsx(PsButton, { className: 'flex-1', disabled: !canAddPage || isBusy, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Plus }), onClick: onAddPage, variant: 'surface', children: labels.addPage }), _jsx(PsButton, { className: 'flex-1', isLoading: isBusy, loadingLabel: labels.finishing, onClick: onFinish, variant: 'primary', children: labels.finish })] })] })] }));
|
|
16
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const PsDocumentScannerLabels = {
|
|
2
|
+
addPage: 'Add page',
|
|
3
|
+
autoDetect: 'Auto-detect edges',
|
|
4
|
+
cameraDenied: 'Camera access was blocked. Allow it in your browser settings, or take a photo or choose existing photos instead.',
|
|
5
|
+
cameraStarting: 'Starting camera',
|
|
6
|
+
cameraUnavailable: 'No camera is available here. Take a photo or choose existing photos instead.',
|
|
7
|
+
backToPages: 'Back to pages',
|
|
8
|
+
capture: 'Capture page',
|
|
9
|
+
choosePhotos: 'Choose photos',
|
|
10
|
+
close: 'Close scanner',
|
|
11
|
+
cornerBottomLeft: 'Bottom left corner',
|
|
12
|
+
cornerBottomRight: 'Bottom right corner',
|
|
13
|
+
cornerTopLeft: 'Top left corner',
|
|
14
|
+
cornerTopRight: 'Top right corner',
|
|
15
|
+
cropApply: 'Use crop',
|
|
16
|
+
cropHint: 'Drag the corners to fit the page, or use the arrow keys on a focused corner.',
|
|
17
|
+
cropTitle: 'Adjust edges',
|
|
18
|
+
deletePage: 'Delete page',
|
|
19
|
+
dragPage: 'Drag {page} to reorder',
|
|
20
|
+
editCrop: 'Edit edges',
|
|
21
|
+
edgesFound: 'Page edges found.',
|
|
22
|
+
edgesNotFound: 'Edges not found. Adjust the corners.',
|
|
23
|
+
filterColour: 'Colour',
|
|
24
|
+
filterDocument: 'Black and white',
|
|
25
|
+
filterGreyscale: 'Greyscale',
|
|
26
|
+
filterOriginal: 'Original',
|
|
27
|
+
filters: 'Enhance',
|
|
28
|
+
finish: 'Use scan',
|
|
29
|
+
finishing: 'Preparing your scan',
|
|
30
|
+
maxPages: 'You reached the page limit.',
|
|
31
|
+
moveDown: 'Move {page} down',
|
|
32
|
+
moveUp: 'Move {page} up',
|
|
33
|
+
page: 'Page',
|
|
34
|
+
pageAdded: 'Page {position} added.',
|
|
35
|
+
pageDeleted: 'Page deleted.',
|
|
36
|
+
pageMoved: '{page} moved to position {position} of {count}.',
|
|
37
|
+
pages: 'Pages',
|
|
38
|
+
processFailed: 'The scan could not be prepared. Try again.',
|
|
39
|
+
reorderCancelled: '{page} stays where it was.',
|
|
40
|
+
reorderDropped: '{page} dropped at {position} of {count}.',
|
|
41
|
+
reorderInstructions: 'Press space to lift a page, the arrow keys to move it and space to drop it.',
|
|
42
|
+
reorderLifted: '{page} lifted from {position} of {count}.',
|
|
43
|
+
reorderMoved: '{page} is now {position} of {count}.',
|
|
44
|
+
retake: 'Retake',
|
|
45
|
+
rotate: 'Rotate',
|
|
46
|
+
takePhoto: 'Take photo',
|
|
47
|
+
title: 'Scan document',
|
|
48
|
+
torchOff: 'Turn light off',
|
|
49
|
+
torchOn: 'Turn light on',
|
|
50
|
+
};
|