mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
move save button to left sidebar, allow the view to switch away from the viewer while annotations tool is selected
This commit is contained in:
parent
d036444af1
commit
786252e6f8
@ -307,7 +307,19 @@ export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function Annotation
|
||||
},
|
||||
getSelectedAnnotation: () => {
|
||||
const api = annotationApi as AnnotationApiSurface | undefined;
|
||||
return api?.getSelectedAnnotation?.() ?? null;
|
||||
if (!api?.getSelectedAnnotation) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return api.getSelectedAnnotation();
|
||||
} catch (error) {
|
||||
// Some EmbedPDF builds expose getSelectedAnnotation with an internal
|
||||
// `this`/state dependency (e.g. reading `selectedUid` from undefined).
|
||||
// If that happens, fail gracefully and treat it as "no selection"
|
||||
// instead of crashing the entire annotations tool.
|
||||
console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
deselectAnnotation: () => {
|
||||
const api = annotationApi as AnnotationApiSurface | undefined;
|
||||
|
||||
@ -250,7 +250,7 @@ const EmbedPdfViewerContent = ({
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [historyApiRef, setHasUnsavedChanges]);
|
||||
}, [historyApiRef.current, setHasUnsavedChanges]);
|
||||
|
||||
// Register checker for unsaved changes (annotations only for now)
|
||||
useEffect(() => {
|
||||
@ -303,10 +303,20 @@ const EmbedPdfViewerContent = ({
|
||||
}
|
||||
}, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]);
|
||||
|
||||
// Register viewer right-rail buttons (including optional Save for annotations)
|
||||
useViewerRightRailButtons({
|
||||
onSaveAnnotations: applyChanges,
|
||||
});
|
||||
// Expose annotation apply via a global event so tools (like Annotate) can
|
||||
// trigger saves from the left sidebar without tight coupling.
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
void applyChanges();
|
||||
};
|
||||
window.addEventListener('stirling-annotations-apply', handler);
|
||||
return () => {
|
||||
window.removeEventListener('stirling-annotations-apply', handler);
|
||||
};
|
||||
}, [applyChanges]);
|
||||
|
||||
// Register viewer right-rail buttons
|
||||
useViewerRightRailButtons();
|
||||
|
||||
const sidebarWidthRem = 15;
|
||||
const totalRightMargin =
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { ActionIcon, Popover } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
@ -13,15 +13,7 @@ import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState } from '@app/contexts/NavigationContext';
|
||||
import { BASE_PATH, withBasePath } from '@app/constants/app';
|
||||
|
||||
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) {
|
||||
export function useViewerRightRailButtons() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const viewer = useViewer();
|
||||
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
||||
@ -30,14 +22,6 @@ export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOption
|
||||
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) || '/';
|
||||
@ -239,23 +223,6 @@ export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOption
|
||||
|
||||
// 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,
|
||||
|
||||
@ -38,7 +38,7 @@ const isKnownAnnotationTool = (toolId: string | undefined | null): toolId is Ann
|
||||
|
||||
const Annotate = (_props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedTool, workbench } = useNavigation();
|
||||
const { selectedTool, workbench, hasUnsavedChanges } = useNavigation();
|
||||
const { selectedFiles } = useFileSelection();
|
||||
const {
|
||||
signatureApiRef,
|
||||
@ -143,6 +143,10 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
setTextAlignment,
|
||||
} = styleActions;
|
||||
|
||||
const handleApplyChanges = useCallback(() => {
|
||||
window.dispatchEvent(new CustomEvent('stirling-annotations-apply'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isAnnotateActive = workbench === 'viewer' && selectedTool === 'annotate';
|
||||
if (wasAnnotateActiveRef.current && !isAnnotateActive) {
|
||||
@ -371,6 +375,8 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
undo={undo}
|
||||
redo={redo}
|
||||
historyAvailability={historyAvailability}
|
||||
onApplyChanges={handleApplyChanges}
|
||||
applyDisabled={!hasUnsavedChanges}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@ -102,6 +102,8 @@ interface AnnotationPanelProps {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
historyAvailability: { canUndo: boolean; canRedo: boolean };
|
||||
onApplyChanges: () => void;
|
||||
applyDisabled: boolean;
|
||||
}
|
||||
|
||||
// AnnotationPanel component extracted from Annotate.tsx to keep the main file smaller.
|
||||
@ -136,6 +138,8 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
undo,
|
||||
redo,
|
||||
historyAvailability,
|
||||
onApplyChanges,
|
||||
applyDisabled,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@ -1291,6 +1295,19 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
|
||||
{colorPickerComponent}
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="md"
|
||||
radius="md"
|
||||
mt="sm"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
disabled={applyDisabled}
|
||||
onClick={onApplyChanges}
|
||||
>
|
||||
{t('annotation.applyChanges', 'Apply Changes')}
|
||||
</Button>
|
||||
|
||||
<SuggestedToolsSection />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@ -261,7 +261,19 @@ export function useAnnotationSelection({
|
||||
if (!api) return;
|
||||
|
||||
const checkSelection = () => {
|
||||
const ann = api.getSelectedAnnotation?.();
|
||||
let ann: any = null;
|
||||
if (typeof api.getSelectedAnnotation === 'function') {
|
||||
try {
|
||||
ann = api.getSelectedAnnotation();
|
||||
} catch (error) {
|
||||
// Some builds of the annotation plugin can throw when reading
|
||||
// internal selection state (e.g., accessing `selectedUid` on
|
||||
// an undefined object). Treat this as "no current selection"
|
||||
// instead of crashing the annotations tool.
|
||||
console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error);
|
||||
ann = null;
|
||||
}
|
||||
}
|
||||
const currentId = ann?.object?.id ?? ann?.id ?? null;
|
||||
if (currentId !== selectedAnnIdRef.current) {
|
||||
applySelectionFromAnnotation(ann ?? null);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user