mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
Undo redo, edit in post
This commit is contained in:
parent
200a53c27b
commit
59a365bb4e
@ -4097,7 +4097,10 @@ stampSettings = "Stamp Settings"
|
||||
savingCopy = "Preparing download..."
|
||||
saveFailed = "Unable to save copy"
|
||||
saveReady = "Download ready"
|
||||
selectAndMove = "Select and Move"
|
||||
selectAndMove = "Select and Edit"
|
||||
editSelectDescription = "Click an existing annotation to edit its colour, opacity, text, or size."
|
||||
editStampHint = "To change the image, delete this stamp and add a new one."
|
||||
editSwitchToSelect = "Switch to Select & Edit to edit this annotation."
|
||||
undo = "Undo"
|
||||
redo = "Redo"
|
||||
|
||||
|
||||
@ -25,6 +25,12 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
|
||||
const { selectedTool } = useNavigationState();
|
||||
const isSignMode = selectedTool === 'sign';
|
||||
|
||||
// Check if we're in any annotation tool that should disable the toggle
|
||||
const isInAnnotationTool = selectedTool === 'annotate' || selectedTool === 'sign' || selectedTool === 'addImage' || selectedTool === 'addText';
|
||||
|
||||
// Check if we're on annotate tool to highlight the button
|
||||
const isAnnotateActive = selectedTool === 'annotate';
|
||||
|
||||
// Don't show any annotation controls in sign mode
|
||||
if (isSignMode) {
|
||||
return null;
|
||||
@ -35,13 +41,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
|
||||
{/* Annotation Visibility Toggle */}
|
||||
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
variant={isAnnotateActive ? "filled" : "subtle"}
|
||||
color="blue"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationsVisibility();
|
||||
}}
|
||||
disabled={disabled || currentView !== 'viewer'}
|
||||
disabled={disabled || currentView !== 'viewer' || isInAnnotationTool}
|
||||
>
|
||||
<LocalIcon
|
||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||
|
||||
@ -125,6 +125,22 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
setTextColor,
|
||||
setTextBackgroundColor,
|
||||
setNoteBackgroundColor,
|
||||
setInkColor,
|
||||
setHighlightColor,
|
||||
setHighlightOpacity,
|
||||
setFreehandHighlighterWidth,
|
||||
setUnderlineColor,
|
||||
setUnderlineOpacity,
|
||||
setStrikeoutColor,
|
||||
setStrikeoutOpacity,
|
||||
setSquigglyColor,
|
||||
setSquigglyOpacity,
|
||||
setShapeStrokeColor,
|
||||
setShapeFillColor,
|
||||
setShapeOpacity,
|
||||
setShapeStrokeOpacity,
|
||||
setShapeFillOpacity,
|
||||
setTextAlignment,
|
||||
} = styleActions;
|
||||
|
||||
useEffect(() => {
|
||||
@ -150,22 +166,32 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
const historyApi = historyApiRef?.current;
|
||||
if (!historyApi) return;
|
||||
|
||||
const updateAvailability = () => {
|
||||
const updateAvailability = () =>
|
||||
setHistoryAvailability({
|
||||
canUndo: historyApi.canUndo?.() ?? false,
|
||||
canRedo: historyApi.canRedo?.() ?? false,
|
||||
});
|
||||
};
|
||||
|
||||
updateAvailability();
|
||||
|
||||
if (historyApi.subscribe) {
|
||||
let interval: ReturnType<typeof setInterval> | undefined;
|
||||
if (!historyApi.subscribe) {
|
||||
// Fallback polling in case the history API doesn't support subscriptions
|
||||
interval = setInterval(updateAvailability, 350);
|
||||
} else {
|
||||
const unsubscribe = historyApi.subscribe(updateAvailability);
|
||||
if (typeof unsubscribe === 'function') {
|
||||
return () => unsubscribe();
|
||||
}
|
||||
return () => {
|
||||
if (typeof unsubscribe === 'function') {
|
||||
unsubscribe();
|
||||
}
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}
|
||||
}, [historyApiRef]);
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [historyApiRef?.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewerContext) return;
|
||||
@ -295,6 +321,22 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
setTextColor,
|
||||
setTextBackgroundColor,
|
||||
setNoteBackgroundColor,
|
||||
setInkColor,
|
||||
setHighlightColor,
|
||||
setHighlightOpacity,
|
||||
setFreehandHighlighterWidth,
|
||||
setUnderlineColor,
|
||||
setUnderlineOpacity,
|
||||
setStrikeoutColor,
|
||||
setStrikeoutOpacity,
|
||||
setSquigglyColor,
|
||||
setSquigglyOpacity,
|
||||
setShapeStrokeColor,
|
||||
setShapeFillColor,
|
||||
setShapeOpacity,
|
||||
setShapeStrokeOpacity,
|
||||
setShapeFillOpacity,
|
||||
setTextAlignment,
|
||||
});
|
||||
|
||||
const steps =
|
||||
|
||||
@ -6,6 +6,7 @@ import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { ColorPicker, ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker';
|
||||
import { ImageUploader } from '@app/components/annotation/shared/ImageUploader';
|
||||
import { SuggestedToolsSection } from '@app/components/tools/shared/SuggestedToolsSection';
|
||||
import { DrawingControls } from '@app/components/annotation/shared/DrawingControls';
|
||||
import type { AnnotationToolId, AnnotationAPI } from '@app/components/viewer/viewerTypes';
|
||||
|
||||
interface StyleState {
|
||||
@ -235,7 +236,14 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
const defaultStyleControls = (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
{activeTool === 'stamp' ? (
|
||||
{activeTool === 'select' ? (
|
||||
<>
|
||||
<Text size="sm" fw={600}>{t('annotation.selectAndMove', 'Select and Edit')}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('annotation.editSelectDescription', 'Click an existing annotation to edit its color, opacity, text, or size.')}
|
||||
</Text>
|
||||
</>
|
||||
) : activeTool === 'stamp' ? (
|
||||
<>
|
||||
<Text size="sm" fw={600}>{t('annotation.stampSettings', 'Stamp Settings')}</Text>
|
||||
<ImageUploader
|
||||
@ -377,6 +385,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTool === 'underline' && (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.opacity', 'Opacity')}</Text>
|
||||
<Slider min={10} max={100} value={underlineOpacity} onChange={setUnderlineOpacity} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{activeTool === 'inkHighlighter' && (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.strokeWidth', 'Width')}</Text>
|
||||
@ -530,10 +545,32 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const selectedAnnotationControls = selectedAnn && (() => {
|
||||
const type = selectedAnn.object?.type;
|
||||
const selectedDerivedTool = selectedAnn?.object ? deriveToolFromAnnotation(selectedAnn.object) : undefined;
|
||||
const selectedIsTextMarkup =
|
||||
(selectedAnn?.object?.type && [9, 10, 11, 12].includes(selectedAnn.object.type)) ||
|
||||
(selectedDerivedTool ? ['highlight', 'underline', 'strikeout', 'squiggly'].includes(selectedDerivedTool) : false);
|
||||
|
||||
if ([9, 10, 11, 12].includes(type)) {
|
||||
const selectedAnnotationControls = selectedAnn && (() => {
|
||||
const rawType = selectedAnn.object?.type;
|
||||
const toolId = selectedDerivedTool ?? deriveToolFromAnnotation(selectedAnn.object);
|
||||
const derivedType =
|
||||
toolId === 'highlight' ? 9
|
||||
: toolId === 'underline' ? 10
|
||||
: toolId === 'squiggly' ? 11
|
||||
: toolId === 'strikeout' ? 12
|
||||
: toolId === 'line' ? 4
|
||||
: toolId === 'square' ? 5
|
||||
: toolId === 'circle' ? 6
|
||||
: toolId === 'polygon' ? 7
|
||||
: toolId === 'polyline' ? 8
|
||||
: toolId === 'text' ? 3
|
||||
: toolId === 'note' ? 3
|
||||
: toolId === 'stamp' ? 13
|
||||
: toolId === 'ink' ? 15
|
||||
: undefined;
|
||||
const type = typeof rawType === 'number' ? rawType : derivedType;
|
||||
|
||||
if (toolId && ['highlight', 'underline', 'strikeout', 'squiggly'].includes(toolId)) {
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
@ -569,35 +606,73 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 15) {
|
||||
if (type === 15 || toolId === 'inkHighlighter' || toolId === 'ink') {
|
||||
const isHighlighter = toolId === 'inkHighlighter';
|
||||
const thicknessValue =
|
||||
selectedAnn.object?.strokeWidth ??
|
||||
selectedAnn.object?.borderWidth ??
|
||||
selectedAnn.object?.lineWidth ??
|
||||
selectedAnn.object?.thickness ??
|
||||
(isHighlighter ? freehandHighlighterWidth : inkWidth);
|
||||
const colorValue = selectedAnn.object?.color ?? (isHighlighter ? highlightColor : inkColor);
|
||||
const opacityValue = Math.round(((selectedAnn.object?.opacity ?? 1) * 100) || (isHighlighter ? highlightOpacity : 100));
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={600}>{t('annotation.editInk', 'Edit Pen')}</Text>
|
||||
<Text size="sm" fw={600}>
|
||||
{isHighlighter ? t('annotation.freehandHighlighter', 'Freehand Highlighter') : t('annotation.editInk', 'Edit Pen')}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.color', 'Color')}</Text>
|
||||
<ColorSwatchButton
|
||||
color={selectedAnn.object?.color ?? inkColor}
|
||||
color={colorValue}
|
||||
size={28}
|
||||
onClick={() => {
|
||||
setColorPickerTarget('ink');
|
||||
setColorPickerTarget(isHighlighter ? 'highlight' : 'ink');
|
||||
setIsColorPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{isHighlighter && (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.opacity', 'Opacity')}</Text>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
value={opacityValue}
|
||||
onChange={(value) => {
|
||||
setHighlightOpacity(value);
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ opacity: value / 100 }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.strokeWidth', 'Width')}</Text>
|
||||
<Slider
|
||||
min={1}
|
||||
max={12}
|
||||
value={selectedAnn.object?.strokeWidth ?? inkWidth}
|
||||
max={isHighlighter ? 20 : 12}
|
||||
value={thicknessValue}
|
||||
onChange={(value) => {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ strokeWidth: value }
|
||||
{
|
||||
strokeWidth: value,
|
||||
borderWidth: value,
|
||||
lineWidth: value,
|
||||
thickness: value,
|
||||
}
|
||||
);
|
||||
setInkWidth(value);
|
||||
if (isHighlighter) {
|
||||
setFreehandHighlighterWidth?.(value);
|
||||
} else {
|
||||
setInkWidth(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@ -606,12 +681,24 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 3) {
|
||||
const derivedTool = deriveToolFromAnnotation(selectedAnn.object);
|
||||
const isNote = derivedTool === 'note';
|
||||
if (type === 3 || toolId === 'text' || toolId === 'note') {
|
||||
const isNote = toolId === 'note';
|
||||
const selectedBackground =
|
||||
selectedAnn.object?.backgroundColor ??
|
||||
(isNote ? noteBackgroundColor || '#ffffff' : textBackgroundColor || '#ffffff');
|
||||
const alignValue = selectedAnn.object?.textAlign;
|
||||
const currentAlign =
|
||||
typeof alignValue === 'number'
|
||||
? alignValue === 1
|
||||
? 'center'
|
||||
: alignValue === 2
|
||||
? 'right'
|
||||
: 'left'
|
||||
: alignValue === 'center'
|
||||
? 'center'
|
||||
: alignValue === 'right'
|
||||
? 'right'
|
||||
: 'left';
|
||||
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
@ -725,12 +812,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
<Text size="xs" c="dimmed" mb={4}>{t('annotation.textAlignment', 'Text Alignment')}</Text>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant={(selectedAnn.object?.textAlign ?? 'left') === 'left' ? 'filled' : 'default'}
|
||||
variant={currentAlign === 'left' ? 'filled' : 'default'}
|
||||
onClick={() => {
|
||||
setTextAlignment('left');
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ textAlign: 'left' }
|
||||
{ textAlign: 0 }
|
||||
);
|
||||
}}
|
||||
size="md"
|
||||
@ -738,12 +826,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
<LocalIcon icon="format-align-left" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant={(selectedAnn.object?.textAlign ?? 'left') === 'center' ? 'filled' : 'default'}
|
||||
variant={currentAlign === 'center' ? 'filled' : 'default'}
|
||||
onClick={() => {
|
||||
setTextAlignment('center');
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ textAlign: 'center' }
|
||||
{ textAlign: 1 }
|
||||
);
|
||||
}}
|
||||
size="md"
|
||||
@ -751,12 +840,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
<LocalIcon icon="format-align-center" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant={(selectedAnn.object?.textAlign ?? 'left') === 'right' ? 'filled' : 'default'}
|
||||
variant={currentAlign === 'right' ? 'filled' : 'default'}
|
||||
onClick={() => {
|
||||
setTextAlignment('right');
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ textAlign: 'right' }
|
||||
{ textAlign: 2 }
|
||||
);
|
||||
}}
|
||||
size="md"
|
||||
@ -770,7 +860,35 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 4) {
|
||||
if (type === 13 || toolId === 'stamp') {
|
||||
const imageSrc = selectedAnn.object?.imageSrc || selectedAnn.object?.data || selectedAnn.object?.url;
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={600}>{t('annotation.stamp', 'Add Image')}</Text>
|
||||
{imageSrc ? (
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" c="dimmed">{t('annotation.imagePreview', 'Preview')}</Text>
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={t('annotation.stamp', 'Add Image')}
|
||||
style={{ maxWidth: '100%', maxHeight: '180px', objectFit: 'contain', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('annotation.unsupportedType', 'This annotation type is not fully supported for editing.')}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('annotation.editStampHint', 'To change the image, delete this stamp and add a new one.')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
if ([4, 8].includes(type) || toolId === 'line' || toolId === 'polyline') {
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
@ -826,8 +944,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if ([5, 6, 7].includes(type)) {
|
||||
if ([5, 6, 7].includes(type) || toolId === 'square' || toolId === 'circle' || toolId === 'polygon') {
|
||||
const shapeName = type === 5 ? 'Square' : type === 6 ? 'Circle' : 'Polygon';
|
||||
const strokeColorValue = selectedAnn.object?.strokeColor ?? shapeStrokeColor;
|
||||
const fillColorValue = selectedAnn.object?.color ?? shapeFillColor;
|
||||
const opacityValue = Math.round(((selectedAnn.object?.opacity ?? shapeOpacity / 100) * 100) || 100);
|
||||
const pageIndex = selectedAnn.object?.pageIndex ?? 0;
|
||||
const annId = selectedAnn.object?.id;
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
<Stack gap="sm">
|
||||
@ -836,7 +959,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
<Stack gap={4} align="center">
|
||||
<Text size="xs" c="dimmed">{t('annotation.strokeColor', 'Stroke Color')}</Text>
|
||||
<ColorSwatchButton
|
||||
color={selectedAnn.object?.strokeColor ?? shapeStrokeColor}
|
||||
color={strokeColorValue}
|
||||
size={28}
|
||||
onClick={() => {
|
||||
setColorPickerTarget('shapeStroke');
|
||||
@ -847,7 +970,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
<Stack gap={4} align="center">
|
||||
<Text size="xs" c="dimmed">{t('annotation.fillColor', 'Fill Color')}</Text>
|
||||
<ColorSwatchButton
|
||||
color={selectedAnn.object?.color ?? shapeFillColor}
|
||||
color={fillColorValue}
|
||||
size={28}
|
||||
onClick={() => {
|
||||
setColorPickerTarget('shapeFill');
|
||||
@ -861,13 +984,18 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
value={Math.round(((selectedAnn.object?.opacity ?? 1) * 100) || 100)}
|
||||
value={opacityValue}
|
||||
onChange={(value) => {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{ opacity: value / 100 }
|
||||
);
|
||||
setShapeOpacity(value);
|
||||
setShapeStrokeOpacity(value);
|
||||
setShapeFillOpacity(value);
|
||||
if (annId) {
|
||||
annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, {
|
||||
opacity: value / 100,
|
||||
strokeOpacity: value / 100,
|
||||
fillOpacity: value / 100,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@ -879,15 +1007,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
max={12}
|
||||
value={selectedAnn.object?.borderWidth ?? shapeThickness}
|
||||
onChange={(value) => {
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{
|
||||
if (annId) {
|
||||
annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, {
|
||||
borderWidth: value,
|
||||
strokeWidth: value,
|
||||
lineWidth: value,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
setShapeThickness(value);
|
||||
}}
|
||||
/>
|
||||
@ -897,15 +1023,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
variant={(selectedAnn.object?.borderWidth ?? shapeThickness) === 0 ? 'filled' : 'light'}
|
||||
onClick={() => {
|
||||
const newValue = (selectedAnn.object?.borderWidth ?? shapeThickness) === 0 ? 1 : 0;
|
||||
annotationApiRef?.current?.updateAnnotation?.(
|
||||
selectedAnn.object?.pageIndex ?? 0,
|
||||
selectedAnn.object?.id,
|
||||
{
|
||||
if (annId) {
|
||||
annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, {
|
||||
borderWidth: newValue,
|
||||
strokeWidth: newValue,
|
||||
lineWidth: newValue,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
setShapeThickness(newValue);
|
||||
}}
|
||||
>
|
||||
@ -941,6 +1065,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
colorPickerTarget !== 'noteBackground' &&
|
||||
colorPickerTarget !== 'shapeStroke' &&
|
||||
colorPickerTarget !== 'shapeFill' &&
|
||||
colorPickerTarget !== 'underline' &&
|
||||
colorPickerTarget !== null
|
||||
}
|
||||
opacity={
|
||||
@ -1108,8 +1233,8 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Tooltip label={t('annotation.selectAndMove', 'Select and move annotations')}>
|
||||
<Group gap="xs" wrap="nowrap" align="center">
|
||||
<Tooltip label={t('annotation.selectAndMove', 'Select and edit annotations')}>
|
||||
<ActionIcon
|
||||
variant={activeTool === 'select' ? 'filled' : 'default'}
|
||||
size="lg"
|
||||
@ -1126,32 +1251,19 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
>
|
||||
<LocalIcon icon="material-symbols:touch-app-rounded" width={20} height={20} />
|
||||
<Text component="span" size="sm" fw={500}>
|
||||
{t('annotation.selectAndMove', 'Select and Move')}
|
||||
{t('annotation.selectAndMove', 'Select and Edit')}
|
||||
</Text>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t('annotation.undo', 'Undo')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={undo}
|
||||
disabled={!historyAvailability.canUndo}
|
||||
>
|
||||
<LocalIcon icon="undo" width={20} height={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t('annotation.redo', 'Redo')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={redo}
|
||||
disabled={!historyAvailability.canRedo}
|
||||
>
|
||||
<LocalIcon icon="redo" width={20} height={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DrawingControls
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
canUndo={historyAvailability.canUndo}
|
||||
canRedo={historyAvailability.canRedo}
|
||||
showPlaceButton={false}
|
||||
additionalControls={null}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Box>
|
||||
@ -1174,9 +1286,11 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
{renderToolButtons(otherTools)}
|
||||
</Box>
|
||||
|
||||
{!selectedAnn && defaultStyleControls}
|
||||
{activeTool !== 'select' && defaultStyleControls}
|
||||
|
||||
{selectedAnn && selectedAnnotationControls}
|
||||
{activeTool === 'select' && selectedAnn && selectedAnnotationControls}
|
||||
|
||||
{activeTool === 'select' && !selectedAnn && defaultStyleControls}
|
||||
|
||||
{colorPickerComponent}
|
||||
|
||||
|
||||
@ -10,12 +10,67 @@ interface UseAnnotationSelectionParams {
|
||||
setSelectedTextDraft: (text: string) => void;
|
||||
setSelectedFontSize: (size: number) => void;
|
||||
setInkWidth: (value: number) => void;
|
||||
setFreehandHighlighterWidth?: (value: number) => void;
|
||||
setShapeThickness: (value: number) => void;
|
||||
setTextColor: (value: string) => void;
|
||||
setTextBackgroundColor: (value: string) => void;
|
||||
setNoteBackgroundColor: (value: string) => void;
|
||||
setInkColor: (value: string) => void;
|
||||
setHighlightColor: (value: string) => void;
|
||||
setHighlightOpacity: (value: number) => void;
|
||||
setUnderlineColor: (value: string) => void;
|
||||
setUnderlineOpacity: (value: number) => void;
|
||||
setStrikeoutColor: (value: string) => void;
|
||||
setStrikeoutOpacity: (value: number) => void;
|
||||
setSquigglyColor: (value: string) => void;
|
||||
setSquigglyOpacity: (value: number) => void;
|
||||
setShapeStrokeColor: (value: string) => void;
|
||||
setShapeFillColor: (value: string) => void;
|
||||
setShapeOpacity: (value: number) => void;
|
||||
setShapeStrokeOpacity: (value: number) => void;
|
||||
setShapeFillOpacity: (value: number) => void;
|
||||
setTextAlignment: (value: 'left' | 'center' | 'right') => void;
|
||||
}
|
||||
|
||||
const MARKUP_TOOL_IDS = ['highlight', 'underline', 'strikeout', 'squiggly'] as const;
|
||||
const DRAWING_TOOL_IDS = ['ink', 'inkHighlighter'] as const;
|
||||
|
||||
const isTextMarkupAnnotation = (annotation: any): boolean => {
|
||||
const toolId =
|
||||
annotation?.customData?.annotationToolId ||
|
||||
annotation?.customData?.toolId ||
|
||||
annotation?.object?.customData?.annotationToolId ||
|
||||
annotation?.object?.customData?.toolId;
|
||||
if (toolId && MARKUP_TOOL_IDS.includes(toolId)) return true;
|
||||
|
||||
const type = annotation?.type ?? annotation?.object?.type;
|
||||
if (typeof type === 'number' && [9, 10, 11, 12].includes(type)) return true;
|
||||
|
||||
const subtype = annotation?.subtype ?? annotation?.object?.subtype;
|
||||
if (typeof subtype === 'string') {
|
||||
const lower = subtype.toLowerCase();
|
||||
if (MARKUP_TOOL_IDS.some((t) => lower.includes(t))) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const shouldStayOnPlacementTool = (annotation: any, derivedTool?: string | null | undefined): boolean => {
|
||||
const toolId =
|
||||
derivedTool ||
|
||||
annotation?.customData?.annotationToolId ||
|
||||
annotation?.customData?.toolId ||
|
||||
annotation?.object?.customData?.annotationToolId ||
|
||||
annotation?.object?.customData?.toolId;
|
||||
|
||||
if (toolId && (MARKUP_TOOL_IDS.includes(toolId as any) || DRAWING_TOOL_IDS.includes(toolId as any))) {
|
||||
return true;
|
||||
}
|
||||
const type = annotation?.type ?? annotation?.object?.type;
|
||||
if (typeof type === 'number' && type === 15) return true; // ink family
|
||||
if (isTextMarkupAnnotation(annotation)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
export function useAnnotationSelection({
|
||||
annotationApiRef,
|
||||
deriveToolFromAnnotation,
|
||||
@ -29,6 +84,22 @@ export function useAnnotationSelection({
|
||||
setTextColor,
|
||||
setTextBackgroundColor,
|
||||
setNoteBackgroundColor,
|
||||
setInkColor,
|
||||
setHighlightColor,
|
||||
setHighlightOpacity,
|
||||
setUnderlineColor,
|
||||
setUnderlineOpacity,
|
||||
setStrikeoutColor,
|
||||
setStrikeoutOpacity,
|
||||
setSquigglyColor,
|
||||
setSquigglyOpacity,
|
||||
setShapeStrokeColor,
|
||||
setShapeFillColor,
|
||||
setShapeOpacity,
|
||||
setShapeStrokeOpacity,
|
||||
setShapeFillOpacity,
|
||||
setTextAlignment,
|
||||
setFreehandHighlighterWidth,
|
||||
}: UseAnnotationSelectionParams) {
|
||||
const [selectedAnn, setSelectedAnn] = useState<any | null>(null);
|
||||
const [selectedAnnId, setSelectedAnnId] = useState<string | null>(null);
|
||||
@ -39,9 +110,12 @@ export function useAnnotationSelection({
|
||||
const annObject = ann?.object ?? ann ?? null;
|
||||
const annId = annObject?.id ?? null;
|
||||
const type = annObject?.type;
|
||||
const derivedTool = annObject ? deriveToolFromAnnotation(annObject) : undefined;
|
||||
selectedAnnIdRef.current = annId;
|
||||
setSelectedAnnId(annId);
|
||||
setSelectedAnn(ann || null);
|
||||
// Normalize selected annotation to always expose .object for edit panels
|
||||
const normalizedSelection = ann?.object ? ann : annObject ? { object: annObject } : null;
|
||||
setSelectedAnn(normalizedSelection);
|
||||
|
||||
if (annObject?.contents !== undefined) {
|
||||
setSelectedTextDraft(annObject.contents ?? '');
|
||||
@ -49,30 +123,105 @@ export function useAnnotationSelection({
|
||||
if (annObject?.fontSize !== undefined) {
|
||||
setSelectedFontSize(annObject.fontSize ?? 14);
|
||||
}
|
||||
if (annObject?.textAlign !== undefined) {
|
||||
const align = annObject.textAlign;
|
||||
if (typeof align === 'string') {
|
||||
const normalized = align === 'center' ? 'center' : align === 'right' ? 'right' : 'left';
|
||||
setTextAlignment(normalized);
|
||||
} else if (typeof align === 'number') {
|
||||
const normalized = align === 1 ? 'center' : align === 2 ? 'right' : 'left';
|
||||
setTextAlignment(normalized);
|
||||
}
|
||||
}
|
||||
if (type === 3) {
|
||||
const derivedTool = deriveToolFromAnnotation(annObject);
|
||||
const background = annObject?.backgroundColor as string | undefined;
|
||||
if (annObject?.textColor) {
|
||||
setTextColor(annObject.textColor);
|
||||
const background =
|
||||
(annObject?.backgroundColor as string | undefined) ||
|
||||
(annObject?.fillColor as string | undefined) ||
|
||||
undefined;
|
||||
const textColor = (annObject?.textColor as string | undefined) || (annObject?.color as string | undefined);
|
||||
if (textColor) {
|
||||
setTextColor(textColor);
|
||||
}
|
||||
if (derivedTool === 'note') {
|
||||
if (background) {
|
||||
setNoteBackgroundColor(background);
|
||||
}
|
||||
setNoteBackgroundColor(background || '');
|
||||
} else {
|
||||
setTextBackgroundColor(background || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 15 && annObject?.strokeWidth !== undefined) {
|
||||
setInkWidth(annObject.strokeWidth ?? 2);
|
||||
} else if (type >= 4 && type <= 8 && annObject?.strokeWidth !== undefined) {
|
||||
setShapeThickness(annObject.strokeWidth ?? 1);
|
||||
if (type === 15) {
|
||||
const width =
|
||||
annObject?.strokeWidth ?? annObject?.borderWidth ?? annObject?.lineWidth ?? annObject?.thickness;
|
||||
if (derivedTool === 'inkHighlighter') {
|
||||
if (annObject?.color) setHighlightColor(annObject.color);
|
||||
if (annObject?.opacity !== undefined) {
|
||||
setHighlightOpacity(Math.round((annObject.opacity ?? 1) * 100));
|
||||
}
|
||||
if (width !== undefined && setFreehandHighlighterWidth) {
|
||||
setFreehandHighlighterWidth(width);
|
||||
}
|
||||
} else {
|
||||
if (width !== undefined) setInkWidth(width ?? 2);
|
||||
if (annObject?.color) {
|
||||
setInkColor(annObject.color);
|
||||
}
|
||||
}
|
||||
} else if (type >= 4 && type <= 8) {
|
||||
const width = annObject?.strokeWidth ?? annObject?.borderWidth ?? annObject?.lineWidth;
|
||||
if (width !== undefined) {
|
||||
setShapeThickness(width ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
const matchingTool = deriveToolFromAnnotation(annObject);
|
||||
if (matchingTool && matchingTool !== activeToolRef.current && !manualToolSwitch.current) {
|
||||
setActiveTool(matchingTool);
|
||||
if (type === 9) {
|
||||
if (annObject?.color) setHighlightColor(annObject.color);
|
||||
if (annObject?.opacity !== undefined) setHighlightOpacity(Math.round((annObject.opacity ?? 1) * 100));
|
||||
} else if (type === 10) {
|
||||
if (annObject?.color) setUnderlineColor(annObject.color);
|
||||
if (annObject?.opacity !== undefined) setUnderlineOpacity(Math.round((annObject.opacity ?? 1) * 100));
|
||||
} else if (type === 12) {
|
||||
if (annObject?.color) setStrikeoutColor(annObject.color);
|
||||
if (annObject?.opacity !== undefined) setStrikeoutOpacity(Math.round((annObject.opacity ?? 1) * 100));
|
||||
} else if (type === 11) {
|
||||
if (annObject?.color) setSquigglyColor(annObject.color);
|
||||
if (annObject?.opacity !== undefined) setSquigglyOpacity(Math.round((annObject.opacity ?? 1) * 100));
|
||||
}
|
||||
|
||||
if ([4, 5, 6, 7, 8].includes(type)) {
|
||||
const stroke = (annObject?.strokeColor as string | undefined) ?? (annObject?.color as string | undefined);
|
||||
if (stroke) setShapeStrokeColor(stroke);
|
||||
if ([5, 6, 7].includes(type)) {
|
||||
const fill = (annObject?.color as string | undefined) ?? (annObject?.fillColor as string | undefined);
|
||||
if (fill) setShapeFillColor(fill);
|
||||
}
|
||||
const opacity =
|
||||
annObject?.opacity !== undefined ? Math.round((annObject.opacity ?? 1) * 100) : undefined;
|
||||
const strokeOpacityValue =
|
||||
annObject?.strokeOpacity !== undefined
|
||||
? Math.round((annObject.strokeOpacity ?? 1) * 100)
|
||||
: undefined;
|
||||
const fillOpacityValue =
|
||||
annObject?.fillOpacity !== undefined ? Math.round((annObject.fillOpacity ?? 1) * 100) : undefined;
|
||||
if (opacity !== undefined) {
|
||||
setShapeOpacity(opacity);
|
||||
setShapeStrokeOpacity(strokeOpacityValue ?? opacity);
|
||||
setShapeFillOpacity(fillOpacityValue ?? opacity);
|
||||
} else {
|
||||
if (strokeOpacityValue !== undefined) setShapeStrokeOpacity(strokeOpacityValue);
|
||||
if (fillOpacityValue !== undefined) setShapeFillOpacity(fillOpacityValue);
|
||||
}
|
||||
}
|
||||
|
||||
const matchingTool = derivedTool;
|
||||
const stayOnPlacement = shouldStayOnPlacementTool(annObject, matchingTool);
|
||||
if (matchingTool && activeToolRef.current !== 'select' && !stayOnPlacement) {
|
||||
activeToolRef.current = 'select';
|
||||
setActiveTool('select');
|
||||
// Immediately enable select tool to avoid re-entering placement after creation.
|
||||
annotationApiRef.current?.activateAnnotationTool?.('select');
|
||||
} else if (activeToolRef.current === 'select') {
|
||||
// Keep the viewer in Select mode so clicking existing annotations does not re-enable placement.
|
||||
annotationApiRef.current?.activateAnnotationTool?.('select');
|
||||
}
|
||||
},
|
||||
[
|
||||
@ -87,6 +236,23 @@ export function useAnnotationSelection({
|
||||
setShapeThickness,
|
||||
setTextBackgroundColor,
|
||||
setTextColor,
|
||||
setInkColor,
|
||||
setHighlightColor,
|
||||
setHighlightOpacity,
|
||||
setUnderlineColor,
|
||||
setUnderlineOpacity,
|
||||
setStrikeoutColor,
|
||||
setStrikeoutOpacity,
|
||||
setSquigglyColor,
|
||||
setSquigglyOpacity,
|
||||
setShapeStrokeColor,
|
||||
setShapeFillColor,
|
||||
setShapeOpacity,
|
||||
setShapeStrokeOpacity,
|
||||
setShapeFillOpacity,
|
||||
setTextAlignment,
|
||||
setFreehandHighlighterWidth,
|
||||
shouldStayOnPlacementTool,
|
||||
]
|
||||
);
|
||||
|
||||
@ -94,12 +260,64 @@ export function useAnnotationSelection({
|
||||
const api = annotationApiRef.current as any;
|
||||
if (!api) return;
|
||||
|
||||
const checkSelection = () => {
|
||||
const ann = api.getSelectedAnnotation?.();
|
||||
const currentId = ann?.object?.id ?? ann?.id ?? null;
|
||||
if (currentId !== selectedAnnIdRef.current) {
|
||||
applySelectionFromAnnotation(ann ?? null);
|
||||
}
|
||||
};
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
if (typeof api.onAnnotationEvent === 'function') {
|
||||
const handler = (event: any) => {
|
||||
const ann = event?.annotation ?? event?.selectedAnnotation ?? null;
|
||||
switch (event?.type) {
|
||||
const eventType = event?.type;
|
||||
switch (eventType) {
|
||||
case 'create':
|
||||
case 'add':
|
||||
case 'added':
|
||||
case 'created':
|
||||
case 'annotationCreated':
|
||||
case 'annotationAdded':
|
||||
case 'complete': {
|
||||
const eventAnn = ann ?? api.getSelectedAnnotation?.();
|
||||
applySelectionFromAnnotation(eventAnn);
|
||||
const currentTool = activeToolRef.current;
|
||||
const tool =
|
||||
deriveToolFromAnnotation((eventAnn as any)?.object ?? eventAnn ?? api.getSelectedAnnotation?.()) ||
|
||||
currentTool;
|
||||
const stayOnPlacement =
|
||||
shouldStayOnPlacementTool(eventAnn, tool) ||
|
||||
(tool ? DRAWING_TOOL_IDS.includes(tool as any) : false);
|
||||
if (activeToolRef.current !== 'select' && !stayOnPlacement) {
|
||||
activeToolRef.current = 'select';
|
||||
setActiveTool('select');
|
||||
annotationApiRef.current?.activateAnnotationTool?.('select');
|
||||
}
|
||||
// Re-read selection after the viewer updates to ensure we have the full annotation object for the edit panel.
|
||||
setTimeout(() => {
|
||||
const selected = api.getSelectedAnnotation?.();
|
||||
applySelectionFromAnnotation(selected ?? eventAnn ?? null);
|
||||
const derivedAfter =
|
||||
deriveToolFromAnnotation((selected as any)?.object ?? selected ?? eventAnn ?? null) || activeToolRef.current;
|
||||
const stayOnPlacementAfter =
|
||||
shouldStayOnPlacementTool(selected ?? eventAnn ?? null, derivedAfter) ||
|
||||
(derivedAfter ? DRAWING_TOOL_IDS.includes(derivedAfter as any) : false);
|
||||
if (activeToolRef.current !== 'select' && !stayOnPlacementAfter) {
|
||||
activeToolRef.current = 'select';
|
||||
setActiveTool('select');
|
||||
annotationApiRef.current?.activateAnnotationTool?.('select');
|
||||
}
|
||||
}, 50);
|
||||
break;
|
||||
}
|
||||
case 'select':
|
||||
case 'selected':
|
||||
case 'annotationSelected':
|
||||
case 'annotationClicked':
|
||||
case 'annotationTapped':
|
||||
applySelectionFromAnnotation(ann ?? api.getSelectedAnnotation?.());
|
||||
break;
|
||||
case 'deselect':
|
||||
@ -127,20 +345,19 @@ export function useAnnotationSelection({
|
||||
};
|
||||
|
||||
const unsubscribe = api.onAnnotationEvent(handler);
|
||||
interval = setInterval(checkSelection, 450);
|
||||
return () => {
|
||||
if (typeof unsubscribe === 'function') {
|
||||
unsubscribe();
|
||||
}
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const ann = api.getSelectedAnnotation?.();
|
||||
if ((ann?.object?.id ?? null) !== selectedAnnIdRef.current) {
|
||||
applySelectionFromAnnotation(ann ?? null);
|
||||
}
|
||||
}, 350);
|
||||
return () => clearInterval(interval);
|
||||
interval = setInterval(checkSelection, 350);
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [annotationApiRef, applySelectionFromAnnotation]);
|
||||
|
||||
return {
|
||||
|
||||
@ -95,7 +95,7 @@ export const useAnnotationStyleState = (
|
||||
const [shapeOpacity, setShapeOpacity] = useState(50);
|
||||
const [shapeStrokeOpacity, setShapeStrokeOpacity] = useState(50);
|
||||
const [shapeFillOpacity, setShapeFillOpacity] = useState(50);
|
||||
const [shapeThickness, setShapeThickness] = useState(1);
|
||||
const [shapeThickness, setShapeThickness] = useState(2);
|
||||
|
||||
const buildToolOptions = useCallback<BuildToolOptionsFn>(
|
||||
(toolId, extras) => {
|
||||
@ -135,6 +135,7 @@ export const useAnnotationStyleState = (
|
||||
const textAlignNumber = textAlignment === 'left' ? 0 : textAlignment === 'center' ? 1 : 2;
|
||||
return {
|
||||
color: textColor,
|
||||
textColor: textColor,
|
||||
fontSize: textSize,
|
||||
textAlign: textAlignNumber,
|
||||
...(textBackgroundColor ? { fillColor: textBackgroundColor } : {}),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user