mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Fix and improve pageeditor
This commit is contained in:
parent
9b14609236
commit
646cedfb0f
@ -11,7 +11,8 @@
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(node:*)"
|
||||
"Bash(node:*)",
|
||||
"Bash(npm run dev:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"defaultMode": "acceptEdits"
|
||||
|
52
frontend/package-lock.json
generated
52
frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mantine/core": "^8.0.1",
|
||||
@ -17,6 +18,7 @@
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@ -119,6 +121,17 @@
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.4.tgz",
|
||||
"integrity": "sha512-lZHnO9BJdHPKnwB0uvVUCyDnIhL+WAHzXQ2EXX0qacogOsnvIUiCgY0BLKhBqTCWln3/f/Ox5jU54MKO6ayh9A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"bind-event-listener": "^3.0.0",
|
||||
"raf-schd": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@ -2226,6 +2239,33 @@
|
||||
"tailwindcss": "4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
@ -2876,6 +2916,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bind-event-listener": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz",
|
||||
"integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@ -6261,6 +6307,12 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
|
@ -5,6 +5,7 @@
|
||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"proxy": "http://localhost:8080",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mantine/core": "^8.0.1",
|
||||
@ -13,6 +14,7 @@
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@ -34,8 +36,6 @@
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npx tsc --noEmit && vite",
|
||||
"build": "npx tsc --noEmit && vite build",
|
||||
"dev": "npx tsc --noEmit && vite",
|
||||
"build": "npx tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
|
@ -752,24 +752,10 @@ const FileEditor = ({
|
||||
<SkeletonLoader type="fileGrid" count={6} />
|
||||
</Box>
|
||||
) : (
|
||||
<DragDropGrid
|
||||
items={files}
|
||||
selectedItems={localSelectedIds as any /* FIX ME */}
|
||||
selectionMode={selectionMode}
|
||||
isAnimating={isAnimating}
|
||||
onDragStart={handleDragStart as any /* FIX ME */}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter as any /* FIX ME */}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop as any /* FIX ME */}
|
||||
onEndZoneDragEnter={handleEndZoneDragEnter}
|
||||
draggedItem={draggedFile as any /* FIX ME */}
|
||||
dropTarget={dropTarget as any /* FIX ME */}
|
||||
multiItemDrag={multiFileDrag as any /* FIX ME */}
|
||||
dragPosition={dragPosition}
|
||||
renderItem={(file, index, refs) => (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '1.5rem', padding: '1rem' }}>
|
||||
{files.map((file, index) => (
|
||||
<FileThumbnail
|
||||
key={file.id}
|
||||
file={file}
|
||||
index={index}
|
||||
totalFiles={files.length}
|
||||
@ -778,7 +764,7 @@ const FileEditor = ({
|
||||
draggedFile={draggedFile}
|
||||
dropTarget={dropTarget}
|
||||
isAnimating={isAnimating}
|
||||
fileRefs={refs}
|
||||
fileRefs={fileRefs}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
@ -792,21 +778,8 @@ const FileEditor = ({
|
||||
toolMode={toolMode}
|
||||
isSupported={isFileSupported(file.name)}
|
||||
/>
|
||||
)}
|
||||
renderSplitMarker={(file, index) => (
|
||||
<div
|
||||
style={{
|
||||
width: '2px',
|
||||
height: '24rem',
|
||||
borderLeft: '2px dashed #3b82f6',
|
||||
backgroundColor: 'transparent',
|
||||
marginLeft: '-0.75rem',
|
||||
marginRight: '-0.75rem',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import styles from './PageEditor.module.css';
|
||||
|
||||
interface DragDropItem {
|
||||
@ -12,19 +14,9 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
selectedItems: number[];
|
||||
selectionMode: boolean;
|
||||
isAnimating: boolean;
|
||||
onDragStart: (pageNumber: number) => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOver: (e: React.DragEvent) => void;
|
||||
onDragEnter: (pageNumber: number) => void;
|
||||
onDragLeave: () => void;
|
||||
onDrop: (e: React.DragEvent, targetPageNumber: number | 'end') => void;
|
||||
onEndZoneDragEnter: () => void;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => void;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||
draggedItem: number | null;
|
||||
dropTarget: number | 'end' | null;
|
||||
multiItemDrag: {pageNumbers: number[], count: number} | null;
|
||||
dragPosition: {x: number, y: number} | null;
|
||||
}
|
||||
|
||||
const DragDropGrid = <T extends DragDropItem>({
|
||||
@ -32,217 +24,88 @@ const DragDropGrid = <T extends DragDropItem>({
|
||||
selectedItems,
|
||||
selectionMode,
|
||||
isAnimating,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onEndZoneDragEnter,
|
||||
onReorderPages,
|
||||
renderItem,
|
||||
renderSplitMarker,
|
||||
draggedItem,
|
||||
dropTarget,
|
||||
multiItemDrag,
|
||||
dragPosition,
|
||||
}: DragDropGridProps<T>) => {
|
||||
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
// Virtualization configuration - adjust for document size
|
||||
const isLargeDocument = items.length > 1000; // Only virtualize for very large documents
|
||||
const ITEM_HEIGHT = 340; // Height of PageThumbnail + gap (20rem + gap)
|
||||
const ITEMS_PER_ROW = 4; // Approximate items per row
|
||||
const BUFFER_SIZE = isLargeDocument ? 2 : 3; // Larger buffer for smoother scrolling
|
||||
const OVERSCAN = ITEMS_PER_ROW * BUFFER_SIZE; // Total buffer items
|
||||
|
||||
// Log virtualization stats for debugging
|
||||
React.useEffect(() => {
|
||||
if (items.length > 100) {
|
||||
console.log(`📊 DragDropGrid: Virtualizing ${items.length} items (large doc: ${isLargeDocument}, buffer: ${BUFFER_SIZE})`);
|
||||
}
|
||||
}, [items.length, isLargeDocument, BUFFER_SIZE]);
|
||||
// Grid configuration
|
||||
const ITEMS_PER_ROW = 4;
|
||||
const ITEM_HEIGHT = 340; // 20rem + gap
|
||||
const OVERSCAN = items.length > 1000 ? 8 : 4; // More overscan for large documents
|
||||
|
||||
// Throttled scroll handler to prevent excessive re-renders
|
||||
const throttleRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Detect scroll position from parent container
|
||||
useEffect(() => {
|
||||
const updateScrollPosition = () => {
|
||||
// Throttle scroll updates for better performance
|
||||
if (throttleRef.current) {
|
||||
cancelAnimationFrame(throttleRef.current);
|
||||
}
|
||||
|
||||
throttleRef.current = requestAnimationFrame(() => {
|
||||
const scrollingParent = containerRef.current?.closest('[data-scrolling-container]') ||
|
||||
containerRef.current?.offsetParent?.closest('div[style*="overflow"]');
|
||||
|
||||
if (scrollingParent) {
|
||||
setScrollTop(scrollingParent.scrollTop || 0);
|
||||
}
|
||||
// Virtualization with react-virtual library
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: Math.ceil(items.length / ITEMS_PER_ROW),
|
||||
getScrollElement: () => containerRef.current?.closest('[data-scrolling-container]') as Element,
|
||||
estimateSize: () => ITEM_HEIGHT,
|
||||
overscan: OVERSCAN,
|
||||
});
|
||||
};
|
||||
|
||||
const scrollingParent = containerRef.current?.closest('[data-scrolling-container]') ||
|
||||
containerRef.current?.offsetParent?.closest('div[style*="overflow"]');
|
||||
|
||||
if (scrollingParent) {
|
||||
// Use passive listener for better scrolling performance
|
||||
scrollingParent.addEventListener('scroll', updateScrollPosition, { passive: true });
|
||||
updateScrollPosition(); // Initial position
|
||||
|
||||
return () => {
|
||||
scrollingParent.removeEventListener('scroll', updateScrollPosition);
|
||||
if (throttleRef.current) {
|
||||
cancelAnimationFrame(throttleRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Calculate visible range with virtualization (only for very large documents)
|
||||
const { startIndex, endIndex, totalHeight, topSpacer } = useMemo(() => {
|
||||
// Skip virtualization for smaller documents to avoid jankiness
|
||||
if (!isLargeDocument) {
|
||||
return {
|
||||
startIndex: 0,
|
||||
endIndex: items.length,
|
||||
totalHeight: Math.ceil(items.length / ITEMS_PER_ROW) * ITEM_HEIGHT,
|
||||
topSpacer: 0
|
||||
};
|
||||
}
|
||||
|
||||
const containerHeight = containerRef.current?.clientHeight || 600;
|
||||
const rowHeight = ITEM_HEIGHT;
|
||||
const totalRows = Math.ceil(items.length / ITEMS_PER_ROW);
|
||||
const visibleRows = Math.ceil(containerHeight / rowHeight);
|
||||
|
||||
const startRow = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_SIZE);
|
||||
const endRow = Math.min(totalRows, startRow + visibleRows + BUFFER_SIZE * 2);
|
||||
|
||||
const startIndex = startRow * ITEMS_PER_ROW;
|
||||
const endIndex = Math.min(items.length, endRow * ITEMS_PER_ROW);
|
||||
const totalHeight = totalRows * rowHeight;
|
||||
const topSpacer = startRow * rowHeight;
|
||||
|
||||
return { startIndex, endIndex, totalHeight, topSpacer };
|
||||
}, [items.length, scrollTop, ITEM_HEIGHT, ITEMS_PER_ROW, BUFFER_SIZE, isLargeDocument]);
|
||||
|
||||
// Only render visible items for performance
|
||||
const visibleItems = useMemo(() => {
|
||||
const visible = items.slice(startIndex, endIndex);
|
||||
|
||||
// Debug logging for large documents
|
||||
if (items.length > 500 && visible.length > 0) {
|
||||
console.log(`📊 DragDropGrid: Rendering ${visible.length} items (${startIndex}-${endIndex-1}) of ${items.length} total`);
|
||||
}
|
||||
|
||||
return visible;
|
||||
}, [items, startIndex, endIndex]);
|
||||
|
||||
// Global drag cleanup
|
||||
useEffect(() => {
|
||||
const handleGlobalDragEnd = () => {
|
||||
onDragEnd();
|
||||
};
|
||||
|
||||
const handleGlobalDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if (draggedItem) {
|
||||
document.addEventListener('dragend', handleGlobalDragEnd);
|
||||
document.addEventListener('drop', handleGlobalDrop);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('dragend', handleGlobalDragEnd);
|
||||
document.removeEventListener('drop', handleGlobalDrop);
|
||||
};
|
||||
}, [draggedItem, onDragEnd]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
style={{
|
||||
// Performance optimizations for smooth scrolling
|
||||
transform: 'translateZ(0)', // Force hardware acceleration
|
||||
backfaceVisibility: 'hidden', // Better rendering performance
|
||||
WebkitOverflowScrolling: 'touch', // Smooth scrolling on iOS
|
||||
// Basic container styles
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
height: totalHeight,
|
||||
paddingBottom: '100px'
|
||||
}}
|
||||
>
|
||||
{/* Top spacer for virtualization */}
|
||||
<div style={{ height: topSpacer }} />
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const startIndex = virtualRow.index * ITEMS_PER_ROW;
|
||||
const endIndex = Math.min(startIndex + ITEMS_PER_ROW, items.length);
|
||||
const rowItems = items.slice(startIndex, endIndex);
|
||||
|
||||
{/* Visible items container */}
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '1.5rem',
|
||||
justifyContent: 'flex-start',
|
||||
// Prevent layout shifts during scrolling
|
||||
containIntrinsicSize: '20rem 20rem',
|
||||
contain: 'layout style',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{visibleItems.map((item, visibleIndex) => {
|
||||
const actualIndex = startIndex + visibleIndex;
|
||||
{rowItems.map((item, itemIndex) => {
|
||||
const actualIndex = startIndex + itemIndex;
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
{/* Split marker */}
|
||||
{renderSplitMarker && item.splitBefore && actualIndex > 0 && renderSplitMarker(item, actualIndex)}
|
||||
|
||||
{/* Item */}
|
||||
{renderItem(item, actualIndex, itemRefs)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* End drop zone - inline with pages */}
|
||||
<div className="w-[20rem] h-[20rem] flex items-center justify-center flex-shrink-0">
|
||||
<div
|
||||
data-drop-zone="end"
|
||||
className={`cursor-pointer select-none w-[15rem] h-[15rem] flex items-center justify-center flex-shrink-0 shadow-sm hover:shadow-md transition-all relative ${
|
||||
dropTarget === 'end'
|
||||
? 'ring-2 ring-green-500 bg-green-50'
|
||||
: 'bg-white hover:bg-blue-50 border-2 border-dashed border-gray-300 hover:border-blue-400'
|
||||
}`}
|
||||
style={{ borderRadius: '12px' }}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnter={onEndZoneDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={(e) => onDrop(e, 'end')}
|
||||
>
|
||||
<div className="text-gray-500 text-sm text-center font-medium">
|
||||
Drop here to<br />move to end
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Multi-item drag indicator */}
|
||||
{multiItemDrag && dragPosition && (
|
||||
<div
|
||||
className={styles.multiDragIndicator}
|
||||
style={{
|
||||
left: dragPosition.x,
|
||||
top: dragPosition.y,
|
||||
}}
|
||||
>
|
||||
{multiItemDrag.count} items
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -232,11 +232,6 @@ const PageEditor = ({
|
||||
const [csvInput, setCsvInput] = useState<string>("");
|
||||
const [selectionMode, setSelectionMode] = useState(false);
|
||||
|
||||
// Drag and drop state
|
||||
const [draggedPage, setDraggedPage] = useState<number | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<number | 'end' | null>(null);
|
||||
const [multiPageDrag, setMultiPageDrag] = useState<{pageNumbers: number[], count: number} | null>(null);
|
||||
const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null);
|
||||
|
||||
// Export state
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
@ -287,7 +282,7 @@ const PageEditor = ({
|
||||
|
||||
// Simple cache-first thumbnail generation (no complex detection needed)
|
||||
|
||||
// Simple thumbnail generation - generate pages 2+ that aren't cached
|
||||
// Lazy thumbnail generation - only generate when needed, with intelligent batching
|
||||
const generateMissingThumbnails = useCallback(async () => {
|
||||
if (!mergedPdfDocument || !primaryFileId || activeFileIds.length !== 1) {
|
||||
return;
|
||||
@ -299,7 +294,34 @@ const PageEditor = ({
|
||||
const totalPages = mergedPdfDocument.totalPages;
|
||||
if (totalPages <= 1) return; // Only page 1, nothing to generate
|
||||
|
||||
// Check which pages 2+ need thumbnails (not in cache)
|
||||
// For very large documents (2000+ pages), be much more conservative
|
||||
const isVeryLargeDocument = totalPages > 2000;
|
||||
|
||||
if (isVeryLargeDocument) {
|
||||
console.log(`📸 PageEditor: Very large document (${totalPages} pages) - using minimal thumbnail generation`);
|
||||
// For very large docs, only generate the next visible batch (pages 2-25) to avoid UI blocking
|
||||
const pageNumbersToGenerate = [];
|
||||
for (let pageNum = 2; pageNum <= Math.min(25, totalPages); pageNum++) {
|
||||
const pageId = `${primaryFileId}-page-${pageNum}`;
|
||||
if (!getThumbnailFromCache(pageId)) {
|
||||
pageNumbersToGenerate.push(pageNum);
|
||||
}
|
||||
}
|
||||
|
||||
if (pageNumbersToGenerate.length > 0) {
|
||||
console.log(`📸 PageEditor: Generating initial batch for large doc: pages [${pageNumbersToGenerate.join(', ')}]`);
|
||||
await generateThumbnailBatch(file, primaryFileId, pageNumbersToGenerate);
|
||||
}
|
||||
|
||||
// Schedule remaining thumbnails with delay to avoid blocking
|
||||
setTimeout(() => {
|
||||
generateRemainingThumbnailsLazily(file, primaryFileId, totalPages, 26);
|
||||
}, 2000); // 2 second delay before starting background generation
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// For smaller documents, check which pages 2+ need thumbnails
|
||||
const pageNumbersToGenerate = [];
|
||||
for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
|
||||
const pageId = `${primaryFileId}-page-${pageNum}`;
|
||||
@ -313,19 +335,23 @@ const PageEditor = ({
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📸 PageEditor: Generating thumbnails for pages: [${pageNumbersToGenerate.join(', ')}]`);
|
||||
console.log(`📸 PageEditor: Generating thumbnails for pages: [${pageNumbersToGenerate.slice(0, 5).join(', ')}${pageNumbersToGenerate.length > 5 ? '...' : ''}]`);
|
||||
await generateThumbnailBatch(file, primaryFileId, pageNumbersToGenerate);
|
||||
}, [mergedPdfDocument, primaryFileId, activeFileIds, selectors]);
|
||||
|
||||
// Helper function to generate thumbnails in batches
|
||||
const generateThumbnailBatch = useCallback(async (file: File, fileId: string, pageNumbers: number[]) => {
|
||||
try {
|
||||
// Load PDF array buffer for Web Workers
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Calculate quality scale based on file size
|
||||
const scale = calculateScaleFromFileSize(selectors.getFileRecord(primaryFileId)?.size || 0);
|
||||
const scale = calculateScaleFromFileSize(selectors.getFileRecord(fileId)?.size || 0);
|
||||
|
||||
// Start parallel thumbnail generation WITHOUT blocking the main thread
|
||||
await generateThumbnails(
|
||||
arrayBuffer,
|
||||
pageNumbersToGenerate,
|
||||
pageNumbers,
|
||||
{
|
||||
scale, // Dynamic quality based on file size
|
||||
quality: 0.8,
|
||||
@ -338,7 +364,7 @@ const PageEditor = ({
|
||||
requestAnimationFrame(() => {
|
||||
progress.thumbnails.forEach(({ pageNumber, thumbnail }) => {
|
||||
// Use stable fileId for cache key
|
||||
const pageId = `${primaryFileId}-page-${pageNumber}`;
|
||||
const pageId = `${fileId}-page-${pageNumber}`;
|
||||
addThumbnailToCache(pageId, thumbnail);
|
||||
|
||||
// Don't update context state - thumbnails stay in cache only
|
||||
@ -349,11 +375,40 @@ const PageEditor = ({
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`📸 PageEditor: Thumbnail generation completed for pages [${pageNumbersToGenerate.join(', ')}]`);
|
||||
console.log(`📸 PageEditor: Thumbnail generation completed for ${pageNumbers.length} pages`);
|
||||
} catch (error) {
|
||||
console.error('PageEditor: Thumbnail generation failed:', error);
|
||||
}
|
||||
}, [mergedPdfDocument, primaryFileId, activeFileIds, generateThumbnails, getThumbnailFromCache, addThumbnailToCache, selectors]);
|
||||
}, [generateThumbnails, addThumbnailToCache, selectors]);
|
||||
|
||||
// Background generation for remaining pages in very large documents
|
||||
const generateRemainingThumbnailsLazily = useCallback(async (file: File, fileId: string, totalPages: number, startPage: number) => {
|
||||
console.log(`📸 PageEditor: Starting background thumbnail generation from page ${startPage} to ${totalPages}`);
|
||||
|
||||
// Generate in small chunks to avoid blocking
|
||||
const CHUNK_SIZE = 50;
|
||||
for (let start = startPage; start <= totalPages; start += CHUNK_SIZE) {
|
||||
const end = Math.min(start + CHUNK_SIZE - 1, totalPages);
|
||||
const chunkPageNumbers = [];
|
||||
|
||||
for (let pageNum = start; pageNum <= end; pageNum++) {
|
||||
const pageId = `${fileId}-page-${pageNum}`;
|
||||
if (!getThumbnailFromCache(pageId)) {
|
||||
chunkPageNumbers.push(pageNum);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunkPageNumbers.length > 0) {
|
||||
console.log(`📸 PageEditor: Background generating chunk: pages ${start}-${end} (${chunkPageNumbers.length} needed)`);
|
||||
await generateThumbnailBatch(file, fileId, chunkPageNumbers);
|
||||
|
||||
// Small delay between chunks to keep UI responsive
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📸 PageEditor: Background thumbnail generation completed for ${totalPages} pages`);
|
||||
}, [getThumbnailFromCache, generateThumbnailBatch]);
|
||||
|
||||
// Simple useEffect - just generate missing thumbnails when document is ready
|
||||
useEffect(() => {
|
||||
@ -388,30 +443,6 @@ const PageEditor = ({
|
||||
setCsvInput(newCsvInput);
|
||||
}, [selectedPageNumbers]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalDragEnd = () => {
|
||||
// Clean up drag state when drag operation ends anywhere
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setMultiPageDrag(null);
|
||||
setDragPosition(null);
|
||||
};
|
||||
|
||||
const handleGlobalDrop = (e: DragEvent) => {
|
||||
// Prevent default to handle invalid drops
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if (draggedPage) {
|
||||
document.addEventListener('dragend', handleGlobalDragEnd);
|
||||
document.addEventListener('drop', handleGlobalDrop);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('dragend', handleGlobalDragEnd);
|
||||
document.removeEventListener('drop', handleGlobalDrop);
|
||||
};
|
||||
}, [draggedPage]);
|
||||
|
||||
const selectAll = useCallback(() => {
|
||||
if (mergedPdfDocument) {
|
||||
@ -484,73 +515,8 @@ const PageEditor = ({
|
||||
actions.setSelectedPages(pageNumbers);
|
||||
}, [csvInput, parseCSVInput, actions]);
|
||||
|
||||
const handleDragStart = useCallback((pageNumber: number) => {
|
||||
setDraggedPage(pageNumber);
|
||||
|
||||
// Check if this is a multi-page drag in selection mode
|
||||
if (selectionMode && selectedPageNumbers.includes(pageNumber) && selectedPageNumbers.length > 1) {
|
||||
setMultiPageDrag({
|
||||
pageNumbers: selectedPageNumbers,
|
||||
count: selectedPageNumbers.length
|
||||
});
|
||||
} else {
|
||||
setMultiPageDrag(null);
|
||||
}
|
||||
}, [selectionMode, selectedPageNumbers]);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
// Clean up drag state regardless of where the drop happened
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setMultiPageDrag(null);
|
||||
setDragPosition(null);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!draggedPage) return;
|
||||
|
||||
// Update drag position for multi-page indicator
|
||||
if (multiPageDrag) {
|
||||
setDragPosition({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
|
||||
// Get the element under the mouse cursor
|
||||
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!elementUnderCursor) return;
|
||||
|
||||
// Find the closest page container
|
||||
const pageContainer = elementUnderCursor.closest('[data-page-number]');
|
||||
if (pageContainer) {
|
||||
const pageNumberStr = pageContainer.getAttribute('data-page-number');
|
||||
const pageNumber = pageNumberStr ? parseInt(pageNumberStr) : null;
|
||||
if (pageNumber && pageNumber !== draggedPage) {
|
||||
setDropTarget(pageNumber);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if over the end zone
|
||||
const endZone = elementUnderCursor.closest('[data-drop-zone="end"]');
|
||||
if (endZone) {
|
||||
setDropTarget('end');
|
||||
return;
|
||||
}
|
||||
|
||||
// If not over any valid drop target, clear it
|
||||
setDropTarget(null);
|
||||
}, [draggedPage, multiPageDrag]);
|
||||
|
||||
const handleDragEnter = useCallback((pageNumber: number) => {
|
||||
if (draggedPage && pageNumber !== draggedPage) {
|
||||
setDropTarget(pageNumber);
|
||||
}
|
||||
}, [draggedPage]);
|
||||
|
||||
const handleDragLeave = useCallback(() => {
|
||||
// Don't clear drop target on drag leave - let dragover handle it
|
||||
}, []);
|
||||
|
||||
// Update PDF document state with edit tracking
|
||||
const setPdfDocument = useCallback((updatedDoc: PDFDocument) => {
|
||||
@ -791,34 +757,22 @@ const PageEditor = ({
|
||||
}, 10); // Small delay to allow state update
|
||||
}, [displayDocument, isAnimating, executeCommand, selectionMode, selectedPageNumbers, setPdfDocument]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent, targetPageNumber: number | 'end') => {
|
||||
e.preventDefault();
|
||||
if (!draggedPage || !displayDocument || draggedPage === targetPageNumber) return;
|
||||
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
|
||||
if (!displayDocument) return;
|
||||
|
||||
let targetIndex: number;
|
||||
if (targetPageNumber === 'end') {
|
||||
targetIndex = displayDocument.pages.length;
|
||||
} else {
|
||||
targetIndex = displayDocument.pages.findIndex(p => p.pageNumber === targetPageNumber);
|
||||
if (targetIndex === -1) return;
|
||||
}
|
||||
const pagesToMove = selectedPages && selectedPages.length > 1
|
||||
? selectedPages
|
||||
: [sourcePageNumber];
|
||||
|
||||
animateReorder(draggedPage, targetIndex);
|
||||
const sourceIndex = displayDocument.pages.findIndex(p => p.pageNumber === sourcePageNumber);
|
||||
if (sourceIndex === -1 || sourceIndex === targetIndex) return;
|
||||
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setMultiPageDrag(null);
|
||||
setDragPosition(null);
|
||||
animateReorder(sourcePageNumber, targetIndex);
|
||||
|
||||
const moveCount = multiPageDrag ? multiPageDrag.count : 1;
|
||||
const moveCount = pagesToMove.length;
|
||||
setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`);
|
||||
}, [draggedPage, displayDocument, animateReorder, multiPageDrag]);
|
||||
}, [displayDocument, animateReorder]);
|
||||
|
||||
const handleEndZoneDragEnter = useCallback(() => {
|
||||
if (draggedPage) {
|
||||
setDropTarget('end');
|
||||
}
|
||||
}, [draggedPage]);
|
||||
|
||||
const handleRotate = useCallback((direction: 'left' | 'right') => {
|
||||
if (!displayDocument) return;
|
||||
@ -1058,6 +1012,13 @@ const PageEditor = ({
|
||||
const draftKey = `draft-${mergedPdfDocument.id || 'merged'}`;
|
||||
// Use centralized IndexedDB manager
|
||||
const db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.DRAFTS);
|
||||
|
||||
// Check if the drafts object store exists before using it
|
||||
if (!db.objectStoreNames.contains('drafts')) {
|
||||
console.log('📝 Drafts object store not found, skipping draft check');
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = db.transaction('drafts', 'readonly');
|
||||
const store = transaction.objectStore('drafts');
|
||||
const getRequest = store.get(draftKey);
|
||||
@ -1288,17 +1249,7 @@ const PageEditor = ({
|
||||
selectedItems={selectedPageNumbers}
|
||||
selectionMode={selectionMode}
|
||||
isAnimating={isAnimating}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onEndZoneDragEnter={handleEndZoneDragEnter}
|
||||
draggedItem={draggedPage}
|
||||
dropTarget={dropTarget}
|
||||
multiItemDrag={multiPageDrag}
|
||||
dragPosition={dragPosition}
|
||||
onReorderPages={handleReorderPages}
|
||||
renderItem={(page, index, refs) => (
|
||||
<PageThumbnail
|
||||
page={page}
|
||||
@ -1307,17 +1258,10 @@ const PageEditor = ({
|
||||
originalFile={activeFileIds.length === 1 && primaryFileId ? selectors.getFile(primaryFileId) : undefined}
|
||||
selectedPages={selectedPageNumbers}
|
||||
selectionMode={selectionMode}
|
||||
draggedPage={draggedPage}
|
||||
dropTarget={dropTarget === 'end' ? null : dropTarget}
|
||||
movingPage={movingPage}
|
||||
isAnimating={isAnimating}
|
||||
pageRefs={refs}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onReorderPages={handleReorderPages}
|
||||
onTogglePage={togglePage}
|
||||
onAnimateReorder={animateReorder}
|
||||
onExecuteCommand={executeCommand}
|
||||
|
@ -6,7 +6,7 @@ import RotateLeftIcon from '@mui/icons-material/RotateLeft';
|
||||
import RotateRightIcon from '@mui/icons-material/RotateRight';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
||||
import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../commands/pageCommands';
|
||||
import { Command } from '../../hooks/useUndoRedo';
|
||||
@ -27,22 +27,15 @@ interface PageThumbnailProps {
|
||||
originalFile?: File; // For lazy thumbnail generation
|
||||
selectedPages: number[];
|
||||
selectionMode: boolean;
|
||||
draggedPage: number | null;
|
||||
dropTarget: number | 'end' | null;
|
||||
movingPage: number | null;
|
||||
isAnimating: boolean;
|
||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
onDragStart: (pageNumber: number) => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOver: (e: React.DragEvent) => void;
|
||||
onDragEnter: (pageNumber: number) => void;
|
||||
onDragLeave: () => void;
|
||||
onDrop: (e: React.DragEvent, pageNumber: number) => void;
|
||||
onTogglePage: (pageNumber: number) => void;
|
||||
onAnimateReorder: (pageNumber: number, targetIndex: number) => void;
|
||||
onExecuteCommand: (command: Command) => void;
|
||||
onSetStatus: (status: string) => void;
|
||||
onSetMovingPage: (pageNumber: number | null) => void;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => void;
|
||||
RotatePagesCommand: typeof RotatePagesCommand;
|
||||
DeletePagesCommand: typeof DeletePagesCommand;
|
||||
ToggleSplitCommand: typeof ToggleSplitCommand;
|
||||
@ -57,22 +50,15 @@ const PageThumbnail = React.memo(({
|
||||
originalFile,
|
||||
selectedPages,
|
||||
selectionMode,
|
||||
draggedPage,
|
||||
dropTarget,
|
||||
movingPage,
|
||||
isAnimating,
|
||||
pageRefs,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onTogglePage,
|
||||
onAnimateReorder,
|
||||
onExecuteCommand,
|
||||
onSetStatus,
|
||||
onSetMovingPage,
|
||||
onReorderPages,
|
||||
RotatePagesCommand,
|
||||
DeletePagesCommand,
|
||||
ToggleSplitCommand,
|
||||
@ -80,6 +66,8 @@ const PageThumbnail = React.memo(({
|
||||
setPdfDocument,
|
||||
}: PageThumbnailProps) => {
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
||||
const { state, selectors } = useFileState();
|
||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||
|
||||
@ -130,14 +118,69 @@ const PageThumbnail = React.memo(({
|
||||
}, [page.id, originalFile, requestThumbnail, getThumbnailFromCache]); // Removed thumbnailUrl to prevent loops
|
||||
|
||||
|
||||
// Register this component with pageRefs for animations
|
||||
const pageElementRef = useCallback((element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
pageRefs.current.set(page.id, element);
|
||||
dragElementRef.current = element;
|
||||
|
||||
const dragCleanup = draggable({
|
||||
element,
|
||||
getInitialData: () => ({
|
||||
pageNumber: page.pageNumber,
|
||||
pageId: page.id,
|
||||
selectedPages: selectionMode && selectedPages.includes(page.pageNumber)
|
||||
? selectedPages
|
||||
: [page.pageNumber]
|
||||
}),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: ({ location }) => {
|
||||
setIsDragging(false);
|
||||
|
||||
if (location.current.dropTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropTarget = location.current.dropTargets[0];
|
||||
const targetData = dropTarget.data;
|
||||
|
||||
if (targetData.type === 'page') {
|
||||
const targetPageNumber = targetData.pageNumber as number;
|
||||
const targetIndex = pdfDocument.pages.findIndex(p => p.pageNumber === targetPageNumber);
|
||||
if (targetIndex !== -1) {
|
||||
const pagesToMove = selectionMode && selectedPages.includes(page.pageNumber)
|
||||
? selectedPages
|
||||
: undefined;
|
||||
onReorderPages(page.pageNumber, targetIndex, pagesToMove);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
element.style.cursor = 'grab';
|
||||
|
||||
|
||||
const dropCleanup = dropTargetForElements({
|
||||
element,
|
||||
getData: () => ({
|
||||
type: 'page',
|
||||
pageNumber: page.pageNumber
|
||||
}),
|
||||
onDrop: ({ source }) => {}
|
||||
});
|
||||
|
||||
(element as any).__dragCleanup = () => {
|
||||
dragCleanup();
|
||||
dropCleanup();
|
||||
};
|
||||
} else {
|
||||
pageRefs.current.delete(page.id);
|
||||
if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) {
|
||||
(dragElementRef.current as any).__dragCleanup();
|
||||
}
|
||||
}, [page.id, pageRefs]);
|
||||
}
|
||||
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPages, pdfDocument.pages, onReorderPages]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -159,25 +202,13 @@ const PageThumbnail = React.memo(({
|
||||
${selectionMode
|
||||
? 'bg-white hover:bg-gray-50'
|
||||
: 'bg-white hover:bg-gray-50'}
|
||||
${draggedPage === page.pageNumber ? 'opacity-50 scale-95' : ''}
|
||||
${isDragging ? 'opacity-50 scale-95' : ''}
|
||||
${movingPage === page.pageNumber ? 'page-moving' : ''}
|
||||
`}
|
||||
style={{
|
||||
transform: (() => {
|
||||
if (!isAnimating && draggedPage && page.pageNumber !== draggedPage && dropTarget === page.pageNumber) {
|
||||
return 'translateX(20px)';
|
||||
}
|
||||
return 'translateX(0)';
|
||||
})(),
|
||||
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
|
||||
}}
|
||||
draggable
|
||||
onDragStart={() => onDragStart(page.pageNumber)}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnter={() => onDragEnter(page.pageNumber)}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={(e) => onDrop(e, page.pageNumber)}
|
||||
draggable={false}
|
||||
>
|
||||
{selectionMode && (
|
||||
<div
|
||||
@ -201,7 +232,6 @@ const PageThumbnail = React.memo(({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
console.log('📸 Checkbox clicked for page', page.pageNumber);
|
||||
e.stopPropagation();
|
||||
onTogglePage(page.pageNumber);
|
||||
}}
|
||||
@ -216,7 +246,7 @@ const PageThumbnail = React.memo(({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="page-container w-[90%] h-[90%]">
|
||||
<div className="page-container w-[90%] h-[90%]" draggable={false}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
@ -234,6 +264,7 @@ const PageThumbnail = React.memo(({
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={`Page ${page.pageNumber}`}
|
||||
draggable={false}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@ -415,16 +446,6 @@ const PageThumbnail = React.memo(({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DragIndicatorIcon
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
color: 'rgba(0,0,0,0.3)',
|
||||
fontSize: 16,
|
||||
zIndex: 1
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -444,8 +465,6 @@ const PageThumbnail = React.memo(({
|
||||
(prevProps.selectedPages === nextProps.selectedPages ||
|
||||
arraysEqual(prevProps.selectedPages, nextProps.selectedPages)) &&
|
||||
prevProps.selectionMode === nextProps.selectionMode &&
|
||||
prevProps.draggedPage === nextProps.draggedPage &&
|
||||
prevProps.dropTarget === nextProps.dropTarget &&
|
||||
prevProps.movingPage === nextProps.movingPage &&
|
||||
prevProps.isAnimating === nextProps.isAnimating
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user