mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
Polish and tests
This commit is contained in:
parent
39b55f7f19
commit
aa78a41026
@ -4018,23 +4018,26 @@ deleteSelected = "Delete Selected Pages"
|
||||
closePdf = "Close PDF"
|
||||
exportAll = "Export PDF"
|
||||
downloadSelected = "Download Selected Files"
|
||||
downloadAll = "Download All"
|
||||
saveAll = "Save All"
|
||||
annotations = "Annotations"
|
||||
exportSelected = "Export Selected Pages"
|
||||
saveChanges = "Save Changes"
|
||||
toggleTheme = "Toggle Theme"
|
||||
toggleBookmarks = "Toggle Bookmarks"
|
||||
language = "Language"
|
||||
toggleAnnotations = "Toggle Annotations Visibility"
|
||||
search = "Search PDF"
|
||||
panMode = "Pan Mode"
|
||||
rotateLeft = "Rotate Left"
|
||||
rotateRight = "Rotate Right"
|
||||
toggleSidebar = "Toggle Sidebar"
|
||||
exportSelected = "Export Selected Pages"
|
||||
toggleAnnotations = "Toggle Annotations Visibility"
|
||||
annotationMode = "Toggle Annotation Mode"
|
||||
toggleBookmarks = "Toggle Bookmarks"
|
||||
print = "Print PDF"
|
||||
draw = "Draw"
|
||||
save = "Save"
|
||||
saveChanges = "Save Changes"
|
||||
downloadAll = "Download All"
|
||||
saveAll = "Save All"
|
||||
|
||||
[textAlign]
|
||||
left = "Left"
|
||||
center = "Center"
|
||||
right = "Right"
|
||||
|
||||
[annotation]
|
||||
title = "Annotate"
|
||||
@ -4060,6 +4063,7 @@ underline = "Underline"
|
||||
strikeout = "Strikeout"
|
||||
squiggly = "Squiggly"
|
||||
inkHighlighter = "Freehand Highlighter"
|
||||
freehandHighlighter = "Freehand Highlighter"
|
||||
square = "Square"
|
||||
circle = "Circle"
|
||||
polygon = "Polygon"
|
||||
@ -4086,6 +4090,19 @@ textAlignment = "Text Alignment"
|
||||
noteIcon = "Note Icon"
|
||||
imagePreview = "Preview"
|
||||
contents = "Text"
|
||||
backgroundColor = "Background colour"
|
||||
clearBackground = "Remove background"
|
||||
noBackground = "No background"
|
||||
stampSettings = "Stamp Settings"
|
||||
savingCopy = "Preparing download..."
|
||||
saveFailed = "Unable to save copy"
|
||||
saveReady = "Download ready"
|
||||
resumeTooltip = "Resume placement"
|
||||
resume = "Resume placement"
|
||||
pauseTooltip = "Pause placement"
|
||||
pause = "Pause placement"
|
||||
undo = "Undo"
|
||||
redo = "Redo"
|
||||
|
||||
[search]
|
||||
title = "Search PDF"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box } from '@mantine/core';
|
||||
import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box, SegmentedControl } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ColorPicker } from '@app/components/annotation/shared/ColorPicker';
|
||||
|
||||
interface TextInputWithFontProps {
|
||||
@ -43,6 +44,7 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
||||
colorLabel,
|
||||
onAnyChange
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString());
|
||||
const fontSizeCombobox = useCombobox();
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
@ -221,7 +223,7 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
||||
{onTextAlignChange && (
|
||||
<SegmentedControl
|
||||
value={textAlign}
|
||||
onChange={(value) => {
|
||||
onChange={(value: string) => {
|
||||
onTextAlignChange(value as 'left' | 'center' | 'right');
|
||||
onAnyChange?.();
|
||||
}}
|
||||
|
||||
@ -126,9 +126,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
}),
|
||||
|
||||
// Register pan plugin (depends on Viewport, InteractionManager) - keep disabled to prevent drag panning
|
||||
createPluginRegistration(PanPluginPackage, {
|
||||
defaultMode: 'disabled',
|
||||
}),
|
||||
createPluginRegistration(PanPluginPackage, {}),
|
||||
|
||||
// Register zoom plugin with configuration
|
||||
createPluginRegistration(ZoomPluginPackage, {
|
||||
@ -265,7 +263,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'highlight',
|
||||
name: 'Highlight',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.HIGHLIGHT,
|
||||
color: '#ffd54f',
|
||||
@ -281,7 +279,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'underline',
|
||||
name: 'Underline',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.UNDERLINE,
|
||||
color: '#ffb300',
|
||||
@ -297,7 +295,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'strikeout',
|
||||
name: 'Strikeout',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.STRIKEOUT,
|
||||
color: '#e53935',
|
||||
@ -313,7 +311,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'squiggly',
|
||||
name: 'Squiggly',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.SQUIGGLY,
|
||||
color: '#00acc1',
|
||||
@ -329,7 +327,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'ink',
|
||||
name: 'Pen',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.INK,
|
||||
color: '#1f2933',
|
||||
@ -348,7 +346,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'inkHighlighter',
|
||||
name: 'Ink Highlighter',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.INK && annotation.color === '#ffd54f' ? 8 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK && annotation.color === '#ffd54f' ? 8 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.INK,
|
||||
color: '#ffd54f',
|
||||
@ -367,7 +365,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'square',
|
||||
name: 'Square',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.SQUARE,
|
||||
color: '#0000ff', // fill color (blue)
|
||||
@ -391,7 +389,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'circle',
|
||||
name: 'Circle',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.CIRCLE,
|
||||
color: '#0000ff', // fill color (blue)
|
||||
@ -415,7 +413,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'line',
|
||||
name: 'Line',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.LINE ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.LINE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.LINE,
|
||||
color: '#1565c0',
|
||||
@ -439,7 +437,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'lineArrow',
|
||||
name: 'Arrow',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.LINE && (annotation.endStyle === 'ClosedArrow' || annotation.lineEndingStyles?.end === 'ClosedArrow') ? 9 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.LINE && (annotation.endStyle === 'ClosedArrow' || annotation.lineEndingStyles?.end === 'ClosedArrow') ? 9 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.LINE,
|
||||
color: '#1565c0',
|
||||
@ -464,7 +462,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'polyline',
|
||||
name: 'Polyline',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.POLYLINE ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.POLYLINE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.POLYLINE,
|
||||
color: '#1565c0',
|
||||
@ -485,7 +483,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'polygon',
|
||||
name: 'Polygon',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.POLYGON,
|
||||
color: '#0000ff', // fill color (blue)
|
||||
@ -508,7 +506,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'text',
|
||||
name: 'Text',
|
||||
interaction: { exclusive: true, cursor: 'text' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 10 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.FREETEXT,
|
||||
textColor: '#111111',
|
||||
@ -528,7 +526,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'note',
|
||||
name: 'Note',
|
||||
interaction: { exclusive: true, cursor: 'pointer' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 8 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 8 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.FREETEXT,
|
||||
textColor: '#1b1b1b',
|
||||
@ -552,7 +550,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedA
|
||||
id: 'stamp',
|
||||
name: 'Image Stamp',
|
||||
interaction: { exclusive: false, cursor: 'copy' },
|
||||
matchScore: (annotation) => (annotation.type === PdfAnnotationSubtype.STAMP ? 5 : 0),
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.STAMP ? 5 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } 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';
|
||||
@ -9,6 +9,9 @@ import { SearchInterface } from '@app/components/viewer/SearchInterface';
|
||||
import ViewerAnnotationControls from '@app/components/shared/rightRail/ViewerAnnotationControls';
|
||||
import { useSidebarContext } from '@app/contexts/SidebarContext';
|
||||
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState } from '@app/contexts/NavigationContext';
|
||||
import { BASE_PATH, withBasePath } from '@app/constants/app';
|
||||
|
||||
export function useViewerRightRailButtons() {
|
||||
const { t, i18n } = useTranslation();
|
||||
@ -16,6 +19,32 @@ export function useViewerRightRailButtons() {
|
||||
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
||||
const { handleToolSelect } = useToolWorkflow();
|
||||
const { selectedTool } = useNavigationState();
|
||||
|
||||
const stripBasePath = useCallback((path: string) => {
|
||||
if (BASE_PATH && path.startsWith(BASE_PATH)) {
|
||||
return path.slice(BASE_PATH.length) || '/';
|
||||
}
|
||||
return path;
|
||||
}, []);
|
||||
|
||||
const isAnnotationsPath = useCallback(() => {
|
||||
const cleanPath = stripBasePath(window.location.pathname).toLowerCase();
|
||||
return cleanPath === '/annotations' || cleanPath.endsWith('/annotations');
|
||||
}, [stripBasePath]);
|
||||
|
||||
const [isAnnotationsActive, setIsAnnotationsActive] = useState<boolean>(() => isAnnotationsPath());
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnnotationsActive(isAnnotationsPath());
|
||||
}, [selectedTool, isAnnotationsPath]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = () => setIsAnnotationsActive(isAnnotationsPath());
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, [isAnnotationsPath]);
|
||||
|
||||
// Lift i18n labels out of memo for clarity
|
||||
const searchLabel = t('rightRail.search', 'Search PDF');
|
||||
@ -25,6 +54,7 @@ export function useViewerRightRailButtons() {
|
||||
const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar');
|
||||
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
||||
const printLabel = t('rightRail.print', 'Print PDF');
|
||||
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
||||
|
||||
const viewerButtons = useMemo<RightRailButtonWithAction[]>(() => {
|
||||
return [
|
||||
@ -147,6 +177,36 @@ export function useViewerRightRailButtons() {
|
||||
viewer.printActions.print();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'viewer-annotations',
|
||||
tooltip: annotationsLabel,
|
||||
ariaLabel: annotationsLabel,
|
||||
section: 'top' as const,
|
||||
order: 58,
|
||||
render: ({ disabled }) => (
|
||||
<Tooltip content={annotationsLabel} position={tooltipPosition} offset={12} arrow portalTarget={document.body}>
|
||||
<ActionIcon
|
||||
variant={isAnnotationsActive ? 'default' : 'subtle'}
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
if (disabled || isAnnotationsActive) return;
|
||||
const targetPath = withBasePath('/annotations');
|
||||
if (window.location.pathname !== targetPath) {
|
||||
window.history.pushState(null, '', targetPath);
|
||||
}
|
||||
setIsAnnotationsActive(true);
|
||||
handleToolSelect('annotate');
|
||||
}}
|
||||
disabled={disabled || isAnnotationsActive}
|
||||
aria-pressed={isAnnotationsActive}
|
||||
style={isAnnotationsActive ? { backgroundColor: 'var(--right-rail-pan-active-bg)' } : undefined}
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'viewer-annotation-controls',
|
||||
section: 'top' as const,
|
||||
@ -156,7 +216,7 @@ export function useViewerRightRailButtons() {
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [t, i18n.language, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, bookmarkLabel, printLabel, tooltipPosition]);
|
||||
}, [t, i18n.language, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, bookmarkLabel, printLabel, tooltipPosition, annotationsLabel, isAnnotationsActive, handleToolSelect]);
|
||||
|
||||
useRightRailButtons(viewerButtons);
|
||||
}
|
||||
|
||||
@ -66,10 +66,12 @@ export type AnnotationSelection = unknown;
|
||||
export interface AnnotationToolOptions {
|
||||
color?: string;
|
||||
fillColor?: string;
|
||||
strokeColor?: string;
|
||||
opacity?: number;
|
||||
strokeOpacity?: number;
|
||||
fillOpacity?: number;
|
||||
thickness?: number;
|
||||
borderWidth?: number;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
textAlign?: number; // 0 = Left, 1 = Center, 2 = Right
|
||||
|
||||
@ -253,7 +253,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||
component: Annotate,
|
||||
description: t('home.annotate.desc', 'Highlight, draw, add notes, and shapes directly in the viewer'),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.EDIT,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
workbench: 'viewer',
|
||||
operationConfig: signOperationConfig,
|
||||
automationSettings: null,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState, useContext, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Text, Group, ActionIcon, Stack, Divider, Slider, Box, Tooltip as MantineTooltip, Button, TextInput, Textarea, NumberInput, Tooltip, Paper } from '@mantine/core';
|
||||
import { Text, Group, ActionIcon, Stack, Slider, Box, Tooltip as MantineTooltip, Button, Textarea, Tooltip, Paper } from '@mantine/core';
|
||||
import { alert as showToast, updateToast } from '@app/components/toast';
|
||||
|
||||
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
|
||||
@ -53,7 +53,7 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
setSignatureConfig,
|
||||
setPlacementMode,
|
||||
placementPreviewSize,
|
||||
activateSignaturePlacementMode,
|
||||
activateSignaturePlacementMode: _activateSignaturePlacementMode,
|
||||
setPlacementPreviewSize,
|
||||
} = useSignature();
|
||||
const viewerContext = useContext(ViewerContext);
|
||||
@ -72,6 +72,8 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
const [squigglyColor, setSquigglyColor] = useState('#00acc1');
|
||||
const [squigglyOpacity, setSquigglyOpacity] = useState(100);
|
||||
const [textColor, setTextColor] = useState('#111111');
|
||||
const [textBackgroundColor, setTextBackgroundColor] = useState<string>('');
|
||||
const [noteBackgroundColor, setNoteBackgroundColor] = useState('#ffd54f');
|
||||
const [textSize, setTextSize] = useState(14);
|
||||
const [textAlignment, setTextAlignment] = useState<'left' | 'center' | 'right'>('left');
|
||||
const [shapeStrokeColor, setShapeStrokeColor] = useState('#cf5b5b');
|
||||
@ -80,7 +82,9 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
const [shapeStrokeOpacity, setShapeStrokeOpacity] = useState(50);
|
||||
const [shapeFillOpacity, setShapeFillOpacity] = useState(50);
|
||||
const [shapeThickness, setShapeThickness] = useState(1);
|
||||
const [colorPickerTarget, setColorPickerTarget] = useState<'ink' | 'highlight' | 'underline' | 'strikeout' | 'squiggly' | 'text' | 'shapeStroke' | 'shapeFill' | null>(null);
|
||||
const [colorPickerTarget, setColorPickerTarget] = useState<
|
||||
'ink' | 'highlight' | 'inkHighlighter' | 'underline' | 'strikeout' | 'squiggly' | 'text' | 'textBackground' | 'noteBackground' | 'shapeStroke' | 'shapeFill' | null
|
||||
>(null);
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [selectedAnn, setSelectedAnn] = useState<any | null>(null);
|
||||
const [selectedAnnId, setSelectedAnnId] = useState<string | null>(null);
|
||||
@ -174,16 +178,25 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
return { color: strikeoutColor, opacity: strikeoutOpacity / 100, ...metadata };
|
||||
case 'squiggly':
|
||||
return { color: squigglyColor, opacity: squigglyOpacity / 100, ...metadata };
|
||||
case 'text':
|
||||
case 'text': {
|
||||
const textAlignNumber = textAlignment === 'left' ? 0 : textAlignment === 'center' ? 1 : 2;
|
||||
return { color: textColor, fontSize: textSize, textAlign: textAlignNumber, ...metadata };
|
||||
case 'note':
|
||||
return {
|
||||
color: textColor,
|
||||
fontSize: textSize,
|
||||
textAlign: textAlignNumber,
|
||||
...(textBackgroundColor ? { fillColor: textBackgroundColor } : {}),
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
case 'note': {
|
||||
const noteFillColor = noteBackgroundColor || 'transparent';
|
||||
return {
|
||||
color: textColor, // text color
|
||||
fillColor: highlightColor, // background color, shares highlight picker defaults
|
||||
fillColor: noteFillColor,
|
||||
opacity: 1,
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
case 'square':
|
||||
case 'circle':
|
||||
case 'polygon':
|
||||
@ -217,7 +230,33 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}, [highlightColor, highlightOpacity, inkColor, inkWidth, freehandHighlighterWidth, underlineColor, underlineOpacity, strikeoutColor, strikeoutOpacity, squigglyColor, squigglyOpacity, textColor, textSize, textAlignment, shapeStrokeColor, shapeFillColor, shapeOpacity, shapeStrokeOpacity, shapeFillOpacity, shapeThickness, stampImageData, stampImageSize, cssToPdfSize]);
|
||||
}, [
|
||||
highlightColor,
|
||||
highlightOpacity,
|
||||
inkColor,
|
||||
inkWidth,
|
||||
freehandHighlighterWidth,
|
||||
underlineColor,
|
||||
underlineOpacity,
|
||||
strikeoutColor,
|
||||
strikeoutOpacity,
|
||||
squigglyColor,
|
||||
squigglyOpacity,
|
||||
textColor,
|
||||
textSize,
|
||||
textAlignment,
|
||||
textBackgroundColor,
|
||||
noteBackgroundColor,
|
||||
shapeStrokeColor,
|
||||
shapeFillColor,
|
||||
shapeOpacity,
|
||||
shapeStrokeOpacity,
|
||||
shapeFillOpacity,
|
||||
shapeThickness,
|
||||
stampImageData,
|
||||
stampImageSize,
|
||||
cssToPdfSize,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setToolAndWorkbench('annotate', 'viewer');
|
||||
@ -433,6 +472,7 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
|
||||
const applySelectionFromAnnotation = useCallback((ann: any | null) => {
|
||||
const annObject = ann?.object ?? ann ?? null;
|
||||
const type = annObject?.type;
|
||||
const annId = annObject?.id ?? null;
|
||||
selectedAnnIdRef.current = annId;
|
||||
setSelectedAnnId(annId);
|
||||
@ -444,8 +484,21 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
if (annObject?.fontSize !== undefined) {
|
||||
setSelectedFontSize(annObject.fontSize ?? 14);
|
||||
}
|
||||
if (type === 3) {
|
||||
const derivedTool = deriveToolFromAnnotation(annObject);
|
||||
const background = annObject?.backgroundColor as string | undefined;
|
||||
if (annObject?.textColor) {
|
||||
setTextColor(annObject.textColor);
|
||||
}
|
||||
if (derivedTool === 'note') {
|
||||
if (background) {
|
||||
setNoteBackgroundColor(background);
|
||||
}
|
||||
} else {
|
||||
setTextBackgroundColor(background || '');
|
||||
}
|
||||
}
|
||||
// Sync width properties based on annotation type
|
||||
const type = annObject?.type;
|
||||
if (type === 15 && annObject?.strokeWidth !== undefined) {
|
||||
// Type 15 = INK, uses strokeWidth
|
||||
setInkWidth(annObject.strokeWidth ?? 2);
|
||||
@ -536,7 +589,7 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
|
||||
const otherTools: { id: AnnotationToolId; label: string; icon: string }[] = [
|
||||
{ id: 'text', label: t('annotation.text', 'Text box'), icon: 'text-fields' },
|
||||
{ id: 'note', label: t('annotation.note', 'Note'), icon: 'sticky-note-2' },
|
||||
// { id: 'note', label: t('annotation.note', 'Note'), icon: 'sticky-note-2' },
|
||||
{ id: 'stamp', label: t('annotation.stamp', 'Add Image'), icon: 'add-photo-alternate' },
|
||||
];
|
||||
|
||||
@ -550,12 +603,16 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
: colorPickerTarget === 'strikeout'
|
||||
? strikeoutColor
|
||||
: colorPickerTarget === 'squiggly'
|
||||
? squigglyColor
|
||||
: colorPickerTarget === 'shapeStroke'
|
||||
? shapeStrokeColor
|
||||
: colorPickerTarget === 'shapeFill'
|
||||
? squigglyColor
|
||||
: colorPickerTarget === 'shapeStroke'
|
||||
? shapeStrokeColor
|
||||
: colorPickerTarget === 'shapeFill'
|
||||
? shapeFillColor
|
||||
: textColor;
|
||||
: colorPickerTarget === 'textBackground'
|
||||
? (textBackgroundColor || '#ffffff')
|
||||
: colorPickerTarget === 'noteBackground'
|
||||
? (noteBackgroundColor || '#ffffff')
|
||||
: textColor;
|
||||
|
||||
const steps = useMemo(() => {
|
||||
if (selectedFiles.length === 0) return [];
|
||||
@ -767,9 +824,72 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.backgroundColor', 'Background color')}</Text>
|
||||
<Group gap="xs" align="center">
|
||||
<ColorSwatchButton
|
||||
color={textBackgroundColor || '#ffffff'}
|
||||
size={30}
|
||||
onClick={() => {
|
||||
setColorPickerTarget('textBackground');
|
||||
setIsColorPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
variant={textBackgroundColor ? 'light' : 'default'}
|
||||
onClick={() => {
|
||||
setTextBackgroundColor('');
|
||||
annotationApiRef?.current?.setAnnotationStyle?.('text', buildToolOptions('text'));
|
||||
if (selectedAnn?.object?.type === 3 && deriveToolFromAnnotation(selectedAnn.object) !== 'note') {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ backgroundColor: 'transparent', fillColor: 'transparent' }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{textBackgroundColor ? t('annotation.clearBackground', 'Remove background') : t('annotation.noBackground', 'No background')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTool === 'note' && (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.backgroundColor', 'Background color')}</Text>
|
||||
<Group gap="xs" align="center">
|
||||
<ColorSwatchButton
|
||||
color={noteBackgroundColor || '#ffffff'}
|
||||
size={30}
|
||||
onClick={() => {
|
||||
setColorPickerTarget('noteBackground');
|
||||
setIsColorPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
variant={noteBackgroundColor ? 'light' : 'default'}
|
||||
onClick={() => {
|
||||
setNoteBackgroundColor('');
|
||||
annotationApiRef?.current?.setAnnotationStyle?.('note', buildToolOptions('note'));
|
||||
if (selectedAnn?.object?.type === 3 && deriveToolFromAnnotation(selectedAnn.object) === 'note') {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ backgroundColor: 'transparent', fillColor: 'transparent' }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{noteBackgroundColor ? t('annotation.clearBackground', 'Remove background') : t('annotation.noBackground', 'No background')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{['square', 'circle', 'line', 'polygon'].includes(activeTool) && (
|
||||
<>
|
||||
<Box>
|
||||
@ -895,10 +1015,15 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
|
||||
// Type 3: Text box
|
||||
if (type === 3) {
|
||||
const derivedTool = deriveToolFromAnnotation(selectedAnn.object);
|
||||
const isNote = derivedTool === 'note';
|
||||
const selectedBackground =
|
||||
selectedAnn.object?.backgroundColor ??
|
||||
(isNote ? noteBackgroundColor || '#ffffff' : textBackgroundColor || '#ffffff');
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={600}>{t('annotation.editText', 'Edit Text Box')}</Text>
|
||||
<Text size="sm" fw={600}>{isNote ? t('annotation.editNote', 'Edit Sticky Note') : t('annotation.editText', 'Edit Text Box')}</Text>
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.color', 'Color')}</Text>
|
||||
<ColorSwatchButton
|
||||
@ -910,6 +1035,37 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.backgroundColor', 'Background color')}</Text>
|
||||
<Group gap="xs" align="center">
|
||||
<ColorSwatchButton
|
||||
color={selectedBackground}
|
||||
size={28}
|
||||
onClick={() => {
|
||||
setColorPickerTarget(isNote ? 'noteBackground' : 'textBackground');
|
||||
setIsColorPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
variant={selectedAnn.object?.backgroundColor ? 'light' : 'default'}
|
||||
onClick={() => {
|
||||
if (isNote) {
|
||||
setNoteBackgroundColor('');
|
||||
} else {
|
||||
setTextBackgroundColor('');
|
||||
}
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ backgroundColor: 'transparent', fillColor: 'transparent' }
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('annotation.clearBackground', 'Remove background')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
<Textarea
|
||||
label={t('annotation.text', 'Text')}
|
||||
value={selectedTextDraft}
|
||||
@ -1194,7 +1350,14 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={activeColor}
|
||||
showOpacity={colorPickerTarget !== 'text' && colorPickerTarget !== 'shapeStroke' && colorPickerTarget !== 'shapeFill' && colorPickerTarget !== null}
|
||||
showOpacity={
|
||||
colorPickerTarget !== 'text' &&
|
||||
colorPickerTarget !== 'textBackground' &&
|
||||
colorPickerTarget !== 'noteBackground' &&
|
||||
colorPickerTarget !== 'shapeStroke' &&
|
||||
colorPickerTarget !== 'shapeFill' &&
|
||||
colorPickerTarget !== null
|
||||
}
|
||||
opacity={
|
||||
colorPickerTarget === 'highlight' ? highlightOpacity :
|
||||
colorPickerTarget === 'underline' ? underlineOpacity :
|
||||
@ -1285,6 +1448,30 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
if (selectedAnn?.object?.id) {
|
||||
annotationApiRef?.current?.updateAnnotation?.(selectedAnn.object.pageIndex ?? 0, selectedAnn.object.id, { color });
|
||||
}
|
||||
} else if (colorPickerTarget === 'textBackground') {
|
||||
setTextBackgroundColor(color);
|
||||
if (activeTool === 'text') {
|
||||
annotationApiRef?.current?.setAnnotationStyle?.('text', buildToolOptions('text'));
|
||||
}
|
||||
if (selectedAnn?.object?.type === 3 && deriveToolFromAnnotation(selectedAnn.object) !== 'note') {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ backgroundColor: color, fillColor: color }
|
||||
);
|
||||
}
|
||||
} else if (colorPickerTarget === 'noteBackground') {
|
||||
setNoteBackgroundColor(color);
|
||||
if (activeTool === 'note') {
|
||||
annotationApiRef?.current?.setAnnotationStyle?.('note', buildToolOptions('note'));
|
||||
}
|
||||
if (selectedAnn?.object?.type === 3 && deriveToolFromAnnotation(selectedAnn.object) === 'note') {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ backgroundColor: color, fillColor: color }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setTextColor(color);
|
||||
if (activeTool === 'text') {
|
||||
@ -1449,7 +1636,7 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
{colorPickerComponent}
|
||||
|
||||
{/* Suggested Tools */}
|
||||
<SuggestedToolsSection currentTool="annotate" />
|
||||
<SuggestedToolsSection />
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
@ -1470,8 +1657,13 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
squigglyOpacity,
|
||||
inkColor,
|
||||
inkWidth,
|
||||
textBackgroundColor,
|
||||
noteBackgroundColor,
|
||||
textAlignment,
|
||||
freehandHighlighterWidth,
|
||||
shapeStrokeColor,
|
||||
shapeFillColor,
|
||||
shapeOpacity,
|
||||
shapeStrokeOpacity,
|
||||
shapeFillOpacity,
|
||||
shapeThickness,
|
||||
@ -1504,7 +1696,20 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
steps,
|
||||
review: {
|
||||
isVisible: false,
|
||||
operation: { files: [], downloadUrl: null },
|
||||
operation: {
|
||||
files: [],
|
||||
thumbnails: [],
|
||||
isGeneratingThumbnails: false,
|
||||
downloadUrl: null,
|
||||
downloadFilename: '',
|
||||
isLoading: false,
|
||||
status: '',
|
||||
errorMessage: null,
|
||||
progress: null,
|
||||
executeOperation: async () => {},
|
||||
resetResults: () => {},
|
||||
clearError: () => {},
|
||||
},
|
||||
title: '',
|
||||
onFileClick: () => {},
|
||||
onUndo: () => {},
|
||||
|
||||
@ -70,6 +70,8 @@ export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
|
||||
'/scanner-image-split': 'scannerImageSplit',
|
||||
|
||||
// Annotation and content removal
|
||||
'/annotations': 'annotate',
|
||||
'/annotate': 'annotate',
|
||||
'/remove-annotations': 'removeAnnotations',
|
||||
'/remove-image': 'removeImage',
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user