mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
add save button and allow user to change view while annotations tool is open
This commit is contained in:
parent
f5b9f5a910
commit
d036444af1
@ -56,9 +56,6 @@ const EmbedPdfViewerContent = ({
|
||||
exportActions,
|
||||
} = useViewer();
|
||||
|
||||
// Register viewer right-rail buttons
|
||||
useViewerRightRailButtons();
|
||||
|
||||
const scrollState = getScrollState();
|
||||
const rotationState = getRotationState();
|
||||
|
||||
@ -73,6 +70,11 @@ const EmbedPdfViewerContent = ({
|
||||
// Get signature and annotation contexts
|
||||
const { signatureApiRef, annotationApiRef, historyApiRef, signatureConfig, isPlacementMode } = useSignature();
|
||||
|
||||
// Track whether there are unsaved annotation changes in this viewer session.
|
||||
// This is our source of truth for navigation guards; it is set when the
|
||||
// annotation history changes, and cleared after we successfully apply changes.
|
||||
const hasAnnotationChangesRef = useRef(false);
|
||||
|
||||
// Get current file from FileContext
|
||||
const { selectors, state } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
@ -225,6 +227,31 @@ const EmbedPdfViewerContent = ({
|
||||
};
|
||||
}, [isViewerHovered, isSearchInterfaceVisible, zoomActions, searchInterfaceActions]);
|
||||
|
||||
// Watch the annotation history API to detect when the document becomes "dirty".
|
||||
// We treat any change that makes the history undoable as unsaved changes until
|
||||
// the user explicitly applies them via applyChanges.
|
||||
useEffect(() => {
|
||||
const historyApi = historyApiRef.current;
|
||||
if (!historyApi || !historyApi.subscribe) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateHasChanges = () => {
|
||||
const canUndo = historyApi.canUndo?.() ?? false;
|
||||
if (!hasAnnotationChangesRef.current && canUndo) {
|
||||
hasAnnotationChangesRef.current = true;
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = historyApi.subscribe(updateHasChanges);
|
||||
return () => {
|
||||
if (typeof unsubscribe === 'function') {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [historyApiRef, setHasUnsavedChanges]);
|
||||
|
||||
// Register checker for unsaved changes (annotations only for now)
|
||||
useEffect(() => {
|
||||
if (previewFile) {
|
||||
@ -232,39 +259,28 @@ const EmbedPdfViewerContent = ({
|
||||
}
|
||||
|
||||
const checkForChanges = () => {
|
||||
// Check for annotation changes via history
|
||||
const hasAnnotationChanges = historyApiRef.current?.canUndo() || false;
|
||||
|
||||
console.log('[Viewer] Checking for unsaved changes:', {
|
||||
hasAnnotationChanges
|
||||
});
|
||||
const hasAnnotationChanges = hasAnnotationChangesRef.current;
|
||||
return hasAnnotationChanges;
|
||||
};
|
||||
|
||||
console.log('[Viewer] Registering unsaved changes checker');
|
||||
registerUnsavedChangesChecker(checkForChanges);
|
||||
|
||||
return () => {
|
||||
console.log('[Viewer] Unregistering unsaved changes checker');
|
||||
unregisterUnsavedChangesChecker();
|
||||
};
|
||||
}, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
|
||||
}, [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';
|
||||
@ -279,12 +295,19 @@ const EmbedPdfViewerContent = ({
|
||||
// Step 4: Consume files (replace in context)
|
||||
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
|
||||
|
||||
// Mark annotations as saved so navigation away from the viewer is allowed.
|
||||
hasAnnotationChangesRef.current = false;
|
||||
setHasUnsavedChanges(false);
|
||||
} catch (error) {
|
||||
console.error('Apply changes failed:', error);
|
||||
}
|
||||
}, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]);
|
||||
|
||||
// Register viewer right-rail buttons (including optional Save for annotations)
|
||||
useViewerRightRailButtons({
|
||||
onSaveAnnotations: applyChanges,
|
||||
});
|
||||
|
||||
const sidebarWidthRem = 15;
|
||||
const totalRightMargin =
|
||||
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { ActionIcon, Popover } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
@ -13,7 +13,15 @@ import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState } from '@app/contexts/NavigationContext';
|
||||
import { BASE_PATH, withBasePath } from '@app/constants/app';
|
||||
|
||||
export function useViewerRightRailButtons() {
|
||||
interface ViewerRightRailButtonsOptions {
|
||||
/**
|
||||
* Optional handler to save annotation changes to a new PDF version.
|
||||
* When provided, a Save button will be shown in the viewer right rail.
|
||||
*/
|
||||
onSaveAnnotations?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOptions) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const viewer = useViewer();
|
||||
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
||||
@ -22,6 +30,14 @@ export function useViewerRightRailButtons() {
|
||||
const { handleToolSelect } = useToolWorkflow();
|
||||
const { selectedTool } = useNavigationState();
|
||||
|
||||
// Keep the latest save handler in a ref to avoid re-registering right-rail
|
||||
// buttons on every render when the callback identity changes.
|
||||
const saveAnnotationsRef = useRef<(() => void | Promise<void>) | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
saveAnnotationsRef.current = options?.onSaveAnnotations;
|
||||
}, [options?.onSaveAnnotations]);
|
||||
|
||||
const stripBasePath = useCallback((path: string) => {
|
||||
if (BASE_PATH && path.startsWith(BASE_PATH)) {
|
||||
return path.slice(BASE_PATH.length) || '/';
|
||||
@ -55,9 +71,13 @@ export function useViewerRightRailButtons() {
|
||||
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
||||
const printLabel = t('rightRail.print', 'Print PDF');
|
||||
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
||||
const saveChangesLabel = t('rightRail.saveChanges', 'Save Changes');
|
||||
|
||||
const viewerButtons = useMemo<RightRailButtonWithAction[]>(() => {
|
||||
return [
|
||||
const exportState = viewer.getExportState();
|
||||
const canExport = Boolean(exportState?.canExport);
|
||||
|
||||
const buttons: RightRailButtonWithAction[] = [
|
||||
{
|
||||
id: 'viewer-search',
|
||||
tooltip: searchLabel,
|
||||
@ -214,9 +234,47 @@ export function useViewerRightRailButtons() {
|
||||
render: ({ disabled }) => (
|
||||
<ViewerAnnotationControls currentView="viewer" disabled={disabled} />
|
||||
)
|
||||
}
|
||||
},
|
||||
];
|
||||
}, [t, i18n.language, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, bookmarkLabel, printLabel, tooltipPosition, annotationsLabel, isAnnotationsActive, handleToolSelect]);
|
||||
|
||||
// Optional: Save button for annotations (always registered when this hook is used
|
||||
// with a save handler; uses a ref to avoid infinite re-registration loops).
|
||||
buttons.push({
|
||||
id: 'viewer-save-annotations',
|
||||
icon: <LocalIcon icon="save" width="1.5rem" height="1.5rem" />,
|
||||
tooltip: saveChangesLabel,
|
||||
ariaLabel: saveChangesLabel,
|
||||
section: 'top' as const,
|
||||
order: 59,
|
||||
disabled: !canExport,
|
||||
visible: true,
|
||||
onClick: () => {
|
||||
const handler = saveAnnotationsRef.current;
|
||||
if (handler) {
|
||||
void handler();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return buttons;
|
||||
}, [
|
||||
t,
|
||||
i18n.language,
|
||||
viewer,
|
||||
isPanning,
|
||||
searchLabel,
|
||||
panLabel,
|
||||
rotateLeftLabel,
|
||||
rotateRightLabel,
|
||||
sidebarLabel,
|
||||
bookmarkLabel,
|
||||
printLabel,
|
||||
tooltipPosition,
|
||||
annotationsLabel,
|
||||
saveChangesLabel,
|
||||
isAnnotationsActive,
|
||||
handleToolSelect,
|
||||
]);
|
||||
|
||||
useRightRailButtons(viewerButtons);
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ const isKnownAnnotationTool = (toolId: string | undefined | null): toolId is Ann
|
||||
|
||||
const Annotate = (_props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setToolAndWorkbench, selectedTool, workbench } = useNavigation();
|
||||
const { selectedTool, workbench } = useNavigation();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
const {
|
||||
signatureApiRef,
|
||||
@ -143,10 +143,6 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
setTextAlignment,
|
||||
} = styleActions;
|
||||
|
||||
useEffect(() => {
|
||||
setToolAndWorkbench('annotate', 'viewer');
|
||||
}, [setToolAndWorkbench]);
|
||||
|
||||
useEffect(() => {
|
||||
const isAnnotateActive = workbench === 'viewer' && selectedTool === 'annotate';
|
||||
if (wasAnnotateActiveRef.current && !isAnnotateActive) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user