mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
Performance improvements
This commit is contained in:
parent
e7f7b7e201
commit
a5ec62fa08
@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { GRID_CONSTANTS } from './constants';
|
||||
@ -29,7 +29,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
selectionMode: boolean;
|
||||
isAnimating: boolean;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, isOver: boolean, dropSide: 'left' | 'right' | null, dragHandleProps?: any) => React.ReactNode;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, isOver: boolean, dragHandleProps?: any) => React.ReactNode;
|
||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
}
|
||||
@ -44,13 +44,11 @@ interface DraggableItemProps<T extends DragDropItem> {
|
||||
getBoxSelection: () => string[];
|
||||
activeId: string | null;
|
||||
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, isOver: boolean, dropSide: 'left' | 'right' | null, dragHandleProps?: any) => React.ReactNode;
|
||||
onUpdateDropTarget: (itemId: string | null) => void;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>, boxSelectedIds: string[], clearBoxSelection: () => void, getBoxSelection: () => string[], activeId: string | null, isOver: boolean, dragHandleProps?: any) => React.ReactNode;
|
||||
}
|
||||
|
||||
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, getThumbnailData, renderItem }: DraggableItemProps<T>) => {
|
||||
const elementRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [dropSide, setDropSide] = React.useState<'left' | 'right' | null>(null);
|
||||
|
||||
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, getThumbnailData, renderItem, onUpdateDropTarget }: DraggableItemProps<T>) => {
|
||||
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
|
||||
id: item.id,
|
||||
data: {
|
||||
@ -80,33 +78,23 @@ const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelec
|
||||
data: { index, pageNumber: index + 1 }
|
||||
});
|
||||
|
||||
// Track cursor position when hovered to determine drop side
|
||||
// Notify parent when hover state changes
|
||||
React.useEffect(() => {
|
||||
if (!isOver || !elementRef.current) {
|
||||
setDropSide(null);
|
||||
return;
|
||||
if (isOver) {
|
||||
onUpdateDropTarget(item.id);
|
||||
} else {
|
||||
onUpdateDropTarget(null);
|
||||
}
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
if (!elementRef.current) return;
|
||||
const rect = elementRef.current.getBoundingClientRect();
|
||||
const midpoint = rect.left + rect.width / 2;
|
||||
setDropSide(e.clientX < midpoint ? 'left' : 'right');
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
return () => window.removeEventListener('pointermove', handlePointerMove);
|
||||
}, [isOver]);
|
||||
}, [isOver, item.id, onUpdateDropTarget]);
|
||||
|
||||
const setNodeRef = useCallback((element: HTMLDivElement | null) => {
|
||||
elementRef.current = element;
|
||||
setDraggableRef(element);
|
||||
setDroppableRef(element);
|
||||
}, [setDraggableRef, setDroppableRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderItem(item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, isOver, dropSide, { ref: setNodeRef, ...attributes, ...listeners })}
|
||||
{renderItem(item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, isOver, { ref: setNodeRef, ...attributes, ...listeners })}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -132,12 +120,115 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
// Drag state
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [dragPreview, setDragPreview] = useState<{ src: string; rotation: number } | null>(null);
|
||||
const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
|
||||
const [dropSide, setDropSide] = useState<'left' | 'right' | null>(null);
|
||||
const lastCursorXRef = useRef<number | null>(null);
|
||||
|
||||
// Configure sensors for dnd-kit
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor)
|
||||
);
|
||||
|
||||
// Throttled pointer move handler for drop indicator
|
||||
// Calculate drop position based on cursor location relative to ALL items, not just hovered item
|
||||
useEffect(() => {
|
||||
if (!activeId) {
|
||||
setDropSide(null);
|
||||
setHoveredItemId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let rafId: number | null = null;
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
// Use the actual cursor position (pointer coordinates)
|
||||
const cursorX = e.clientX;
|
||||
const cursorY = e.clientY;
|
||||
|
||||
if (rafId === null) {
|
||||
rafId = requestAnimationFrame(() => {
|
||||
// Step 1: Group items by rows and find closest row to cursor
|
||||
const rows = new Map<number, Array<{ id: string; element: HTMLDivElement; rect: DOMRect }>>();
|
||||
|
||||
itemRefs.current.forEach((element, itemId) => {
|
||||
// Skip the item being dragged
|
||||
if (itemId === activeId) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
const rowCenter = rect.top + rect.height / 2;
|
||||
|
||||
// Group items by their vertical center position (items in same row will have similar centers)
|
||||
let foundRow = false;
|
||||
rows.forEach((items, rowY) => {
|
||||
if (Math.abs(rowY - rowCenter) < rect.height / 4) {
|
||||
items.push({ id: itemId, element, rect });
|
||||
foundRow = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!foundRow) {
|
||||
rows.set(rowCenter, [{ id: itemId, element, rect }]);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Find the closest row to cursor Y position
|
||||
let closestRowY = 0;
|
||||
let closestRowDistance = Infinity;
|
||||
rows.forEach((items, rowY) => {
|
||||
const distance = Math.abs(cursorY - rowY);
|
||||
if (distance < closestRowDistance) {
|
||||
closestRowDistance = distance;
|
||||
closestRowY = rowY;
|
||||
}
|
||||
});
|
||||
|
||||
const closestRow = rows.get(closestRowY);
|
||||
if (!closestRow || closestRow.length === 0) {
|
||||
setHoveredItemId(null);
|
||||
setDropSide(null);
|
||||
rafId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Within the closest row, find the closest edge to cursor X position
|
||||
let closestItemId: string | null = null;
|
||||
let closestDistance = Infinity;
|
||||
let closestSide: 'left' | 'right' = 'left';
|
||||
|
||||
closestRow.forEach(({ id, rect }) => {
|
||||
// Calculate distance to left and right edges
|
||||
const distanceToLeft = Math.abs(cursorX - rect.left);
|
||||
const distanceToRight = Math.abs(cursorX - rect.right);
|
||||
|
||||
// Find the closest edge
|
||||
if (distanceToLeft < closestDistance) {
|
||||
closestDistance = distanceToLeft;
|
||||
closestItemId = id;
|
||||
closestSide = 'left';
|
||||
}
|
||||
if (distanceToRight < closestDistance) {
|
||||
closestDistance = distanceToRight;
|
||||
closestItemId = id;
|
||||
closestSide = 'right';
|
||||
}
|
||||
});
|
||||
|
||||
setHoveredItemId(closestItemId);
|
||||
setDropSide(closestSide);
|
||||
rafId = null;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [activeId]);
|
||||
|
||||
// Responsive grid configuration
|
||||
const [itemsPerRow, setItemsPerRow] = useState(4);
|
||||
const OVERSCAN = visibleItems.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
|
||||
@ -300,14 +391,19 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveId(null);
|
||||
setDragPreview(null);
|
||||
setHoveredItemId(null);
|
||||
setDropSide(null);
|
||||
}, []);
|
||||
|
||||
// Handle drag end
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
const finalDropSide = dropSide;
|
||||
setActiveId(null);
|
||||
setDragPreview(null);
|
||||
setHoveredItemId(null);
|
||||
setDropSide(null);
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
@ -322,18 +418,9 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
const sourcePageNumber = activeData.pageNumber;
|
||||
let targetIndex = overData.index;
|
||||
|
||||
// Calculate drop position from final cursor position
|
||||
const overElement = itemRefs.current.get(over.id as string);
|
||||
if (overElement) {
|
||||
const activatorEvent = (event as any).activatorEvent;
|
||||
const cursorX = activatorEvent ? activatorEvent.clientX + (event.delta?.x || 0) : null;
|
||||
if (cursorX) {
|
||||
const rect = overElement.getBoundingClientRect();
|
||||
const midpoint = rect.left + rect.width / 2;
|
||||
if (cursorX >= midpoint) {
|
||||
targetIndex = targetIndex + 1;
|
||||
}
|
||||
}
|
||||
// Use the final drop side to adjust target index
|
||||
if (finalDropSide === 'right') {
|
||||
targetIndex = targetIndex + 1;
|
||||
}
|
||||
|
||||
// Check if this page is box-selected
|
||||
@ -347,7 +434,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
if (pagesToDrag) {
|
||||
clearBoxSelection();
|
||||
}
|
||||
}, [boxSelectedPageIds, onReorderPages, clearBoxSelection]);
|
||||
}, [boxSelectedPageIds, dropSide, onReorderPages, clearBoxSelection]);
|
||||
|
||||
// Calculate optimal width for centering
|
||||
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
@ -368,6 +455,37 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
zIndex: 1000,
|
||||
} : null;
|
||||
|
||||
// Calculate drop indicator position
|
||||
const dropIndicatorStyle = useMemo(() => {
|
||||
if (!hoveredItemId || !dropSide || !activeId) return null;
|
||||
|
||||
const element = itemRefs.current.get(hoveredItemId);
|
||||
const container = containerRef.current;
|
||||
if (!element || !container) return null;
|
||||
|
||||
const itemRect = element.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
const top = itemRect.top - containerRect.top;
|
||||
const height = itemRect.height;
|
||||
const left = dropSide === 'left'
|
||||
? itemRect.left - containerRect.left - itemGap / 2
|
||||
: itemRect.right - containerRect.left + itemGap / 2;
|
||||
|
||||
return {
|
||||
position: 'absolute' as const,
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: '4px',
|
||||
height: `${height}px`,
|
||||
backgroundColor: 'rgba(96, 165, 250, 0.8)',
|
||||
borderRadius: '2px',
|
||||
boxShadow: '0 0 8px 2px rgba(96, 165, 250, 0.6)',
|
||||
zIndex: 1001,
|
||||
pointerEvents: 'none' as const,
|
||||
};
|
||||
}, [hoveredItemId, dropSide, activeId, itemGap]);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@ -391,6 +509,9 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
{/* Selection box overlay */}
|
||||
{selectionBoxStyle && <div style={selectionBoxStyle} />}
|
||||
|
||||
{/* Global drop indicator */}
|
||||
{dropIndicatorStyle && <div style={dropIndicatorStyle} />}
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
@ -440,6 +561,7 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
getBoxSelection={getBoxSelection}
|
||||
activeId={activeId}
|
||||
getThumbnailData={getThumbnailData}
|
||||
onUpdateDropTarget={setHoveredItemId}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -974,7 +974,7 @@ const PageEditor = ({
|
||||
rotation: page.rotation || 0
|
||||
};
|
||||
}}
|
||||
renderItem={(page, index, refs, boxSelectedIds, clearBoxSelection, getBoxSelection, activeId, isOver, dropSide, dragHandleProps) => {
|
||||
renderItem={(page, index, refs, boxSelectedIds, clearBoxSelection, getBoxSelection, activeId, isOver, dragHandleProps) => {
|
||||
const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0;
|
||||
const isBoxSelected = boxSelectedIds.includes(page.id);
|
||||
return (
|
||||
@ -995,7 +995,6 @@ const PageEditor = ({
|
||||
getBoxSelection={getBoxSelection}
|
||||
activeId={activeId}
|
||||
isOver={isOver}
|
||||
dropSide={dropSide}
|
||||
pageRefs={refs}
|
||||
dragHandleProps={dragHandleProps}
|
||||
onReorderPages={handleReorderPages}
|
||||
|
||||
@ -30,7 +30,6 @@ interface PageThumbnailProps {
|
||||
getBoxSelection?: () => string[];
|
||||
activeId: string | null;
|
||||
isOver: boolean;
|
||||
dropSide: 'left' | 'right' | null;
|
||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
dragHandleProps?: any;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
@ -65,7 +64,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
getBoxSelection,
|
||||
activeId,
|
||||
isOver,
|
||||
dropSide,
|
||||
pageRefs,
|
||||
dragHandleProps,
|
||||
onReorderPages,
|
||||
@ -304,22 +302,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Drop indicator - bar on left or right edge */}
|
||||
{isOver && !isDragging && dropSide && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
[dropSide]: '-4px',
|
||||
top: '0',
|
||||
width: '4px',
|
||||
height: '100%',
|
||||
backgroundColor: '#22c55e',
|
||||
borderRadius: '2px',
|
||||
zIndex: 1000,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<div
|
||||
className={styles.checkboxContainer}
|
||||
|
||||
@ -304,25 +304,31 @@ const TopControls = ({
|
||||
});
|
||||
}, [setCurrentView]);
|
||||
|
||||
// Memoize pageEditorState object to prevent recreating on every render
|
||||
const pageEditorState = useMemo<PageEditorState>(() => ({
|
||||
files: pageEditorFiles,
|
||||
selectedCount,
|
||||
totalCount,
|
||||
onToggleSelection: toggleFileSelection,
|
||||
onReorder: handleReorder,
|
||||
fileColorMap,
|
||||
}), [pageEditorFiles, selectedCount, totalCount, toggleFileSelection, handleReorder, fileColorMap]);
|
||||
|
||||
// Memoize view options to prevent SegmentedControl re-renders
|
||||
const viewOptions = useMemo(() => createViewOptions(
|
||||
currentView,
|
||||
switchingTo,
|
||||
activeFiles,
|
||||
currentFileIndex,
|
||||
onFileSelect,
|
||||
pageEditorState
|
||||
), [currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect, pageEditorState]);
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||
<div className="flex justify-center mt-[0.5rem]">
|
||||
<SegmentedControl
|
||||
data={createViewOptions(
|
||||
currentView,
|
||||
switchingTo,
|
||||
activeFiles,
|
||||
currentFileIndex,
|
||||
onFileSelect,
|
||||
{
|
||||
files: pageEditorFiles,
|
||||
selectedCount,
|
||||
totalCount,
|
||||
onToggleSelection: toggleFileSelection,
|
||||
onReorder: handleReorder,
|
||||
fileColorMap,
|
||||
}
|
||||
)}
|
||||
data={viewOptions}
|
||||
value={currentView}
|
||||
onChange={handleViewChange}
|
||||
color="blue"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { preferencesService, UserPreferences, DEFAULT_PREFERENCES } from '../services/preferencesService';
|
||||
|
||||
interface PreferencesContextValue {
|
||||
@ -50,15 +50,15 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
||||
setPreferences(DEFAULT_PREFERENCES);
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo(() => ({
|
||||
preferences,
|
||||
updatePreference,
|
||||
resetPreferences,
|
||||
isLoading,
|
||||
}), [preferences, updatePreference, resetPreferences, isLoading]);
|
||||
|
||||
return (
|
||||
<PreferencesContext.Provider
|
||||
value={{
|
||||
preferences,
|
||||
updatePreference,
|
||||
resetPreferences,
|
||||
isLoading,
|
||||
}}
|
||||
>
|
||||
<PreferencesContext.Provider value={value}>
|
||||
{children}
|
||||
</PreferencesContext.Provider>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
||||
import React, { createContext, useContext, useState, ReactNode, useRef, useMemo, useCallback } from 'react';
|
||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import { useNavigation } from './NavigationContext';
|
||||
|
||||
@ -280,21 +280,21 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleThumbnailSidebar = () => {
|
||||
const toggleThumbnailSidebar = useCallback(() => {
|
||||
setIsThumbnailSidebarVisible(prev => !prev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleAnnotationsVisibility = () => {
|
||||
const toggleAnnotationsVisibility = useCallback(() => {
|
||||
setIsAnnotationsVisible(prev => !prev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setAnnotationMode = (enabled: boolean) => {
|
||||
const setAnnotationMode = useCallback((enabled: boolean) => {
|
||||
setIsAnnotationModeState(enabled);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleAnnotationMode = () => {
|
||||
const toggleAnnotationMode = useCallback(() => {
|
||||
setIsAnnotationModeState(prev => !prev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// State getters - read from bridge refs
|
||||
const getScrollState = (): ScrollState => {
|
||||
@ -334,7 +334,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
// Action handlers - call APIs directly
|
||||
const scrollActions = {
|
||||
const scrollActions = useMemo(() => ({
|
||||
scrollToPage: (page: number) => {
|
||||
const api = bridgeRefs.current.scroll?.api;
|
||||
if (api?.scrollToPage) {
|
||||
@ -366,9 +366,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
api.scrollToPage({ pageNumber: scrollState.totalPages });
|
||||
}
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const zoomActions = {
|
||||
const zoomActions = useMemo(() => ({
|
||||
zoomIn: () => {
|
||||
const api = bridgeRefs.current.zoom?.api;
|
||||
if (api?.zoomIn) {
|
||||
@ -405,9 +405,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
api.requestZoom(level);
|
||||
}
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const panActions = {
|
||||
const panActions = useMemo(() => ({
|
||||
enablePan: () => {
|
||||
const api = bridgeRefs.current.pan?.api;
|
||||
if (api?.enable) {
|
||||
@ -426,9 +426,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
api.toggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const selectionActions = {
|
||||
const selectionActions = useMemo(() => ({
|
||||
copyToClipboard: () => {
|
||||
const api = bridgeRefs.current.selection?.api;
|
||||
if (api?.copyToClipboard) {
|
||||
@ -449,9 +449,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const spreadActions = {
|
||||
const spreadActions = useMemo(() => ({
|
||||
setSpreadMode: (mode: SpreadMode) => {
|
||||
const api = bridgeRefs.current.spread?.api;
|
||||
if (api?.setSpreadMode) {
|
||||
@ -471,9 +471,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
api.toggleSpreadMode();
|
||||
}
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const rotationActions = {
|
||||
const rotationActions = useMemo(() => ({
|
||||
rotateForward: () => {
|
||||
const api = bridgeRefs.current.rotation?.api;
|
||||
if (api?.rotateForward) {
|
||||
@ -499,9 +499,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const searchActions = {
|
||||
const searchActions = useMemo(() => ({
|
||||
search: async (query: string) => {
|
||||
const api = bridgeRefs.current.search?.api;
|
||||
if (api?.search) {
|
||||
@ -526,9 +526,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
api.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const exportActions = {
|
||||
const exportActions = useMemo(() => ({
|
||||
download: () => {
|
||||
const api = bridgeRefs.current.export?.api;
|
||||
if (api?.download) {
|
||||
@ -548,29 +548,29 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}), []);
|
||||
|
||||
const registerImmediateZoomUpdate = (callback: (percent: number) => void) => {
|
||||
const registerImmediateZoomUpdate = useCallback((callback: (percent: number) => void) => {
|
||||
immediateZoomUpdateCallback.current = callback;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const registerImmediateScrollUpdate = (callback: (currentPage: number, totalPages: number) => void) => {
|
||||
const registerImmediateScrollUpdate = useCallback((callback: (currentPage: number, totalPages: number) => void) => {
|
||||
immediateScrollUpdateCallback.current = callback;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const triggerImmediateScrollUpdate = (currentPage: number, totalPages: number) => {
|
||||
const triggerImmediateScrollUpdate = useCallback((currentPage: number, totalPages: number) => {
|
||||
if (immediateScrollUpdateCallback.current) {
|
||||
immediateScrollUpdateCallback.current(currentPage, totalPages);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const triggerImmediateZoomUpdate = (zoomPercent: number) => {
|
||||
const triggerImmediateZoomUpdate = useCallback((zoomPercent: number) => {
|
||||
if (immediateZoomUpdateCallback.current) {
|
||||
immediateZoomUpdateCallback.current(zoomPercent);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value: ViewerContextType = {
|
||||
const value = useMemo<ViewerContextType>(() => ({
|
||||
// UI state
|
||||
isThumbnailSidebarVisible,
|
||||
toggleThumbnailSidebar,
|
||||
@ -615,7 +615,20 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
|
||||
// Bridge registration
|
||||
registerBridge,
|
||||
};
|
||||
}), [
|
||||
isThumbnailSidebarVisible,
|
||||
isAnnotationsVisible,
|
||||
isAnnotationMode,
|
||||
activeFileIndex,
|
||||
scrollActions,
|
||||
zoomActions,
|
||||
panActions,
|
||||
selectionActions,
|
||||
spreadActions,
|
||||
rotationActions,
|
||||
searchActions,
|
||||
exportActions,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ViewerContext.Provider value={value}>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user