diff --git a/CLAUDE.md b/CLAUDE.md index bc6af38c9..d111f8da3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -192,6 +192,11 @@ return useToolOperation({ - **Preview System**: Tool results can be previewed without polluting file context (Split tool example) - **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing +## Translation Rules + +- **CRITICAL**: Always update translations in `en-GB` only, never `en-US` +- Translation files are located in `frontend/public/locales/` + ## Important Notes - **Java Version**: Minimum JDK 17, supports and recommends JDK 21 diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index d63b1c6c4..405643877 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -1916,6 +1916,7 @@ fileManager.storageError=Storage error occurred fileManager.storageLow=Storage is running low. Consider removing old files. fileManager.uploadError=Failed to upload some files. fileManager.supportMessage=Powered by browser database storage for unlimited capacity +fileManager.loadingFiles=Loading files... # Page Editor pageEditor.title=Page Editor diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index aa8ed7e13..2772ad15e 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3080,7 +3080,12 @@ "panMode": "Pan Mode", "rotateLeft": "Rotate Left", "rotateRight": "Rotate Right", - "toggleSidebar": "Toggle Sidebar" + "toggleSidebar": "Toggle Sidebar", + "exportSelected": "Export Selected Pages", + "toggleAnnotations": "Toggle Annotations Visibility", + "annotationMode": "Toggle Annotation Mode", + "draw": "Draw", + "save": "Save" }, "search": { "title": "Search PDF", @@ -3149,6 +3154,9 @@ "addFiles": "Add Files", "dragFilesInOrClick": "Drag files in or click \"Add Files\" to browse" }, + "fileEditor": { + "addFiles": "Add Files" + }, "fileManager": { "title": "Upload PDF Files", "subtitle": "Add files to your storage for easy access across tools", @@ -3184,7 +3192,6 @@ "googleDriveShort": "Drive", "myFiles": "My Files", "noRecentFiles": "No recent files found", - "dropFilesHint": "Drop files here to upload", "googleDriveNotAvailable": "Google Drive integration not available", "openFiles": "Open Files", "openFile": "Open File", diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index e75b95e28..fb26a3a3a 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -20,7 +20,7 @@ const FileManager: React.FC = ({ selectedTool }) => { const [isDragging, setIsDragging] = useState(false); const [isMobile, setIsMobile] = useState(false); - const { loadRecentFiles, handleRemoveFile } = useFileManager(); + const { loadRecentFiles, handleRemoveFile, loading } = useFileManager(); // File management handlers const isFileSupported = useCallback((fileName: string) => { @@ -123,7 +123,6 @@ const FileManager: React.FC = ({ selectedTool }) => { onDrop={handleNewFileUpload} onDragEnter={() => setIsDragging(true)} onDragLeave={() => setIsDragging(false)} - accept={{}} multiple={true} activateOnClick={false} style={{ @@ -147,6 +146,7 @@ const FileManager: React.FC = ({ selectedTool }) => { onFileRemove={handleRemoveFileByIndex} modalHeight={modalHeight} refreshRecentFiles={refreshRecentFiles} + isLoading={loading} > {isMobile ? : } diff --git a/frontend/src/components/fileEditor/AddFileCard.tsx b/frontend/src/components/fileEditor/AddFileCard.tsx new file mode 100644 index 000000000..dbc319c68 --- /dev/null +++ b/frontend/src/components/fileEditor/AddFileCard.tsx @@ -0,0 +1,177 @@ +import React, { useRef, useState } from 'react'; +import { Button, Group, useMantineColorScheme } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import AddIcon from '@mui/icons-material/Add'; +import { useFilesModalContext } from '../../contexts/FilesModalContext'; +import LocalIcon from '../shared/LocalIcon'; +import { BASE_PATH } from '../../constants/app'; +import styles from './FileEditor.module.css'; + +interface AddFileCardProps { + onFileSelect: (files: File[]) => void; + accept?: string; + multiple?: boolean; +} + +const AddFileCard = ({ + onFileSelect, + accept = "*/*", + multiple = true +}: AddFileCardProps) => { + const { t } = useTranslation(); + const fileInputRef = useRef(null); + const { openFilesModal } = useFilesModalContext(); + const { colorScheme } = useMantineColorScheme(); + const [isUploadHover, setIsUploadHover] = useState(false); + + const handleCardClick = () => { + openFilesModal(); + }; + + const handleNativeUploadClick = (e: React.MouseEvent) => { + e.stopPropagation(); + fileInputRef.current?.click(); + }; + + const handleOpenFilesModal = (e: React.MouseEvent) => { + e.stopPropagation(); + openFilesModal(); + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const files = Array.from(event.target.files || []); + if (files.length > 0) { + onFileSelect(files); + } + // Reset input so same files can be selected again + event.target.value = ''; + }; + + return ( + <> + + +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCardClick(); + } + }} + > + {/* Header bar - matches FileEditorThumbnail structure */} +
+
+ +
+
+ {t('fileEditor.addFiles', 'Add Files')} +
+
+
+ + {/* Main content area */} +
+ {/* Stirling PDF Branding */} + + Stirling PDF + + + {/* Add Files + Native Upload Buttons - styled like LandingPage */} +
setIsUploadHover(false)} + > + + +
+ + {/* Instruction Text */} + + {t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')} + +
+
+ + ); +}; + +export default AddFileCard; \ No newline at end of file diff --git a/frontend/src/components/fileEditor/FileEditor.module.css b/frontend/src/components/fileEditor/FileEditor.module.css index ccabc2fa7..173738c29 100644 --- a/frontend/src/components/fileEditor/FileEditor.module.css +++ b/frontend/src/components/fileEditor/FileEditor.module.css @@ -304,4 +304,84 @@ /* Light mode selected header stroke override */ :global([data-mantine-color-scheme="light"]) .card[data-selected="true"] { outline-color: #3B4B6E; +} + +/* ========================= + Add File Card Styles + ========================= */ + +.addFileCard { + background: var(--file-card-bg); + border: 2px dashed var(--border-default); + border-radius: 0.0625rem; + cursor: pointer; + transition: all 0.18s ease; + max-width: 100%; + max-height: 100%; + overflow: hidden; + margin-left: 0.5rem; + margin-right: 0.5rem; + opacity: 0.7; +} + +.addFileCard:hover { + opacity: 1; + border-color: var(--color-blue-500); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.addFileCard:focus { + outline: 2px solid var(--color-blue-500); + outline-offset: 2px; +} + +.addFileHeader { + background: var(--bg-subtle); + color: var(--text-secondary); + border-bottom: 1px solid var(--border-default); +} + +.addFileCard:hover .addFileHeader { + background: var(--color-blue-500); + color: white; +} + +.addFileContent { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1.5rem 1rem; + gap: 0.5rem; +} + +.addFileIcon { + display: flex; + align-items: center; + justify-content: center; + width: 5rem; + height: 5rem; + border-radius: 50%; + background: var(--bg-subtle); + transition: background-color 0.18s ease; +} + +.addFileCard:hover .addFileIcon { + background: var(--color-blue-50); +} + +.addFileText { + font-weight: 500; + transition: color 0.18s ease; +} + +.addFileCard:hover .addFileText { + color: var(--text-primary); +} + +.addFileSubtext { + font-size: 0.875rem; + opacity: 0.8; } \ No newline at end of file diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 8dbd83480..00bf22480 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -8,6 +8,7 @@ import { useNavigationActions } from '../../contexts/NavigationContext'; import { zipFileService } from '../../services/zipFileService'; import { detectFileExtension } from '../../utils/fileUtils'; import FileEditorThumbnail from './FileEditorThumbnail'; +import AddFileCard from './AddFileCard'; import FilePickerModal from '../shared/FilePickerModal'; import SkeletonLoader from '../shared/SkeletonLoader'; import { FileId, StirlingFile } from '../../types/fileContext'; @@ -171,8 +172,8 @@ const FileEditor = ({ // Process all extracted files if (allExtractedFiles.length > 0) { - // Add files to context (they will be processed automatically) - await addFiles(allExtractedFiles); + // Add files to context and select them automatically + await addFiles(allExtractedFiles, { selectFiles: true }); showStatus(`Added ${allExtractedFiles.length} files`, 'success'); } } catch (err) { @@ -405,6 +406,14 @@ const FileEditor = ({ pointerEvents: 'auto' }} > + {/* Add File Card - only show when files exist */} + {activeStirlingFileStubs.length > 0 && ( + + )} + {activeStirlingFileStubs.map((record, index) => { return ( { + const { t } = useTranslation(); + const { colorScheme } = useMantineColorScheme(); + const { onLocalFileClick } = useFileManagerContext(); + const [isUploadHover, setIsUploadHover] = useState(false); + + const handleUploadClick = () => { + onLocalFileClick(); + }; + + return ( +
+ {/* Container */} +
+ {/* No Recent Files Message */} + + + + {t('fileManager.noRecentFiles', 'No recent files')} + + + + {/* Stirling PDF Logo */} + + Stirling PDF + + + {/* Upload Button */} +
setIsUploadHover(false)} + > + +
+ + {/* Instruction Text */} + + {t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')} + +
+
+ ); +}; + +export default EmptyFilesState; diff --git a/frontend/src/components/fileManager/FileListArea.tsx b/frontend/src/components/fileManager/FileListArea.tsx index 842d0bf0e..556ecc4f1 100644 --- a/frontend/src/components/fileManager/FileListArea.tsx +++ b/frontend/src/components/fileManager/FileListArea.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Center, ScrollArea, Text, Stack } from '@mantine/core'; import CloudIcon from '@mui/icons-material/Cloud'; -import HistoryIcon from '@mui/icons-material/History'; import { useTranslation } from 'react-i18next'; import FileListItem from './FileListItem'; import FileHistoryGroup from './FileHistoryGroup'; +import EmptyFilesState from './EmptyFilesState'; import { useFileManagerContext } from '../../contexts/FileManagerContext'; interface FileListAreaProps { @@ -29,6 +29,7 @@ const FileListArea: React.FC = ({ onFileDoubleClick, onDownloadSingle, isFileSupported, + isLoading, } = useFileManagerContext(); const { t } = useTranslation(); @@ -43,15 +44,11 @@ const FileListArea: React.FC = ({ scrollbarSize={8} > - {recentFiles.length === 0 ? ( + {recentFiles.length === 0 && !isLoading ? ( + + ) : recentFiles.length === 0 && isLoading ? (
- - - {t('fileManager.noRecentFiles', 'No recent files')} - - {t('fileManager.dropFilesHint', 'Drop files anywhere to upload')} - - + {t('fileManager.loadingFiles', 'Loading files...')}
) : ( filteredFiles.map((file, index) => { diff --git a/frontend/src/components/fileManager/HiddenFileInput.tsx b/frontend/src/components/fileManager/HiddenFileInput.tsx index 05f35aae1..8dee9e278 100644 --- a/frontend/src/components/fileManager/HiddenFileInput.tsx +++ b/frontend/src/components/fileManager/HiddenFileInput.tsx @@ -9,7 +9,6 @@ const HiddenFileInput: React.FC = () => { ref={fileInputRef} type="file" multiple={true} - accept={["*/*"] as any} onChange={onFileInputChange} style={{ display: 'none' }} data-testid="file-input" diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx index 787b0e565..5f1fe8d8e 100644 --- a/frontend/src/components/shared/LandingPage.tsx +++ b/frontend/src/components/shared/LandingPage.tsx @@ -41,7 +41,7 @@ const LandingPage = () => { {/* White PDF Page Background */} { ref={fileInputRef} type="file" multiple - accept=".pdf,.zip" + accept="*/*" onChange={handleFileSelect} style={{ display: 'none' }} /> diff --git a/frontend/src/components/shared/NavigationWarningModal.tsx b/frontend/src/components/shared/NavigationWarningModal.tsx index e9622d6d4..203e66ff7 100644 --- a/frontend/src/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/components/shared/NavigationWarningModal.tsx @@ -8,7 +8,7 @@ interface NavigationWarningModalProps { } const NavigationWarningModal = ({ - onApplyAndContinue, + onApplyAndContinue: _onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => { @@ -30,13 +30,6 @@ const NavigationWarningModal = ({ confirmNavigation(); }; - const _handleApplyAndContinue = async () => { - if (onApplyAndContinue) { - await onApplyAndContinue(); - } - setHasUnsavedChanges(false); - confirmNavigation(); - }; const handleExportAndContinue = async () => { if (onExportAndContinue) { @@ -85,7 +78,7 @@ const NavigationWarningModal = ({ {/* TODO:: Add this back in when it works */} - {/* {onApplyAndContinue && ( + {/* {_onApplyAndContinue && (
diff --git a/frontend/src/components/shared/rightRail/RightRail.css b/frontend/src/components/shared/rightRail/RightRail.css index 8d01052a9..d310edc40 100644 --- a/frontend/src/components/shared/rightRail/RightRail.css +++ b/frontend/src/components/shared/rightRail/RightRail.css @@ -67,7 +67,7 @@ } .right-rail-slot.visible { - max-height: 18rem; /* increased to fit additional controls + divider */ + max-height: 40rem; /* increased to fit additional controls + divider */ opacity: 1; } @@ -77,14 +77,14 @@ opacity: 0; } 100% { - max-height: 18rem; + max-height: 40rem; opacity: 1; } } @keyframes rightRailShrinkUp { 0% { - max-height: 18rem; + max-height: 40rem; opacity: 1; } 100% { diff --git a/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx new file mode 100644 index 000000000..92b778e44 --- /dev/null +++ b/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect } from 'react'; +import { ActionIcon, Popover } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import LocalIcon from '../LocalIcon'; +import { Tooltip } from '../Tooltip'; +import { ViewerContext } from '../../../contexts/ViewerContext'; +import { useSignature } from '../../../contexts/SignatureContext'; +import { ColorSwatchButton, ColorPicker } from '../../annotation/shared/ColorPicker'; +import { useFileState, useFileContext } from '../../../contexts/FileContext'; +import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils'; +import { createProcessedFile } from '../../../contexts/file/fileActions'; +import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext'; + +interface ViewerAnnotationControlsProps { + currentView: string; +} + +export default function ViewerAnnotationControls({ currentView }: ViewerAnnotationControlsProps) { + const { t } = useTranslation(); + const [selectedColor, setSelectedColor] = useState('#000000'); + const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); + const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false); + + // Viewer context for PDF controls - safely handle when not available + const viewerContext = React.useContext(ViewerContext); + + // Signature context for accessing drawing API + const { signatureApiRef } = useSignature(); + + // File state for save functionality + const { state, selectors } = useFileState(); + const { actions: fileActions } = useFileContext(); + const activeFiles = selectors.getFiles(); + + // Turn off annotation mode when switching away from viewer + useEffect(() => { + if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) { + viewerContext.setAnnotationMode(false); + } + }, [currentView, viewerContext]); + + return ( + <> + {/* Annotation Visibility Toggle */} + + { + viewerContext?.toggleAnnotationsVisibility(); + }} + disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode} + > + + + + + {/* Annotation Mode Toggle with Drawing Controls */} + {viewerContext?.isAnnotationMode ? ( + // When active: Show color picker on hover +
setIsHoverColorPickerOpen(true)} + onMouseLeave={() => setIsHoverColorPickerOpen(false)} + style={{ display: 'inline-flex' }} + > + setIsHoverColorPickerOpen(false)} + position="left" + withArrow + shadow="md" + offset={8} + > + + { + viewerContext?.toggleAnnotationMode(); + setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off + // Deactivate drawing tool when exiting annotation mode + if (signatureApiRef?.current) { + try { + signatureApiRef.current.deactivateTools(); + } catch (error) { + console.log('Signature API not ready:', error); + } + } + }} + disabled={currentView !== 'viewer'} + aria-label="Drawing mode active" + > + + + + +
+
+
Drawing Color
+ { + setIsHoverColorPickerOpen(false); // Close hover picker + setIsColorPickerOpen(true); // Open main color picker modal + }} + /> +
+
+
+
+
+ ) : ( + // When inactive: Show "Draw" tooltip + + { + viewerContext?.toggleAnnotationMode(); + // Activate ink drawing tool when entering annotation mode + if (signatureApiRef?.current && currentView === 'viewer') { + try { + signatureApiRef.current.activateDrawMode(); + signatureApiRef.current.updateDrawSettings(selectedColor, 2); + } catch (error) { + console.log('Signature API not ready:', error); + } + } + }} + disabled={currentView !== 'viewer'} + aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'} + > + + + + )} + + {/* Save PDF with Annotations */} + + { + if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') { + try { + const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy(); + if (pdfArrayBuffer) { + // Create new File object with flattened annotations + const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); + + // Get the original file name or use a default + const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf'; + const newFile = new File([blob], originalFileName, { type: 'application/pdf' }); + + // Replace the current file in context with the saved version (exact same logic as Sign tool) + if (activeFiles.length > 0) { + // Generate thumbnail and metadata for the saved file + const thumbnailResult = await generateThumbnailWithMetadata(newFile); + const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail); + + // Get current file info + const currentFileIds = state.files.ids; + if (currentFileIds.length > 0) { + const currentFileId = currentFileIds[0]; + const currentRecord = selectors.getStirlingFileStub(currentFileId); + + if (!currentRecord) { + console.error('No file record found for:', currentFileId); + return; + } + + // Create output stub and file (exact same as Sign tool) + const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata); + const outputStirlingFile = createStirlingFile(newFile, outputStub.id); + + // Replace the original file with the saved version + await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]); + } + } + } + } catch (error) { + console.error('Error saving PDF:', error); + } + } + }} + disabled={currentView !== 'viewer'} + > + + + + + {/* Color Picker Modal */} + setIsColorPickerOpen(false)} + selectedColor={selectedColor} + onColorChange={(color) => { + setSelectedColor(color); + // Update drawing tool color if annotation mode is active + if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') { + try { + signatureApiRef.current.updateDrawSettings(color, 2); + } catch (error) { + console.log('Unable to update drawing settings:', error); + } + } + }} + title="Choose Drawing Color" + /> + + ); +} \ No newline at end of file diff --git a/frontend/src/components/viewer/EmbedPdfViewer.tsx b/frontend/src/components/viewer/EmbedPdfViewer.tsx index 986ac406f..f8a7102fa 100644 --- a/frontend/src/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/components/viewer/EmbedPdfViewer.tsx @@ -29,16 +29,19 @@ const EmbedPdfViewerContent = ({ const { colorScheme: _colorScheme } = useMantineColorScheme(); const viewerRef = React.useRef(null); const [isViewerHovered, setIsViewerHovered] = React.useState(false); - const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState } = useViewer(); + const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, isAnnotationMode, isAnnotationsVisible } = useViewer(); const scrollState = getScrollState(); const zoomState = getZoomState(); const spreadState = getSpreadState(); - // Check if we're in signature mode + // Check if we're in signature mode OR viewer annotation mode const { selectedTool } = useNavigationState(); const isSignatureMode = selectedTool === 'sign'; + // Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations + const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible; + // Get signature context const { signatureApiRef, historyApiRef } = useSignature(); @@ -186,7 +189,7 @@ const EmbedPdfViewerContent = ({ } historyApiRef={historyApiRef as React.RefObject} onSignatureAdded={() => { diff --git a/frontend/src/components/viewer/LocalEmbedPDF.tsx b/frontend/src/components/viewer/LocalEmbedPDF.tsx index 2985bae84..842c62945 100644 --- a/frontend/src/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/components/viewer/LocalEmbedPDF.tsx @@ -42,13 +42,13 @@ import { ExportAPIBridge } from './ExportAPIBridge'; interface LocalEmbedPDFProps { file?: File | Blob; url?: string | null; - enableSignature?: boolean; + enableAnnotations?: boolean; onSignatureAdded?: (annotation: any) => void; signatureApiRef?: React.RefObject; historyApiRef?: React.RefObject; } -export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { +export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { const [pdfUrl, setPdfUrl] = useState(null); const [, setAnnotations] = useState>([]); @@ -93,10 +93,10 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA createPluginRegistration(SelectionPluginPackage), // Register history plugin for undo/redo (recommended for annotations) - ...(enableSignature ? [createPluginRegistration(HistoryPluginPackage)] : []), + ...(enableAnnotations ? [createPluginRegistration(HistoryPluginPackage)] : []), // Register annotation plugin (depends on InteractionManager, Selection, History) - ...(enableSignature ? [createPluginRegistration(AnnotationPluginPackage, { + ...(enableAnnotations ? [createPluginRegistration(AnnotationPluginPackage, { annotationAuthor: 'Digital Signature', autoCommit: true, deactivateToolAfterCreate: false, @@ -194,7 +194,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA { + onInitialized={enableAnnotations ? async (registry) => { const annotationPlugin = registry.getPlugin('annotation'); if (!annotationPlugin || !annotationPlugin.provides) return; @@ -265,8 +265,8 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA - {enableSignature && } - {enableSignature && } + {enableAnnotations && } + {enableAnnotations && } {/* Annotation layer for signatures (only when enabled) */} - {enableSignature && ( + {enableAnnotations && ( ); -} +} \ No newline at end of file diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx index 9b541df8a..59fbe43e6 100644 --- a/frontend/src/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx @@ -3,6 +3,7 @@ import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react'; import { PdfAnnotationSubtype, PdfStandardFont, PdfTextAlignment, PdfVerticalAlignment, uuidV4 } from '@embedpdf/models'; import { SignParameters } from '../../hooks/tools/sign/useSignParameters'; import { useSignature } from '../../contexts/SignatureContext'; +import { useViewer } from '../../contexts/ViewerContext'; export interface SignatureAPI { addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void; @@ -20,11 +21,12 @@ export interface SignatureAPI { export const SignatureAPIBridge = forwardRef(function SignatureAPIBridge(_, ref) { const { provides: annotationApi } = useAnnotationCapability(); const { signatureConfig, storeImageData, isPlacementMode } = useSignature(); + const { isAnnotationMode } = useViewer(); - // Enable keyboard deletion of selected annotations - only when in signature placement mode + // Enable keyboard deletion of selected annotations - when in signature placement mode or viewer annotation mode useEffect(() => { - if (!annotationApi || !isPlacementMode) return; + if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Delete' || event.key === 'Backspace') { @@ -65,7 +67,7 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [annotationApi, storeImageData, isPlacementMode]); + }, [annotationApi, storeImageData, isPlacementMode, isAnnotationMode]); useImperativeHandle(ref, () => ({ addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => { diff --git a/frontend/src/components/viewer/ZoomAPIBridge.tsx b/frontend/src/components/viewer/ZoomAPIBridge.tsx index 8cb0f4fcc..fa47b1c8a 100644 --- a/frontend/src/components/viewer/ZoomAPIBridge.tsx +++ b/frontend/src/components/viewer/ZoomAPIBridge.tsx @@ -15,7 +15,19 @@ export function ZoomAPIBridge() { if (zoom && !hasSetInitialZoom.current) { hasSetInitialZoom.current = true; setTimeout(() => { - zoom.requestZoom(1.4); + try { + zoom.requestZoom(1.4); + } catch (error) { + console.log('Zoom initialization delayed, viewport not ready:', error); + // Retry after a longer delay + setTimeout(() => { + try { + zoom.requestZoom(1.4); + } catch (retryError) { + console.log('Zoom initialization failed:', retryError); + } + }, 200); + } }, 50); } }, [zoom]); diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index 6f3afe21b..fb82e9071 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -18,6 +18,7 @@ interface FileManagerContextValue { expandedFileIds: Set; fileGroups: Map; loadedHistoryFiles: Map; + isLoading: boolean; // Handlers onSourceChange: (source: 'recent' | 'local' | 'drive') => void; @@ -58,6 +59,7 @@ interface FileManagerProviderProps { onFileRemove: (index: number) => void; modalHeight: string; refreshRecentFiles: () => Promise; + isLoading: boolean; } export const FileManagerProvider: React.FC = ({ @@ -71,6 +73,7 @@ export const FileManagerProvider: React.FC = ({ onFileRemove, modalHeight, refreshRecentFiles, + isLoading, }) => { const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent'); const [selectedFileIds, setSelectedFileIds] = useState([]); @@ -574,6 +577,7 @@ export const FileManagerProvider: React.FC = ({ expandedFileIds, fileGroups, loadedHistoryFiles, + isLoading, // Handlers onSourceChange: handleSourceChange, @@ -607,6 +611,7 @@ export const FileManagerProvider: React.FC = ({ expandedFileIds, fileGroups, loadedHistoryFiles, + isLoading, handleSourceChange, handleLocalFileClick, handleFileSelect, diff --git a/frontend/src/contexts/ViewerContext.tsx b/frontend/src/contexts/ViewerContext.tsx index 11df4b027..895776f96 100644 --- a/frontend/src/contexts/ViewerContext.tsx +++ b/frontend/src/contexts/ViewerContext.tsx @@ -123,6 +123,15 @@ interface ViewerContextType { isThumbnailSidebarVisible: boolean; toggleThumbnailSidebar: () => void; + // Annotation visibility toggle + isAnnotationsVisible: boolean; + toggleAnnotationsVisibility: () => void; + + // Annotation/drawing mode for viewer + isAnnotationMode: boolean; + setAnnotationMode: (enabled: boolean) => void; + toggleAnnotationMode: () => void; + // State getters - read current state from bridges getScrollState: () => ScrollState; getZoomState: () => ZoomState; @@ -208,6 +217,8 @@ interface ViewerProviderProps { export const ViewerProvider: React.FC = ({ children }) => { // UI state - only state directly managed by this context const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false); + const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true); + const [isAnnotationMode, setIsAnnotationModeState] = useState(false); // Get current navigation state to check if we're in sign mode useNavigation(); @@ -268,6 +279,18 @@ export const ViewerProvider: React.FC = ({ children }) => { setIsThumbnailSidebarVisible(prev => !prev); }; + const toggleAnnotationsVisibility = () => { + setIsAnnotationsVisible(prev => !prev); + }; + + const setAnnotationMode = (enabled: boolean) => { + setIsAnnotationModeState(enabled); + }; + + const toggleAnnotationMode = () => { + setIsAnnotationModeState(prev => !prev); + }; + // State getters - read from bridge refs const getScrollState = (): ScrollState => { return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 }; @@ -547,6 +570,13 @@ export const ViewerProvider: React.FC = ({ children }) => { isThumbnailSidebarVisible, toggleThumbnailSidebar, + // Annotation controls + isAnnotationsVisible, + toggleAnnotationsVisibility, + isAnnotationMode, + setAnnotationMode, + toggleAnnotationMode, + // State getters getScrollState, getZoomState, diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index 6ae14724f..1c817132a 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -476,7 +476,6 @@ export async function addStirlingFileStubs( await addFilesMutex.lock(); try { - if (DEBUG) console.log(`📄 addStirlingFileStubs: Adding ${stirlingFileStubs.length} StirlingFileStubs preserving metadata`); const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId); const validStubs: StirlingFileStub[] = []; @@ -515,14 +514,12 @@ export async function addStirlingFileStubs( record.processedFile.totalPages !== record.processedFile.pages.length; if (needsProcessing) { - if (DEBUG) console.log(`📄 addStirlingFileStubs: Regenerating processedFile for ${record.name}`); // Use centralized metadata generation function const processedFileMetadata = await generateProcessedFileMetadata(stirlingFile); if (processedFileMetadata) { record.processedFile = processedFileMetadata; record.thumbnailUrl = processedFileMetadata.thumbnailUrl; // Update thumbnail if needed - if (DEBUG) console.log(`📄 addStirlingFileStubs: Regenerated processedFile for ${record.name} with ${processedFileMetadata.totalPages} pages`); } else { // Fallback for files that couldn't be processed if (DEBUG) console.warn(`📄 addStirlingFileStubs: Failed to regenerate processedFile for ${record.name}`); @@ -541,7 +538,6 @@ export async function addStirlingFileStubs( // Dispatch ADD_FILES action if we have new files if (validStubs.length > 0) { dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs: validStubs } }); - if (DEBUG) console.log(`📄 addStirlingFileStubs: Successfully added ${validStubs.length} files with preserved metadata`); } return loadedFiles; diff --git a/frontend/src/hooks/useThumbnailGeneration.ts b/frontend/src/hooks/useThumbnailGeneration.ts index 6a22fbcc9..694fdeeb7 100644 --- a/frontend/src/hooks/useThumbnailGeneration.ts +++ b/frontend/src/hooks/useThumbnailGeneration.ts @@ -70,7 +70,6 @@ async function processRequestQueue() { const pageNumbers = requests.map(req => req.pageNumber); const arrayBuffer = await file.arrayBuffer(); - console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`); // Use quickKey for PDF document caching (same metadata, consistent format) const fileId = createQuickKey(file) as FileId; @@ -80,9 +79,8 @@ async function processRequestQueue() { arrayBuffer, pageNumbers, { scale: 1.0, quality: 0.8, batchSize: BATCH_SIZE }, - (progress) => { + (_progress) => { // Optional: Could emit progress events here for UI feedback - console.log(`📸 Batch progress: ${progress.completed}/${progress.total} thumbnails generated`); } );