mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02: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:
parent
bac61c7e9e
commit
a12e457577
@ -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
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect } from 'react';
|
||||
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
|
||||
import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
||||
import { HistoryAPI } from '../components/viewer/HistoryAPIBridge';
|
||||
|
||||
// Signature state interface
|
||||
interface SignatureState {
|
||||
@ -19,11 +20,16 @@ interface SignatureActions {
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
storeImageData: (id: string, data: string) => void;
|
||||
getImageData: (id: string) => string | undefined;
|
||||
}
|
||||
|
||||
// Combined context interface
|
||||
interface SignatureContextValue extends SignatureState, SignatureActions {
|
||||
signatureApiRef: React.RefObject<SignatureAPI | null>;
|
||||
historyApiRef: React.RefObject<HistoryAPI | null>;
|
||||
}
|
||||
|
||||
// Create context
|
||||
@ -39,6 +45,8 @@ const initialState: SignatureState = {
|
||||
export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [state, setState] = useState<SignatureState>(initialState);
|
||||
const signatureApiRef = useRef<SignatureAPI>(null);
|
||||
const historyApiRef = useRef<HistoryAPI>(null);
|
||||
const imageDataStore = useRef<Map<string, string>>(new Map());
|
||||
|
||||
// Actions
|
||||
const setSignatureConfig = useCallback((config: SignParameters | null) => {
|
||||
@ -100,12 +108,36 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children
|
||||
}
|
||||
}, []);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (historyApiRef.current) {
|
||||
historyApiRef.current.undo();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (historyApiRef.current) {
|
||||
historyApiRef.current.redo();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const storeImageData = useCallback((id: string, data: string) => {
|
||||
console.log('Storing image data for annotation:', id, data.length, 'chars');
|
||||
imageDataStore.current.set(id, data);
|
||||
}, []);
|
||||
|
||||
const getImageData = useCallback((id: string) => {
|
||||
const data = imageDataStore.current.get(id);
|
||||
console.log('Retrieving image data for annotation:', id, data?.length, 'chars');
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
|
||||
// No auto-activation - all modes use manual buttons
|
||||
|
||||
const contextValue: SignatureContextValue = {
|
||||
...state,
|
||||
signatureApiRef,
|
||||
historyApiRef,
|
||||
setSignatureConfig,
|
||||
setPlacementMode,
|
||||
activateDrawMode,
|
||||
@ -113,6 +145,10 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children
|
||||
activateSignaturePlacementMode,
|
||||
activateDeleteMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
storeImageData,
|
||||
getImageData,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -12,7 +12,7 @@ import { useSignature } from "../contexts/SignatureContext";
|
||||
const Sign = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setWorkbench } = useNavigation();
|
||||
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings } = useSignature();
|
||||
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo } = useSignature();
|
||||
|
||||
// Manual sync function
|
||||
const syncSignatureConfig = () => {
|
||||
@ -65,6 +65,8 @@ const Sign = (props: BaseToolProps) => {
|
||||
onActivateSignaturePlacement={handleSignaturePlacement}
|
||||
onDeactivateSignature={deactivateDrawMode}
|
||||
onUpdateDrawSettings={updateDrawSettings}
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user