From 346a05748ec5471be3300287914ed7de6bd0b3f1 Mon Sep 17 00:00:00 2001 From: Reece Date: Sat, 6 Dec 2025 11:31:05 +0000 Subject: [PATCH] better --- .../web/ReactRoutingController.java | 10 +- .../configuration/SecurityConfiguration.java | 4 +- .../public/locales/en-GB/translation.toml | 32 + frontend/src/core/components/AppProviders.tsx | 17 +- .../annotation/shared/ColorPicker.tsx | 30 +- .../components/viewer/AnnotationAPIBridge.tsx | 196 +++ .../core/components/viewer/EmbedPdfViewer.tsx | 5 +- .../core/components/viewer/LocalEmbedPDF.tsx | 41 +- .../components/viewer/SignatureAPIBridge.tsx | 4 + .../src/core/components/viewer/viewerTypes.ts | 12 + .../src/core/contexts/AnnotationContext.tsx | 26 + frontend/src/core/tools/Annotate.tsx | 1060 +++++++++++++---- 12 files changed, 1146 insertions(+), 291 deletions(-) create mode 100644 frontend/src/core/components/viewer/AnnotationAPIBridge.tsx create mode 100644 frontend/src/core/contexts/AnnotationContext.tsx diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index db0d95a36..adde1020a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -19,9 +19,10 @@ public class ReactRoutingController { @Value("${server.servlet.context-path:/}") private String contextPath; - @GetMapping(value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE) - public ResponseEntity serveIndexHtml(HttpServletRequest request) - throws IOException { + @GetMapping( + value = {"/", "/index.html"}, + produces = MediaType.TEXT_HTML_VALUE) + public ResponseEntity serveIndexHtml(HttpServletRequest request) throws IOException { ClassPathResource resource = new ClassPathResource("static/index.html"); try (InputStream inputStream = resource.getInputStream()) { @@ -47,8 +48,7 @@ public class ReactRoutingController { @GetMapping( "/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}") - public ResponseEntity forwardRootPaths(HttpServletRequest request) - throws IOException { + public ResponseEntity forwardRootPaths(HttpServletRequest request) throws IOException { return serveIndexHtml(request); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index a3a5eee4f..ab1e4934d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -331,7 +331,9 @@ public class SecurityConfiguration { formLogin -> formLogin .loginPage("/login") // Redirect here when unauthenticated - .loginProcessingUrl("/perform_login") // Process form posts here (not /login) + .loginProcessingUrl( + "/perform_login") // Process form posts here (not + // /login) .successHandler( new CustomAuthenticationSuccessHandler( loginAttemptService, diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index d91fc3563..ae129bde4 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -3911,6 +3911,7 @@ saveChanges = "Save Changes" [annotation] title = "Annotate" +desc = "Use highlight, pen, text, and notes. Changes stay live—no flattening required." highlight = "Highlight" pen = "Pen" text = "Text box" @@ -3921,8 +3922,39 @@ select = "Select" exit = "Exit annotation mode" strokeWidth = "Width" opacity = "Opacity" +strokeOpacity = "Stroke Opacity" +fillOpacity = "Fill Opacity" fontSize = "Font size" chooseColor = "Choose colour" +color = "Colour" +strokeColor = "Stroke Colour" +fillColor = "Fill Colour" +underline = "Underline" +strikeout = "Strikeout" +squiggly = "Squiggly" +inkHighlighter = "Ink Highlighter" +square = "Square" +circle = "Circle" +polygon = "Polygon" +line = "Line" +stamp = "Add Image" +textMarkup = "Text Markup" +drawing = "Drawing" +shapes = "Shapes" +notesStamps = "Notes & Stamps" +settings = "Settings" +borderOn = "Border: On" +borderOff = "Border: Off" +editInk = "Edit Pen" +editLine = "Edit Line" +editNote = "Edit Note" +editText = "Edit Text Box" +editTextMarkup = "Edit Text Markup" +editSelected = "Edit Annotation" +editSquare = "Edit Square" +editCircle = "Edit Circle" +editPolygon = "Edit Polygon" +unsupportedType = "This annotation type is not fully supported for editing." [search] title = "Search PDF" diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 7e47d00e0..0ad5bc0f0 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -12,6 +12,7 @@ import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions } from import { RightRailProvider } from "@app/contexts/RightRailContext"; import { ViewerProvider } from "@app/contexts/ViewerContext"; import { SignatureProvider } from "@app/contexts/SignatureContext"; +import { AnnotationProvider } from "@app/contexts/AnnotationContext"; import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext"; import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext"; import { PageEditorProvider } from "@app/contexts/PageEditorContext"; @@ -93,13 +94,15 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - - - - {children} - - - + + + + + {children} + + + + diff --git a/frontend/src/core/components/annotation/shared/ColorPicker.tsx b/frontend/src/core/components/annotation/shared/ColorPicker.tsx index 04ae501bb..21656b1f2 100644 --- a/frontend/src/core/components/annotation/shared/ColorPicker.tsx +++ b/frontend/src/core/components/annotation/shared/ColorPicker.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch } from '@mantine/core'; +import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch, Slider, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; interface ColorPickerProps { @@ -8,6 +8,10 @@ interface ColorPickerProps { selectedColor: string; onColorChange: (color: string) => void; title?: string; + opacity?: number; + onOpacityChange?: (opacity: number) => void; + showOpacity?: boolean; + opacityLabel?: string; } export const ColorPicker: React.FC = ({ @@ -15,10 +19,15 @@ export const ColorPicker: React.FC = ({ onClose, selectedColor, onColorChange, - title + title, + opacity, + onOpacityChange, + showOpacity = false, + opacityLabel, }) => { const { t } = useTranslation(); const resolvedTitle = title ?? t('colorPicker.title', 'Choose colour'); + const resolvedOpacityLabel = opacityLabel ?? t('annotation.opacity', 'Opacity'); return ( = ({ size="lg" fullWidth /> + {showOpacity && onOpacityChange && opacity !== undefined && ( + + {resolvedOpacityLabel} + + + )} + + )} + + + )} + + )} + + ); + + const selectedAnnotationControls = selectedAnn && (() => { + const type = selectedAnn.object?.type; + + // Type 9: Highlight, Type 10: Underline, Type 11: Squiggly, Type 12: Strikeout + if ([9, 10, 11, 12].includes(type)) { + return ( + + {t('annotation.editTextMarkup', 'Edit Text Markup')} + + {t('annotation.color', 'Color')} + { + setColorPickerTarget('highlight'); + setIsColorPickerOpen(true); + }} + /> + + + {t('annotation.opacity', 'Opacity')} + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { opacity: value / 100 } + ); + }} + /> + + + ); + } + + // Type 15: Ink (pen) + if (type === 15) { + return ( + + {t('annotation.editInk', 'Edit Pen')} + + {t('annotation.color', 'Color')} + { + setColorPickerTarget('ink'); + setIsColorPickerOpen(true); + }} + /> + + + {t('annotation.strokeWidth', 'Width')} + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { + borderWidth: value, + strokeWidth: value, + lineWidth: value, + } + ); + setInkWidth(value); + }} + /> + + + ); + } + + // Type 3: Text box, Type 1: Note + if ([1, 3].includes(type)) { + return ( + + {type === 3 ? t('annotation.editText', 'Edit Text Box') : t('annotation.editNote', 'Edit Note')} + + {t('annotation.color', 'Color')} + { + setColorPickerTarget('text'); + setIsColorPickerOpen(true); + }} + /> + + { + const val = e.currentTarget.value; + setSelectedTextDraft(val); + if (selectedUpdateTimer.current) { + clearTimeout(selectedUpdateTimer.current); + } + selectedUpdateTimer.current = setTimeout(() => { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { contents: val, textColor: selectedAnn.object?.textColor ?? textColor } + ); + }, 120); + }} + /> + {type === 3 && ( + { + const size = typeof val === 'number' ? val : 14; + setSelectedFontSize(size); + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { fontSize: size } + ); + }} + /> + )} + + {t('annotation.opacity', 'Opacity')} + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { opacity: value / 100 } + ); + }} + /> + + + ); + } + + // Type 4: Line + if (type === 4) { + return ( + + {t('annotation.editLine', 'Edit Line')} + + {t('annotation.color', 'Color')} + { + setColorPickerTarget('shapeStroke'); + setIsColorPickerOpen(true); + }} + /> + + + {t('annotation.opacity', 'Opacity')} + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { opacity: value / 100 } + ); + }} + /> + + + {t('annotation.strokeWidth', 'Width')} + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { + borderWidth: value, + strokeWidth: value, + lineWidth: value, + } + ); + setShapeThickness(value); + }} + /> + + + ); + } + + // Type 5: Square, Type 6: Circle, Type 7: Polygon + if ([5, 6, 7].includes(type)) { + const shapeName = type === 5 ? 'Square' : type === 6 ? 'Circle' : 'Polygon'; + return ( + + {t(`annotation.edit${shapeName}`, `Edit ${shapeName}`)} + + + {t('annotation.strokeColor', 'Stroke Color')} + { + setColorPickerTarget('shapeStroke'); + setIsColorPickerOpen(true); + }} + /> + + + {t('annotation.fillColor', 'Fill Color')} + { + setColorPickerTarget('shapeFill'); + setIsColorPickerOpen(true); + }} + /> + + + + {t('annotation.opacity', 'Opacity')} + { + signatureApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { opacity: value / 100 } + ); + }} + /> + + + + {t('annotation.strokeWidth', 'Stroke')} { signatureApiRef?.current?.updateAnnotation?.( selectedAnn.object?.pageIndex ?? 0, selectedAnn.object?.id, - { opacity: value / 100 } - ); - }} - /> - - )} - {(selectedAnn.object?.type === 9 || selectedAnn.object?.type === 15 || selectedAnn.object?.type === 3 || selectedAnn.object?.type === 1) && ( - { - setColorPickerTarget('highlight'); - setIsColorPickerOpen(true); - }} - /> - )} - {(selectedAnn.object?.type === 3 || selectedAnn.object?.type === 1) && ( - <> - { - const val = e.currentTarget.value; - setSelectedTextDraft(val); - if (selectedUpdateTimer.current) { - clearTimeout(selectedUpdateTimer.current); + { + borderWidth: value, + strokeWidth: value, + lineWidth: value, } - selectedUpdateTimer.current = setTimeout(() => { - signatureApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { contents: val, textColor: selectedAnn.object?.textColor ?? textColor } - ); - }, 120); - }} - /> - {selectedAnn.object?.type === 3 && ( - { - const size = typeof val === 'number' ? val : 14; - setSelectedFontSize(size); - signatureApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { fontSize: size } - ); - }} - /> - )} - - )} - {['4','5','7','8','12','15'].includes(String(selectedAnn.object?.type)) && ( - <> - {t('annotation.opacity', 'Opacity')} - { - signatureApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { opacity: value / 100 } - ); - }} - /> - {t('annotation.strokeWidth', 'Stroke')} - { - signatureApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { borderWidth: value } - ); - setShapeThickness(value); - }} - /> - - { - setColorPickerTarget('shapeStroke'); - setIsColorPickerOpen(true); - }} - /> - {['4','5','7','8','12'].includes(String(selectedAnn.object?.type)) && ( - { - setColorPickerTarget('shapeFill'); - setIsColorPickerOpen(true); - }} - /> - )} - - - )} - - )} + ); + setShapeThickness(value); + }} + /> + + + + + ); + } + + // Default fallback + return ( + + {t('annotation.editSelected', 'Edit Annotation')} + {t('annotation.unsupportedType', 'This annotation type is not fully supported for editing.')} + + ); + })(); + + const saveAndColorPicker = ( + <>