mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Feature/v2/navigate save prompt (#4586)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
parent
8aa6aff53a
commit
f9ac1bd62e
@ -5,6 +5,8 @@ import { useNavigationGuard } from "../../contexts/NavigationContext";
|
|||||||
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||||
|
import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
|
||||||
|
import { createStirlingFilesAndStubs } from "../../services/fileStubHelpers";
|
||||||
// Thumbnail generation is now handled by individual PageThumbnail components
|
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||||
import './PageEditor.module.css';
|
import './PageEditor.module.css';
|
||||||
import PageThumbnail from './PageThumbnail';
|
import PageThumbnail from './PageThumbnail';
|
||||||
@ -524,66 +526,38 @@ const PageEditor = ({
|
|||||||
try {
|
try {
|
||||||
// Step 1: Apply DOM changes to document state first
|
// Step 1: Apply DOM changes to document state first
|
||||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||||
mergedPdfDocument || displayDocument, // Original order
|
mergedPdfDocument || displayDocument,
|
||||||
displayDocument, // Current display order (includes reordering)
|
displayDocument,
|
||||||
splitPositions // Position-based splits
|
splitPositions
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Check if we have multiple documents (splits) or single document
|
// Step 2: Export to files
|
||||||
if (Array.isArray(processedDocuments)) {
|
const sourceFiles = getSourceFiles();
|
||||||
// Multiple documents (splits) - export as ZIP
|
const exportFilename = getExportFilename();
|
||||||
const blobs: Blob[] = [];
|
const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
|
||||||
const filenames: string[] = [];
|
|
||||||
|
|
||||||
const sourceFiles = getSourceFiles();
|
// Step 3: Download
|
||||||
const baseExportFilename = getExportFilename();
|
if (files.length > 1) {
|
||||||
const baseName = baseExportFilename.replace(/\.pdf$/i, '');
|
// Multiple files - create ZIP
|
||||||
|
|
||||||
for (let i = 0; i < processedDocuments.length; i++) {
|
|
||||||
const doc = processedDocuments[i];
|
|
||||||
const partFilename = `${baseName}_part_${i + 1}.pdf`;
|
|
||||||
|
|
||||||
const result = sourceFiles
|
|
||||||
? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename })
|
|
||||||
: await pdfExportService.exportPDF(doc, [], { filename: partFilename });
|
|
||||||
blobs.push(result.blob);
|
|
||||||
filenames.push(result.filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ZIP file
|
|
||||||
const JSZip = await import('jszip');
|
const JSZip = await import('jszip');
|
||||||
const zip = new JSZip.default();
|
const zip = new JSZip.default();
|
||||||
|
|
||||||
blobs.forEach((blob, index) => {
|
files.forEach((file) => {
|
||||||
zip.file(filenames[index], blob);
|
zip.file(file.name, file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
|
const exportFilename = getExportFilename();
|
||||||
|
const zipFilename = exportFilename.replace(/\.pdf$/i, '.zip');
|
||||||
|
|
||||||
pdfExportService.downloadFile(zipBlob, zipFilename);
|
pdfExportService.downloadFile(zipBlob, zipFilename);
|
||||||
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
|
||||||
} else {
|
} else {
|
||||||
// Single document - regular export
|
// Single file - download directly
|
||||||
const sourceFiles = getSourceFiles();
|
const file = files[0];
|
||||||
const exportFilename = getExportFilename();
|
pdfExportService.downloadFile(file, file.name);
|
||||||
const result = sourceFiles
|
|
||||||
? await pdfExportService.exportPDFMultiFile(
|
|
||||||
processedDocuments,
|
|
||||||
sourceFiles,
|
|
||||||
[],
|
|
||||||
{ selectedOnly: false, filename: exportFilename }
|
|
||||||
)
|
|
||||||
: await pdfExportService.exportPDF(
|
|
||||||
processedDocuments,
|
|
||||||
[],
|
|
||||||
{ selectedOnly: false, filename: exportFilename }
|
|
||||||
);
|
|
||||||
|
|
||||||
pdfExportService.downloadFile(result.blob, result.filename);
|
|
||||||
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
@ -592,21 +566,39 @@ const PageEditor = ({
|
|||||||
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||||
|
|
||||||
// Apply DOM changes to document state using dedicated service
|
// Apply DOM changes to document state using dedicated service
|
||||||
const applyChanges = useCallback(() => {
|
const applyChanges = useCallback(async () => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
// Pass current display document (which includes reordering) to get both reordering AND DOM changes
|
setExportLoading(true);
|
||||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
try {
|
||||||
mergedPdfDocument || displayDocument, // Original order
|
// Step 1: Apply DOM changes to document state first
|
||||||
displayDocument, // Current display order (includes reordering)
|
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||||
splitPositions // Position-based splits
|
mergedPdfDocument || displayDocument,
|
||||||
);
|
displayDocument,
|
||||||
|
splitPositions
|
||||||
|
);
|
||||||
|
|
||||||
// For apply changes, we only set the first document if it's an array (splits shouldn't affect document state)
|
// Step 2: Export to files
|
||||||
const documentToSet = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments;
|
const sourceFiles = getSourceFiles();
|
||||||
setEditedDocument(documentToSet);
|
const exportFilename = getExportFilename();
|
||||||
|
const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
|
||||||
|
|
||||||
}, [displayDocument, mergedPdfDocument, splitPositions]);
|
// Step 3: Create StirlingFiles and stubs for version history
|
||||||
|
const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
|
||||||
|
if (!parentStub) throw new Error('Parent stub not found');
|
||||||
|
|
||||||
|
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(files, parentStub, 'multiTool');
|
||||||
|
|
||||||
|
// Step 4: Consume files (replace in context)
|
||||||
|
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
|
||||||
|
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
setExportLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Apply changes failed:', error);
|
||||||
|
setExportLoading(false);
|
||||||
|
}
|
||||||
|
}, [displayDocument, mergedPdfDocument, splitPositions, activeFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
|
||||||
|
|
||||||
|
|
||||||
const closePdf = useCallback(() => {
|
const closePdf = useCallback(() => {
|
||||||
@ -793,7 +785,7 @@ const PageEditor = ({
|
|||||||
|
|
||||||
<NavigationWarningModal
|
<NavigationWarningModal
|
||||||
onApplyAndContinue={async () => {
|
onApplyAndContinue={async () => {
|
||||||
applyChanges();
|
await applyChanges();
|
||||||
}}
|
}}
|
||||||
onExportAndContinue={async () => {
|
onExportAndContinue={async () => {
|
||||||
await onExportAll();
|
await onExportAll();
|
||||||
|
|||||||
@ -375,6 +375,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
alt={`Page ${page.pageNumber}`}
|
alt={`Page ${page.pageNumber}`}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
data-original-rotation={page.rotation}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|||||||
@ -17,32 +17,34 @@ export class RotatePageCommand extends DOMCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): void {
|
||||||
// Only update DOM for immediate visual feedback
|
|
||||||
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
// Extract current rotation from transform property to match the animated CSS
|
|
||||||
const currentTransform = img.style.transform || '';
|
const currentTransform = img.style.transform || '';
|
||||||
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||||
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||||
const newRotation = currentRotation + this.degrees;
|
let newRotation = currentRotation + this.degrees;
|
||||||
|
|
||||||
|
newRotation = ((newRotation % 360) + 360) % 360;
|
||||||
|
|
||||||
img.style.transform = `rotate(${newRotation}deg)`;
|
img.style.transform = `rotate(${newRotation}deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
// Only update DOM
|
|
||||||
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
// Extract current rotation from transform property
|
|
||||||
const currentTransform = img.style.transform || '';
|
const currentTransform = img.style.transform || '';
|
||||||
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||||
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||||
const previousRotation = currentRotation - this.degrees;
|
let previousRotation = currentRotation - this.degrees;
|
||||||
|
|
||||||
|
previousRotation = ((previousRotation % 360) + 360) % 360;
|
||||||
|
|
||||||
img.style.transform = `rotate(${previousRotation}deg)`;
|
img.style.transform = `rotate(${previousRotation}deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ interface NavigationWarningModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NavigationWarningModal = ({
|
const NavigationWarningModal = ({
|
||||||
onApplyAndContinue: _onApplyAndContinue,
|
onApplyAndContinue,
|
||||||
onExportAndContinue
|
onExportAndContinue
|
||||||
}: NavigationWarningModalProps) => {
|
}: NavigationWarningModalProps) => {
|
||||||
|
|
||||||
@ -30,6 +30,13 @@ const NavigationWarningModal = ({
|
|||||||
confirmNavigation();
|
confirmNavigation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApplyAndContinue = async () => {
|
||||||
|
if (onApplyAndContinue) {
|
||||||
|
await onApplyAndContinue();
|
||||||
|
}
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
confirmNavigation();
|
||||||
|
};
|
||||||
|
|
||||||
const handleExportAndContinue = async () => {
|
const handleExportAndContinue = async () => {
|
||||||
if (onExportAndContinue) {
|
if (onExportAndContinue) {
|
||||||
@ -49,26 +56,25 @@ const NavigationWarningModal = ({
|
|||||||
onClose={handleKeepWorking}
|
onClose={handleKeepWorking}
|
||||||
title={t("unsavedChangesTitle", "Unsaved Changes")}
|
title={t("unsavedChangesTitle", "Unsaved Changes")}
|
||||||
centered
|
centered
|
||||||
size="lg"
|
size="xl"
|
||||||
closeOnClickOutside={false}
|
closeOnClickOutside={false}
|
||||||
closeOnEscape={false}
|
closeOnEscape={false}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="xl">
|
||||||
<Text>
|
<Text size="md">
|
||||||
{t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")}
|
{t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<Group justify="space-between" gap="xl" mt="xl">
|
||||||
|
<Group gap="xl">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
onClick={handleDiscardChanges}
|
||||||
|
>
|
||||||
|
{t("discardChanges", "Discard Changes")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Group justify="space-between" gap="sm">
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
color="red"
|
|
||||||
onClick={handleDiscardChanges}
|
|
||||||
>
|
|
||||||
{t("discardChanges", "Discard Changes")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Group gap="sm">
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="var(--mantine-color-gray-8)"
|
color="var(--mantine-color-gray-8)"
|
||||||
@ -76,9 +82,19 @@ const NavigationWarningModal = ({
|
|||||||
>
|
>
|
||||||
{t("keepWorking", "Keep Working")}
|
{t("keepWorking", "Keep Working")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
{/* TODO:: Add this back in when it works */}
|
<Group gap="xl">
|
||||||
{/* {_onApplyAndContinue && (
|
{onExportAndContinue && (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={handleExportAndContinue}
|
||||||
|
>
|
||||||
|
{t("exportAndContinue", "Export & Continue")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onApplyAndContinue && (
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
@ -86,14 +102,6 @@ const NavigationWarningModal = ({
|
|||||||
>
|
>
|
||||||
{t("applyAndContinue", "Apply & Continue")}
|
{t("applyAndContinue", "Apply & Continue")}
|
||||||
</Button>
|
</Button>
|
||||||
)} */}
|
|
||||||
|
|
||||||
{onExportAndContinue && (
|
|
||||||
<Button
|
|
||||||
onClick={handleExportAndContinue}
|
|
||||||
>
|
|
||||||
{t("exportAndContinue", "Export & Continue")}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Box, Center, Text, ActionIcon } from '@mantine/core';
|
import { Box, Center, Text, ActionIcon } from '@mantine/core';
|
||||||
import { useMantineTheme, useMantineColorScheme } from '@mantine/core';
|
import { useMantineTheme, useMantineColorScheme } from '@mantine/core';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
import { useFileState } from "../../contexts/FileContext";
|
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
import { useViewer } from "../../contexts/ViewerContext";
|
import { useViewer } from "../../contexts/ViewerContext";
|
||||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||||
import { useNavigationState } from '../../contexts/NavigationContext';
|
import { useNavigationGuard, useNavigationState } from '../../contexts/NavigationContext';
|
||||||
import { useSignature } from '../../contexts/SignatureContext';
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
|
import { createStirlingFilesAndStubs } from '../../services/fileStubHelpers';
|
||||||
|
import NavigationWarningModal from '../shared/NavigationWarningModal';
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
sidebarsVisible: boolean;
|
sidebarsVisible: boolean;
|
||||||
@ -29,11 +31,33 @@ const EmbedPdfViewerContent = ({
|
|||||||
const { colorScheme: _colorScheme } = useMantineColorScheme();
|
const { colorScheme: _colorScheme } = useMantineColorScheme();
|
||||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||||
const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, isAnnotationMode, isAnnotationsVisible } = useViewer();
|
|
||||||
|
const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, getRotationState, isAnnotationMode, isAnnotationsVisible, exportActions } = useViewer();
|
||||||
|
|
||||||
const scrollState = getScrollState();
|
const scrollState = getScrollState();
|
||||||
const zoomState = getZoomState();
|
const zoomState = getZoomState();
|
||||||
const spreadState = getSpreadState();
|
const spreadState = getSpreadState();
|
||||||
|
const rotationState = getRotationState();
|
||||||
|
|
||||||
|
// Track initial rotation to detect changes
|
||||||
|
const initialRotationRef = useRef<number | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialRotationRef.current === null && rotationState.rotation !== undefined) {
|
||||||
|
initialRotationRef.current = rotationState.rotation;
|
||||||
|
}
|
||||||
|
}, [rotationState.rotation]);
|
||||||
|
|
||||||
|
// Get signature context
|
||||||
|
const { signatureApiRef, historyApiRef } = useSignature();
|
||||||
|
|
||||||
|
// Get current file from FileContext
|
||||||
|
const { selectors } = useFileState();
|
||||||
|
const { actions } = useFileActions();
|
||||||
|
const activeFiles = selectors.getFiles();
|
||||||
|
const activeFileIds = activeFiles.map(f => f.fileId);
|
||||||
|
|
||||||
|
// Navigation guard for unsaved changes
|
||||||
|
const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard();
|
||||||
|
|
||||||
// Check if we're in signature mode OR viewer annotation mode
|
// Check if we're in signature mode OR viewer annotation mode
|
||||||
const { selectedTool } = useNavigationState();
|
const { selectedTool } = useNavigationState();
|
||||||
@ -42,13 +66,6 @@ const EmbedPdfViewerContent = ({
|
|||||||
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
|
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
|
||||||
const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
|
const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
|
||||||
|
|
||||||
// Get signature context
|
|
||||||
const { signatureApiRef, historyApiRef } = useSignature();
|
|
||||||
|
|
||||||
// Get current file from FileContext
|
|
||||||
const { selectors } = useFileState();
|
|
||||||
const activeFiles = selectors.getFiles();
|
|
||||||
|
|
||||||
// Determine which file to display
|
// Determine which file to display
|
||||||
const currentFile = React.useMemo(() => {
|
const currentFile = React.useMemo(() => {
|
||||||
if (previewFile) {
|
if (previewFile) {
|
||||||
@ -134,6 +151,65 @@ const EmbedPdfViewerContent = ({
|
|||||||
};
|
};
|
||||||
}, [isViewerHovered]);
|
}, [isViewerHovered]);
|
||||||
|
|
||||||
|
// Register checker for unsaved changes (annotations only for now)
|
||||||
|
useEffect(() => {
|
||||||
|
if (previewFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkForChanges = () => {
|
||||||
|
// Check for annotation changes via history
|
||||||
|
const hasAnnotationChanges = historyApiRef.current?.canUndo() || false;
|
||||||
|
|
||||||
|
console.log('[Viewer] Checking for unsaved changes:', {
|
||||||
|
hasAnnotationChanges
|
||||||
|
});
|
||||||
|
return hasAnnotationChanges;
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[Viewer] Registering unsaved changes checker');
|
||||||
|
registerUnsavedChangesChecker(checkForChanges);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log('[Viewer] Unregistering unsaved changes checker');
|
||||||
|
unregisterUnsavedChangesChecker();
|
||||||
|
};
|
||||||
|
}, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
|
||||||
|
|
||||||
|
// Apply changes - save annotations to new file version
|
||||||
|
const applyChanges = useCallback(async () => {
|
||||||
|
if (!currentFile || activeFileIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Viewer] Applying changes - exporting PDF with annotations');
|
||||||
|
|
||||||
|
// Step 1: Export PDF with annotations using EmbedPDF
|
||||||
|
const arrayBuffer = await exportActions.saveAsCopy();
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
throw new Error('Failed to export PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Viewer] Exported PDF size:', arrayBuffer.byteLength);
|
||||||
|
|
||||||
|
// Step 2: Convert ArrayBuffer to File
|
||||||
|
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
|
||||||
|
const filename = currentFile.name || 'document.pdf';
|
||||||
|
const file = new File([blob], filename, { type: 'application/pdf' });
|
||||||
|
|
||||||
|
// Step 3: Create StirlingFiles and stubs for version history
|
||||||
|
const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
|
||||||
|
if (!parentStub) throw new Error('Parent stub not found');
|
||||||
|
|
||||||
|
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
|
||||||
|
|
||||||
|
// Step 4: Consume files (replace in context)
|
||||||
|
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
|
||||||
|
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Apply changes failed:', error);
|
||||||
|
}
|
||||||
|
}, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -240,6 +316,15 @@ const EmbedPdfViewerContent = ({
|
|||||||
visible={isThumbnailSidebarVisible}
|
visible={isThumbnailSidebarVisible}
|
||||||
onToggle={toggleThumbnailSidebar}
|
onToggle={toggleThumbnailSidebar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Navigation Warning Modal */}
|
||||||
|
{!previewFile && (
|
||||||
|
<NavigationWarningModal
|
||||||
|
onApplyAndContinue={async () => {
|
||||||
|
await applyChanges();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return;
|
if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return;
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
|
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,8 @@ export interface NavigationContextActions {
|
|||||||
setSelectedTool: (toolId: ToolId | null) => void;
|
setSelectedTool: (toolId: ToolId | null) => void;
|
||||||
setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void;
|
setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void;
|
||||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||||
|
registerUnsavedChangesChecker: (checker: () => boolean) => void;
|
||||||
|
unregisterUnsavedChangesChecker: () => void;
|
||||||
showNavigationWarning: (show: boolean) => void;
|
showNavigationWarning: (show: boolean) => void;
|
||||||
requestNavigation: (navigationFn: () => void) => void;
|
requestNavigation: (navigationFn: () => void) => void;
|
||||||
confirmNavigation: () => void;
|
confirmNavigation: () => void;
|
||||||
@ -106,11 +108,29 @@ export const NavigationProvider: React.FC<{
|
|||||||
}> = ({ children }) => {
|
}> = ({ children }) => {
|
||||||
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
|
||||||
|
|
||||||
const actions: NavigationContextActions = {
|
const actions: NavigationContextActions = {
|
||||||
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
||||||
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
|
// Check for unsaved changes using registered checker or state
|
||||||
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
|
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
||||||
|
console.log('[NavigationContext] setWorkbench:', {
|
||||||
|
from: state.workbench,
|
||||||
|
to: workbench,
|
||||||
|
hasChecker: !!unsavedChangesCheckerRef.current,
|
||||||
|
hasUnsavedChanges
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
|
||||||
|
const leavingWorkbenchWithChanges =
|
||||||
|
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
|
||||||
|
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
|
||||||
|
|
||||||
|
if (leavingWorkbenchWithChanges) {
|
||||||
|
// Update state to reflect unsaved changes so modal knows
|
||||||
|
if (!state.hasUnsavedChanges) {
|
||||||
|
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges: true } });
|
||||||
|
}
|
||||||
const performWorkbenchChange = () => {
|
const performWorkbenchChange = () => {
|
||||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||||
};
|
};
|
||||||
@ -126,8 +146,15 @@ export const NavigationProvider: React.FC<{
|
|||||||
}, []),
|
}, []),
|
||||||
|
|
||||||
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
||||||
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
|
// Check for unsaved changes using registered checker or state
|
||||||
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
|
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
||||||
|
|
||||||
|
// If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
|
||||||
|
const leavingWorkbenchWithChanges =
|
||||||
|
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
|
||||||
|
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
|
||||||
|
|
||||||
|
if (leavingWorkbenchWithChanges) {
|
||||||
const performWorkbenchChange = () => {
|
const performWorkbenchChange = () => {
|
||||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||||
};
|
};
|
||||||
@ -142,6 +169,14 @@ export const NavigationProvider: React.FC<{
|
|||||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||||
}, []),
|
}, []),
|
||||||
|
|
||||||
|
registerUnsavedChangesChecker: useCallback((checker: () => boolean) => {
|
||||||
|
unsavedChangesCheckerRef.current = checker;
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
unregisterUnsavedChangesChecker: useCallback(() => {
|
||||||
|
unsavedChangesCheckerRef.current = null;
|
||||||
|
}, []),
|
||||||
|
|
||||||
showNavigationWarning: useCallback((show: boolean) => {
|
showNavigationWarning: useCallback((show: boolean) => {
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
|
||||||
}, []),
|
}, []),
|
||||||
@ -254,6 +289,8 @@ export const useNavigationGuard = () => {
|
|||||||
confirmNavigation: actions.confirmNavigation,
|
confirmNavigation: actions.confirmNavigation,
|
||||||
cancelNavigation: actions.cancelNavigation,
|
cancelNavigation: actions.cancelNavigation,
|
||||||
setHasUnsavedChanges: actions.setHasUnsavedChanges,
|
setHasUnsavedChanges: actions.setHasUnsavedChanges,
|
||||||
setShowNavigationWarning: actions.showNavigationWarning
|
setShowNavigationWarning: actions.showNavigationWarning,
|
||||||
|
registerUnsavedChangesChecker: actions.registerUnsavedChangesChecker,
|
||||||
|
unregisterUnsavedChangesChecker: actions.unregisterUnsavedChangesChecker
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -57,13 +57,13 @@ const addFilesMutex = new SimpleMutex();
|
|||||||
/**
|
/**
|
||||||
* Helper to create ProcessedFile metadata structure
|
* Helper to create ProcessedFile metadata structure
|
||||||
*/
|
*/
|
||||||
export function createProcessedFile(pageCount: number, thumbnail?: string) {
|
export function createProcessedFile(pageCount: number, thumbnail?: string, pageRotations?: number[]) {
|
||||||
return {
|
return {
|
||||||
totalPages: pageCount,
|
totalPages: pageCount,
|
||||||
pages: Array.from({ length: pageCount }, (_, index) => ({
|
pages: Array.from({ length: pageCount }, (_, index) => ({
|
||||||
pageNumber: index + 1,
|
pageNumber: index + 1,
|
||||||
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
||||||
rotation: 0,
|
rotation: pageRotations?.[index] ?? 0,
|
||||||
splitBefore: false
|
splitBefore: false
|
||||||
})),
|
})),
|
||||||
thumbnailUrl: thumbnail,
|
thumbnailUrl: thumbnail,
|
||||||
@ -82,8 +82,22 @@ export async function generateProcessedFileMetadata(file: File): Promise<Process
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await generateThumbnailWithMetadata(file);
|
// Generate unrotated thumbnails for PageEditor (rotation applied via CSS)
|
||||||
return createProcessedFile(result.pageCount, result.thumbnail);
|
const unrotatedResult = await generateThumbnailWithMetadata(file, false);
|
||||||
|
|
||||||
|
// Generate rotated thumbnail for file manager display
|
||||||
|
const rotatedResult = await generateThumbnailWithMetadata(file, true);
|
||||||
|
|
||||||
|
const processedFile = createProcessedFile(
|
||||||
|
unrotatedResult.pageCount,
|
||||||
|
unrotatedResult.thumbnail, // Page thumbnails (unrotated)
|
||||||
|
unrotatedResult.pageRotations
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use rotated thumbnail for file manager
|
||||||
|
processedFile.thumbnailUrl = rotatedResult.thumbnail;
|
||||||
|
|
||||||
|
return processedFile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (DEBUG) console.warn(`📄 Failed to generate processedFileMetadata for ${file.name}:`, error);
|
if (DEBUG) console.warn(`📄 Failed to generate processedFileMetadata for ${file.name}:`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,14 +10,8 @@ export class DocumentManipulationService {
|
|||||||
* Returns single document or multiple documents if splits are present
|
* Returns single document or multiple documents if splits are present
|
||||||
*/
|
*/
|
||||||
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument, splitPositions?: Set<number>): PDFDocument | PDFDocument[] {
|
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument, splitPositions?: Set<number>): PDFDocument | PDFDocument[] {
|
||||||
console.log('DocumentManipulationService: Applying DOM changes to document');
|
|
||||||
console.log('Original document page order:', pdfDocument.pages.map(p => p.pageNumber));
|
|
||||||
console.log('Current display order:', currentDisplayOrder?.pages.map(p => p.pageNumber) || 'none provided');
|
|
||||||
console.log('Split positions:', splitPositions ? Array.from(splitPositions).sort() : 'none');
|
|
||||||
|
|
||||||
// Use current display order (from React state) if provided, otherwise use original order
|
// Use current display order (from React state) if provided, otherwise use original order
|
||||||
const baseDocument = currentDisplayOrder || pdfDocument;
|
const baseDocument = currentDisplayOrder || pdfDocument;
|
||||||
console.log('Using page order:', baseDocument.pages.map(p => p.pageNumber));
|
|
||||||
|
|
||||||
// Apply DOM changes to each page (rotation only now, splits are position-based)
|
// Apply DOM changes to each page (rotation only now, splits are position-based)
|
||||||
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
|
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
|
||||||
@ -57,32 +51,25 @@ export class DocumentManipulationService {
|
|||||||
private createSplitDocuments(document: PDFDocument): PDFDocument[] {
|
private createSplitDocuments(document: PDFDocument): PDFDocument[] {
|
||||||
const documents: PDFDocument[] = [];
|
const documents: PDFDocument[] = [];
|
||||||
const splitPoints: number[] = [];
|
const splitPoints: number[] = [];
|
||||||
|
|
||||||
// Find split points
|
// Find split points
|
||||||
document.pages.forEach((page, index) => {
|
document.pages.forEach((page, index) => {
|
||||||
if (page.splitAfter) {
|
if (page.splitAfter) {
|
||||||
console.log(`Found split marker at page ${page.pageNumber} (index ${index}), adding split point at ${index + 1}`);
|
|
||||||
splitPoints.push(index + 1);
|
splitPoints.push(index + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add end point if not already there
|
// Add end point if not already there
|
||||||
if (splitPoints.length === 0 || splitPoints[splitPoints.length - 1] !== document.pages.length) {
|
if (splitPoints.length === 0 || splitPoints[splitPoints.length - 1] !== document.pages.length) {
|
||||||
splitPoints.push(document.pages.length);
|
splitPoints.push(document.pages.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Final split points:', splitPoints);
|
|
||||||
console.log('Total pages to split:', document.pages.length);
|
|
||||||
|
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
let partNumber = 1;
|
let partNumber = 1;
|
||||||
|
|
||||||
for (const endIndex of splitPoints) {
|
for (const endIndex of splitPoints) {
|
||||||
const segmentPages = document.pages.slice(startIndex, endIndex);
|
const segmentPages = document.pages.slice(startIndex, endIndex);
|
||||||
|
|
||||||
console.log(`Creating split document ${partNumber}: pages ${startIndex}-${endIndex-1} (${segmentPages.length} pages)`);
|
|
||||||
console.log(`Split document ${partNumber} page numbers:`, segmentPages.map(p => p.pageNumber));
|
|
||||||
|
|
||||||
if (segmentPages.length > 0) {
|
if (segmentPages.length > 0) {
|
||||||
documents.push({
|
documents.push({
|
||||||
...document,
|
...document,
|
||||||
@ -93,11 +80,10 @@ export class DocumentManipulationService {
|
|||||||
});
|
});
|
||||||
partNumber++;
|
partNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
startIndex = endIndex;
|
startIndex = endIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Created ${documents.length} split documents`);
|
|
||||||
return documents;
|
return documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +94,6 @@ export class DocumentManipulationService {
|
|||||||
// Find the DOM element for this page
|
// Find the DOM element for this page
|
||||||
const pageElement = document.querySelector(`[data-page-id="${page.id}"]`);
|
const pageElement = document.querySelector(`[data-page-id="${page.id}"]`);
|
||||||
if (!pageElement) {
|
if (!pageElement) {
|
||||||
console.log(`Page ${page.pageNumber}: No DOM element found, keeping original state`);
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +101,7 @@ export class DocumentManipulationService {
|
|||||||
|
|
||||||
// Apply rotation changes from DOM
|
// Apply rotation changes from DOM
|
||||||
updatedPage.rotation = this.getRotationFromDOM(pageElement, page);
|
updatedPage.rotation = this.getRotationFromDOM(pageElement, page);
|
||||||
|
|
||||||
|
|
||||||
return updatedPage;
|
return updatedPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,16 +110,21 @@ export class DocumentManipulationService {
|
|||||||
*/
|
*/
|
||||||
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
|
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img && img.style.transform) {
|
if (img) {
|
||||||
// Parse rotation from transform property (e.g., "rotate(90deg)" -> 90)
|
const originalRotation = parseInt(img.getAttribute('data-original-rotation') || '0');
|
||||||
const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/);
|
|
||||||
const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0;
|
const currentTransform = img.style.transform || '';
|
||||||
|
const rotationMatch = currentTransform.match(/rotate\((-?\d+)deg\)/);
|
||||||
console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
|
const visualRotation = rotationMatch ? parseInt(rotationMatch[1]) : originalRotation;
|
||||||
return domRotation;
|
|
||||||
|
const userChange = ((visualRotation - originalRotation) % 360 + 360) % 360;
|
||||||
|
|
||||||
|
let finalRotation = (originalPage.rotation + userChange) % 360;
|
||||||
|
if (finalRotation === 360) finalRotation = 0;
|
||||||
|
|
||||||
|
return finalRotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Page ${originalPage.pageNumber}: No DOM rotation found, keeping original = ${originalPage.rotation}°`);
|
|
||||||
return originalPage.rotation;
|
return originalPage.rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -200,11 +200,13 @@ export class EnhancedPDFProcessingService {
|
|||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
|
||||||
|
|
||||||
|
const rotation = page.rotate || 0;
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -254,7 +256,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: page.rotate || 0,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -265,11 +267,15 @@ export class EnhancedPDFProcessingService {
|
|||||||
|
|
||||||
// Create placeholder pages for remaining pages
|
// Create placeholder pages for remaining pages
|
||||||
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
for (let i = priorityCount + 1; i <= totalPages; i++) {
|
||||||
|
// Load page just to get rotation
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const rotation = page.rotate || 0;
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
rotation: 0,
|
rotation,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -316,7 +322,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
rotation: 0,
|
rotation: page.rotate || 0,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -333,11 +339,15 @@ export class EnhancedPDFProcessingService {
|
|||||||
|
|
||||||
// Create placeholders for remaining pages
|
// Create placeholders for remaining pages
|
||||||
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
|
||||||
|
// Load page just to get rotation
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const rotation = page.rotate || 0;
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation: 0,
|
rotation,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -367,11 +377,15 @@ export class EnhancedPDFProcessingService {
|
|||||||
// Create placeholder pages without thumbnails
|
// Create placeholder pages without thumbnails
|
||||||
const pages: PDFPage[] = [];
|
const pages: PDFPage[] = [];
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
// Load page just to get rotation
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const rotation = page.rotate || 0;
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
id: `${createQuickKey(file)}-page-${i}`,
|
id: `${createQuickKey(file)}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
rotation: 0,
|
rotation,
|
||||||
selected: false
|
selected: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -390,7 +404,7 @@ export class EnhancedPDFProcessingService {
|
|||||||
const scales = { low: 0.2, medium: 0.5, high: 0.8 }; // Reduced low quality for page editor
|
const scales = { low: 0.2, medium: 0.5, high: 0.8 }; // Reduced low quality for page editor
|
||||||
const scale = scales[quality];
|
const scale = scales[quality];
|
||||||
|
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale, rotation: 0 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
|
|||||||
34
frontend/src/services/fileStubHelpers.ts
Normal file
34
frontend/src/services/fileStubHelpers.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { StirlingFile, StirlingFileStub } from '../types/fileContext';
|
||||||
|
import { createChildStub, generateProcessedFileMetadata } from '../contexts/file/fileActions';
|
||||||
|
import { createStirlingFile } from '../types/fileContext';
|
||||||
|
import { ToolId } from '../types/toolId';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create StirlingFiles and StirlingFileStubs from exported files
|
||||||
|
* Used when saving page editor changes to create version history
|
||||||
|
*/
|
||||||
|
export async function createStirlingFilesAndStubs(
|
||||||
|
files: File[],
|
||||||
|
parentStub: StirlingFileStub,
|
||||||
|
toolId: ToolId
|
||||||
|
): Promise<{ stirlingFiles: StirlingFile[], stubs: StirlingFileStub[] }> {
|
||||||
|
const stirlingFiles: StirlingFile[] = [];
|
||||||
|
const stubs: StirlingFileStub[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const processedFileMetadata = await generateProcessedFileMetadata(file);
|
||||||
|
const childStub = createChildStub(
|
||||||
|
parentStub,
|
||||||
|
{ toolId, timestamp: Date.now() },
|
||||||
|
file,
|
||||||
|
processedFileMetadata?.thumbnailUrl,
|
||||||
|
processedFileMetadata
|
||||||
|
);
|
||||||
|
|
||||||
|
const stirlingFile = createStirlingFile(file, childStub.id);
|
||||||
|
stirlingFiles.push(stirlingFile);
|
||||||
|
stubs.push(childStub);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stirlingFiles, stubs };
|
||||||
|
}
|
||||||
46
frontend/src/services/pdfExportHelpers.ts
Normal file
46
frontend/src/services/pdfExportHelpers.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { PDFDocument } from '../types/pageEditor';
|
||||||
|
import { pdfExportService } from './pdfExportService';
|
||||||
|
import { FileId } from '../types/file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export processed documents to File objects
|
||||||
|
* Handles both single documents and split documents (multiple PDFs)
|
||||||
|
*/
|
||||||
|
export async function exportProcessedDocumentsToFiles(
|
||||||
|
processedDocuments: PDFDocument | PDFDocument[],
|
||||||
|
sourceFiles: Map<FileId, File> | null,
|
||||||
|
exportFilename: string
|
||||||
|
): Promise<File[]> {
|
||||||
|
console.log('exportProcessedDocumentsToFiles called with:', {
|
||||||
|
isArray: Array.isArray(processedDocuments),
|
||||||
|
numDocs: Array.isArray(processedDocuments) ? processedDocuments.length : 1,
|
||||||
|
hasSourceFiles: sourceFiles !== null,
|
||||||
|
sourceFilesSize: sourceFiles?.size
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(processedDocuments)) {
|
||||||
|
// Multiple documents (splits)
|
||||||
|
const files: File[] = [];
|
||||||
|
const baseName = exportFilename.replace(/\.pdf$/i, '');
|
||||||
|
|
||||||
|
for (let i = 0; i < processedDocuments.length; i++) {
|
||||||
|
const doc = processedDocuments[i];
|
||||||
|
const partFilename = `${baseName}_part_${i + 1}.pdf`;
|
||||||
|
|
||||||
|
const result = sourceFiles
|
||||||
|
? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { selectedOnly: false, filename: partFilename })
|
||||||
|
: await pdfExportService.exportPDF(doc, [], { selectedOnly: false, filename: partFilename });
|
||||||
|
|
||||||
|
files.push(new File([result.blob], result.filename, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
} else {
|
||||||
|
// Single document
|
||||||
|
const result = sourceFiles
|
||||||
|
? await pdfExportService.exportPDFMultiFile(processedDocuments, sourceFiles, [], { selectedOnly: false, filename: exportFilename })
|
||||||
|
: await pdfExportService.exportPDF(processedDocuments, [], { selectedOnly: false, filename: exportFilename });
|
||||||
|
|
||||||
|
return [new File([result.blob], result.filename, { type: 'application/pdf' })];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -98,10 +98,7 @@ export class PDFExportService {
|
|||||||
// Create a blank page
|
// Create a blank page
|
||||||
const blankPage = newDoc.addPage(PageSizes.A4);
|
const blankPage = newDoc.addPage(PageSizes.A4);
|
||||||
|
|
||||||
// Apply rotation if needed
|
blankPage.setRotation(degrees(page.rotation));
|
||||||
if (page.rotation !== 0) {
|
|
||||||
blankPage.setRotation(degrees(page.rotation));
|
|
||||||
}
|
|
||||||
} else if (page.originalFileId && loadedDocs.has(page.originalFileId)) {
|
} else if (page.originalFileId && loadedDocs.has(page.originalFileId)) {
|
||||||
// Get the correct source document for this page
|
// Get the correct source document for this page
|
||||||
const sourceDoc = loadedDocs.get(page.originalFileId)!;
|
const sourceDoc = loadedDocs.get(page.originalFileId)!;
|
||||||
@ -111,10 +108,7 @@ export class PDFExportService {
|
|||||||
// Copy the page from the correct source document
|
// Copy the page from the correct source document
|
||||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
||||||
|
|
||||||
// Apply rotation
|
copiedPage.setRotation(degrees(page.rotation));
|
||||||
if (page.rotation !== 0) {
|
|
||||||
copiedPage.setRotation(degrees(page.rotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
newDoc.addPage(copiedPage);
|
newDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
@ -147,10 +141,7 @@ export class PDFExportService {
|
|||||||
// Create a blank page
|
// Create a blank page
|
||||||
const blankPage = newDoc.addPage(PageSizes.A4);
|
const blankPage = newDoc.addPage(PageSizes.A4);
|
||||||
|
|
||||||
// Apply rotation if needed
|
blankPage.setRotation(degrees(page.rotation));
|
||||||
if (page.rotation !== 0) {
|
|
||||||
blankPage.setRotation(degrees(page.rotation));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Get the original page from source document using originalPageNumber
|
// Get the original page from source document using originalPageNumber
|
||||||
const sourcePageIndex = page.originalPageNumber - 1;
|
const sourcePageIndex = page.originalPageNumber - 1;
|
||||||
@ -159,10 +150,7 @@ export class PDFExportService {
|
|||||||
// Copy the page
|
// Copy the page
|
||||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
|
||||||
|
|
||||||
// Apply rotation
|
copiedPage.setRotation(degrees(page.rotation));
|
||||||
if (page.rotation !== 0) {
|
|
||||||
copiedPage.setRotation(degrees(page.rotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
newDoc.addPage(copiedPage);
|
newDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -164,7 +164,7 @@ export class ThumbnailGenerationService {
|
|||||||
for (const pageNumber of batch) {
|
for (const pageNumber of batch) {
|
||||||
try {
|
try {
|
||||||
const page = await pdf.getPage(pageNumber);
|
const page = await pdf.getPage(pageNumber);
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale, rotation: 0 });
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { pdfWorkerManager } from '../services/pdfWorkerManager';
|
|||||||
export interface ThumbnailWithMetadata {
|
export interface ThumbnailWithMetadata {
|
||||||
thumbnail: string; // Always returns a thumbnail (placeholder if needed)
|
thumbnail: string; // Always returns a thumbnail (placeholder if needed)
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
|
pageRotations?: number[]; // Rotation for each page (0, 90, 180, 270)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColorScheme {
|
interface ColorScheme {
|
||||||
@ -377,8 +378,10 @@ export async function generateThumbnailForFile(file: File): Promise<string> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate thumbnail and extract page count for a PDF file - always returns a valid thumbnail
|
* Generate thumbnail and extract page count for a PDF file - always returns a valid thumbnail
|
||||||
|
* @param applyRotation - If true, render thumbnail with PDF rotation applied (for static display).
|
||||||
|
* If false, render without rotation (for CSS-based rotation in PageEditor)
|
||||||
*/
|
*/
|
||||||
export async function generateThumbnailWithMetadata(file: File): Promise<ThumbnailWithMetadata> {
|
export async function generateThumbnailWithMetadata(file: File, applyRotation: boolean = true): Promise<ThumbnailWithMetadata> {
|
||||||
// Non-PDF files have no page count
|
// Non-PDF files have no page count
|
||||||
if (!file.type.startsWith('application/pdf')) {
|
if (!file.type.startsWith('application/pdf')) {
|
||||||
const thumbnail = await generateThumbnailForFile(file);
|
const thumbnail = await generateThumbnailForFile(file);
|
||||||
@ -399,7 +402,13 @@ export async function generateThumbnailWithMetadata(file: File): Promise<Thumbna
|
|||||||
|
|
||||||
const pageCount = pdf.numPages;
|
const pageCount = pdf.numPages;
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
const viewport = page.getViewport({ scale });
|
|
||||||
|
// If applyRotation is false, render without rotation (for CSS-based rotation)
|
||||||
|
// If applyRotation is true, let PDF.js apply rotation (for static display)
|
||||||
|
const viewport = applyRotation
|
||||||
|
? page.getViewport({ scale })
|
||||||
|
: page.getViewport({ scale, rotation: 0 });
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
@ -413,8 +422,16 @@ export async function generateThumbnailWithMetadata(file: File): Promise<Thumbna
|
|||||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||||
const thumbnail = canvas.toDataURL();
|
const thumbnail = canvas.toDataURL();
|
||||||
|
|
||||||
|
// Read rotation for all pages
|
||||||
|
const pageRotations: number[] = [];
|
||||||
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
|
const p = await pdf.getPage(i);
|
||||||
|
const rotation = p.rotate || 0;
|
||||||
|
pageRotations.push(rotation);
|
||||||
|
}
|
||||||
|
|
||||||
pdfWorkerManager.destroyDocument(pdf);
|
pdfWorkerManager.destroyDocument(pdf);
|
||||||
return { thumbnail, pageCount };
|
return { thumbnail, pageCount, pageRotations };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === "PasswordException") {
|
if (error instanceof Error && error.name === "PasswordException") {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user