mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
Add undo/redo functionality and refactor signature settings UI
- Introduced HistoryAPIBridge for managing undo/redo actions. - Updated SignSettings component to include undo/redo buttons. - Refactored signature type selection to use Tabs for better UI. - Enhanced SignatureAPIBridge to store image data for annotations. - Integrated history management into SignatureContext for state handling.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert, Modal, ColorSwatch, Menu, ActionIcon, Slider, Select, Combobox, useCombobox, ColorPicker } from '@mantine/core';
|
||||
import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert, Modal, ColorSwatch, Menu, ActionIcon, Slider, Select, Combobox, useCombobox, ColorPicker, Tabs } from '@mantine/core';
|
||||
import ButtonSelector from "../../shared/ButtonSelector";
|
||||
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
||||
|
||||
@@ -12,9 +12,11 @@ interface SignSettingsProps {
|
||||
onActivateSignaturePlacement?: () => void;
|
||||
onDeactivateSignature?: () => void;
|
||||
onUpdateDrawSettings?: (color: string, size: number) => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
}
|
||||
|
||||
const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature, onUpdateDrawSettings }: SignSettingsProps) => {
|
||||
const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature, onUpdateDrawSettings, onUndo, onRedo }: SignSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
@@ -432,35 +434,43 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Signature Type Selection */}
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
{t('sign.type.title', 'Signature Type')}
|
||||
</Text>
|
||||
<ButtonSelector
|
||||
value={parameters.signatureType}
|
||||
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')}
|
||||
options={[
|
||||
{
|
||||
value: 'draw',
|
||||
label: t('sign.type.draw', 'Draw'),
|
||||
},
|
||||
{
|
||||
value: 'canvas',
|
||||
label: t('sign.type.canvas', 'Canvas'),
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
label: t('sign.type.image', 'Image'),
|
||||
},
|
||||
{
|
||||
value: 'text',
|
||||
label: t('sign.type.text', 'Text'),
|
||||
},
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Tabs
|
||||
value={parameters.signatureType}
|
||||
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')}
|
||||
>
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="draw" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.draw', 'Draw')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="canvas" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.canvas', 'Canvas')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="image" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.image', 'Image')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="text" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.text', 'Text')}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
|
||||
{/* Undo/Redo Controls */}
|
||||
<Group justify="space-between" grow>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onUndo}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('sign.undo', 'Undo')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRedo}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('sign.redo', 'Redo')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Signature Creation based on type */}
|
||||
{parameters.signatureType === 'canvas' && (
|
||||
@@ -782,9 +792,9 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
||||
{(parameters.signatureType === 'canvas' || parameters.signatureType === 'image' || parameters.signatureType === 'text') && (
|
||||
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||
<Text size="sm">
|
||||
{parameters.signatureType === 'canvas' && 'Draw your signature in the canvas above. Placement mode will activate automatically when you finish drawing.'}
|
||||
{parameters.signatureType === 'image' && 'Upload your signature image above. Placement mode will activate automatically when the image is loaded.'}
|
||||
{parameters.signatureType === 'text' && 'Enter your name above. Placement mode will activate automatically when you type your name.'}
|
||||
{parameters.signatureType === 'canvas' && 'After drawing your signature in the canvas above, click anywhere on the PDF to place it.'}
|
||||
{parameters.signatureType === 'image' && 'After uploading your signature image above, click anywhere on the PDF to place it.'}
|
||||
{parameters.signatureType === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -40,7 +40,7 @@ const EmbedPdfViewerContent = ({
|
||||
const isSignatureMode = selectedTool === 'sign';
|
||||
|
||||
// Get signature context
|
||||
const { signatureApiRef } = useSignature();
|
||||
const { signatureApiRef, historyApiRef } = useSignature();
|
||||
|
||||
|
||||
// Get current file from FileContext
|
||||
@@ -189,8 +189,17 @@ const EmbedPdfViewerContent = ({
|
||||
url={effectiveFile.url}
|
||||
enableSignature={isSignatureMode}
|
||||
signatureApiRef={signatureApiRef as React.RefObject<any>}
|
||||
historyApiRef={historyApiRef as React.RefObject<any>}
|
||||
onSignatureAdded={(annotation) => {
|
||||
console.log('Signature added:', annotation);
|
||||
if (annotation.type === 13) {
|
||||
console.log('- imageSrc:', !!annotation.imageSrc, annotation.imageSrc?.length);
|
||||
console.log('- contents:', !!annotation.contents, annotation.contents?.length);
|
||||
console.log('- data:', !!annotation.data, annotation.data?.length);
|
||||
console.log('- imageData:', !!annotation.imageData, annotation.imageData?.length);
|
||||
console.log('- appearance:', !!annotation.appearance, typeof annotation.appearance);
|
||||
console.log('- All keys:', Object.keys(annotation));
|
||||
}
|
||||
// Future: Handle signature completion
|
||||
}}
|
||||
/>
|
||||
|
||||
86
frontend/src/components/viewer/HistoryAPIBridge.tsx
Normal file
86
frontend/src/components/viewer/HistoryAPIBridge.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useImperativeHandle, forwardRef, useEffect } from 'react';
|
||||
import { useHistoryCapability } from '@embedpdf/plugin-history/react';
|
||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
|
||||
export interface HistoryAPI {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: () => boolean;
|
||||
canRedo: () => boolean;
|
||||
}
|
||||
|
||||
export interface HistoryAPIBridgeProps {}
|
||||
|
||||
export const HistoryAPIBridge = forwardRef<HistoryAPI, HistoryAPIBridgeProps>((props, ref) => {
|
||||
const { provides: historyApi } = useHistoryCapability();
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { getImageData } = useSignature();
|
||||
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
undo: () => {
|
||||
if (historyApi) {
|
||||
historyApi.undo();
|
||||
|
||||
// Restore image data for STAMP annotations after undo
|
||||
setTimeout(() => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
for (let pageIndex = 0; pageIndex < 10; pageIndex++) {
|
||||
const pageAnnotationsTask = annotationApi.getPageAnnotations?.({ pageIndex });
|
||||
if (pageAnnotationsTask) {
|
||||
pageAnnotationsTask.toPromise().then((pageAnnotations: any) => {
|
||||
if (pageAnnotations) {
|
||||
pageAnnotations.forEach((ann: any) => {
|
||||
if (ann.type === 13) {
|
||||
const storedImageData = getImageData(ann.id);
|
||||
|
||||
if (storedImageData && (!ann.imageSrc || ann.imageSrc !== storedImageData)) {
|
||||
const originalData = {
|
||||
type: ann.type,
|
||||
rect: ann.rect,
|
||||
author: ann.author || 'Digital Signature',
|
||||
subject: ann.subject || 'Digital Signature',
|
||||
pageIndex: pageIndex,
|
||||
id: ann.id,
|
||||
created: ann.created || new Date(),
|
||||
imageSrc: storedImageData
|
||||
};
|
||||
|
||||
annotationApi.deleteAnnotation(pageIndex, ann.id);
|
||||
setTimeout(() => {
|
||||
annotationApi.createAnnotation(pageIndex, originalData);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
console.error(`Failed to get annotations for page ${pageIndex}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
if (historyApi) {
|
||||
historyApi.redo();
|
||||
}
|
||||
},
|
||||
|
||||
canUndo: () => {
|
||||
return historyApi ? historyApi.canUndo() : false;
|
||||
},
|
||||
|
||||
canRedo: () => {
|
||||
return historyApi ? historyApi.canRedo() : false;
|
||||
},
|
||||
}), [historyApi]);
|
||||
|
||||
return null; // This is a bridge component with no UI
|
||||
});
|
||||
|
||||
HistoryAPIBridge.displayName = 'HistoryAPIBridge';
|
||||
@@ -35,6 +35,7 @@ import { SearchAPIBridge } from './SearchAPIBridge';
|
||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
|
||||
import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
|
||||
|
||||
interface LocalEmbedPDFProps {
|
||||
file?: File | Blob;
|
||||
@@ -42,9 +43,10 @@ interface LocalEmbedPDFProps {
|
||||
enableSignature?: boolean;
|
||||
onSignatureAdded?: (annotation: any) => void;
|
||||
signatureApiRef?: React.RefObject<SignatureAPI>;
|
||||
historyApiRef?: React.RefObject<HistoryAPI>;
|
||||
}
|
||||
|
||||
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef }: LocalEmbedPDFProps) {
|
||||
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
const [annotations, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
|
||||
|
||||
@@ -257,6 +259,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
|
||||
<ThumbnailAPIBridge />
|
||||
<RotateAPIBridge />
|
||||
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
|
||||
{enableSignature && <HistoryAPIBridge ref={historyApiRef} />}
|
||||
<GlobalPointerProvider>
|
||||
<Viewport
|
||||
style={{
|
||||
|
||||
@@ -20,7 +20,8 @@ export interface SignatureAPIBridgeProps {}
|
||||
|
||||
export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgeProps>((props, ref) => {
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { signatureConfig } = useSignature();
|
||||
const { signatureConfig, storeImageData } = useSignature();
|
||||
|
||||
|
||||
// Enable keyboard deletion of selected annotations
|
||||
useEffect(() => {
|
||||
@@ -31,13 +32,30 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
|
||||
|
||||
if (selectedAnnotation) {
|
||||
const annotation = selectedAnnotation as any;
|
||||
const pageIndex = annotation.object?.pageIndex || 0;
|
||||
const id = annotation.object?.id;
|
||||
// First, let EmbedPDF try to handle this natively
|
||||
// Only intervene if needed
|
||||
setTimeout(() => {
|
||||
// Check if the annotation is still selected after a brief delay
|
||||
// If EmbedPDF handled it natively, it should be gone
|
||||
const stillSelected = annotationApi.getSelectedAnnotation?.();
|
||||
|
||||
if (id) {
|
||||
annotationApi.deleteAnnotation(pageIndex, id);
|
||||
}
|
||||
if (stillSelected) {
|
||||
// EmbedPDF didn't handle it, so we need to delete manually
|
||||
const annotation = stillSelected as any;
|
||||
const pageIndex = annotation.object?.pageIndex || 0;
|
||||
const id = annotation.object?.id;
|
||||
|
||||
if (id) {
|
||||
// Try deleteSelected method first (should integrate with history)
|
||||
if ((annotationApi as any).deleteSelected) {
|
||||
(annotationApi as any).deleteSelected();
|
||||
} else {
|
||||
// Fallback to direct deletion
|
||||
annotationApi.deleteAnnotation(pageIndex, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -50,7 +68,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Create image stamp annotation
|
||||
// Create image stamp annotation with proper image data
|
||||
console.log('Creating image annotation with data length:', signatureData?.length);
|
||||
|
||||
const annotationId = uuidV4();
|
||||
|
||||
// Store image data in our persistent store
|
||||
storeImageData(annotationId, signatureData);
|
||||
|
||||
annotationApi.createAnnotation(pageIndex, {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
rect: {
|
||||
@@ -60,8 +85,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
author: 'Digital Signature',
|
||||
subject: 'Digital Signature',
|
||||
pageIndex: pageIndex,
|
||||
id: uuidV4(),
|
||||
id: annotationId,
|
||||
created: new Date(),
|
||||
// Store image data in multiple places to ensure history captures it
|
||||
imageSrc: signatureData,
|
||||
contents: signatureData, // Some annotation systems use contents
|
||||
data: signatureData, // Try data field
|
||||
imageData: signatureData, // Try imageData field
|
||||
appearance: signatureData // Try appearance field
|
||||
});
|
||||
},
|
||||
|
||||
@@ -86,6 +117,10 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
pageIndex: pageIndex,
|
||||
id: uuidV4(),
|
||||
created: new Date(),
|
||||
customData: {
|
||||
signatureText: text,
|
||||
signatureType: 'text'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -235,6 +270,11 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
switch (params.signatureType) {
|
||||
case 'image':
|
||||
if (params.signatureData) {
|
||||
const annotationId = uuidV4();
|
||||
|
||||
// Store image data in our persistent store
|
||||
storeImageData(annotationId, params.signatureData);
|
||||
|
||||
annotationApi.createAnnotation(page, {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
rect: {
|
||||
@@ -244,8 +284,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
author: 'Digital Signature',
|
||||
subject: `Digital Signature - ${params.reason || 'Document signing'}`,
|
||||
pageIndex: page,
|
||||
id: uuidV4(),
|
||||
id: annotationId,
|
||||
created: new Date(),
|
||||
// Store image data in multiple places to ensure history captures it
|
||||
imageSrc: params.signatureData,
|
||||
contents: params.signatureData, // Some annotation systems use contents
|
||||
data: params.signatureData, // Try data field
|
||||
imageData: params.signatureData, // Try imageData field
|
||||
appearance: params.signatureData // Try appearance field
|
||||
});
|
||||
|
||||
// Switch to select mode after placing signature so it can be easily deleted
|
||||
@@ -274,6 +320,10 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
|
||||
pageIndex: page,
|
||||
id: uuidV4(),
|
||||
created: new Date(),
|
||||
customData: {
|
||||
signatureText: params.signerName,
|
||||
signatureType: 'text'
|
||||
}
|
||||
});
|
||||
|
||||
// Switch to select mode after placing signature so it can be easily deleted
|
||||
|
||||
Reference in New Issue
Block a user