Roadmap, robust page refs, copy behaviour
This commit is contained in:
@@ -10,8 +10,17 @@ import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url';
|
||||
interface PagePreviewModalProps {
|
||||
isOpen: boolean;
|
||||
pdf: PdfFile | null;
|
||||
pageIndex: number | null; // original page index (0-based)
|
||||
pageIndex: number | null; // original page index, 0-based
|
||||
rotation: number; // degrees
|
||||
|
||||
visualIndex: number | null; // current position in order, 0-based
|
||||
totalPages: number;
|
||||
|
||||
canGoPrevious: boolean;
|
||||
canGoNext: boolean;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -20,10 +29,45 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
pdf,
|
||||
pageIndex,
|
||||
rotation,
|
||||
visualIndex,
|
||||
totalPages,
|
||||
canGoPrevious,
|
||||
canGoNext,
|
||||
onPrevious,
|
||||
onNext,
|
||||
onClose,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowLeft' && canGoPrevious) {
|
||||
e.preventDefault();
|
||||
onPrevious();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowRight' && canGoNext) {
|
||||
e.preventDefault();
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, canGoPrevious, canGoNext, onPrevious, onNext, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !pdf || pageIndex == null) return;
|
||||
|
||||
@@ -31,6 +75,14 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// copy data for pdf.js (avoid detaching original ArrayBuffer)
|
||||
const src = new Uint8Array(pdf.arrayBuffer);
|
||||
const copy = new Uint8Array(src.byteLength);
|
||||
@@ -44,16 +96,23 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
if (cancelled) return;
|
||||
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
|
||||
const maxWidth = Math.min(window.innerWidth * 0.9, 800);
|
||||
const scale = maxWidth / viewport.width;
|
||||
const maxHeight = window.innerHeight * 0.75;
|
||||
|
||||
const scale = Math.min(
|
||||
maxWidth / viewport.width,
|
||||
maxHeight / viewport.height
|
||||
);
|
||||
|
||||
const scaledViewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
const visibleCanvas = canvasRef.current;
|
||||
if (!visibleCanvas) return;
|
||||
|
||||
const visibleCtx = visibleCanvas.getContext('2d');
|
||||
if (!visibleCtx) return;
|
||||
|
||||
// base size
|
||||
let canvasWidth = scaledViewport.width;
|
||||
let canvasHeight = scaledViewport.height;
|
||||
|
||||
@@ -64,10 +123,9 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
canvasHeight = scaledViewport.width;
|
||||
}
|
||||
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
visibleCanvas.width = canvasWidth;
|
||||
visibleCanvas.height = canvasHeight;
|
||||
|
||||
// render into an offscreen canvas first
|
||||
const baseCanvas = document.createElement('canvas');
|
||||
const baseCtx = baseCanvas.getContext('2d');
|
||||
if (!baseCtx) return;
|
||||
@@ -79,31 +137,29 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
canvasContext: baseCtx,
|
||||
viewport: scaledViewport,
|
||||
});
|
||||
|
||||
await renderTask.promise;
|
||||
if (cancelled) return;
|
||||
|
||||
// draw rotated onto visible canvas
|
||||
ctx.save();
|
||||
visibleCtx.save();
|
||||
|
||||
switch (angle) {
|
||||
case 90:
|
||||
ctx.translate(canvasWidth, 0);
|
||||
ctx.rotate((angle * Math.PI) / 180);
|
||||
visibleCtx.translate(canvasWidth, 0);
|
||||
visibleCtx.rotate((angle * Math.PI) / 180);
|
||||
break;
|
||||
case 180:
|
||||
ctx.translate(canvasWidth, canvasHeight);
|
||||
ctx.rotate((angle * Math.PI) / 180);
|
||||
visibleCtx.translate(canvasWidth, canvasHeight);
|
||||
visibleCtx.rotate((angle * Math.PI) / 180);
|
||||
break;
|
||||
case 270:
|
||||
ctx.translate(0, canvasHeight);
|
||||
ctx.rotate((angle * Math.PI) / 180);
|
||||
break;
|
||||
default:
|
||||
visibleCtx.translate(0, canvasHeight);
|
||||
visibleCtx.rotate((angle * Math.PI) / 180);
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.drawImage(baseCanvas, 0, 0);
|
||||
ctx.restore();
|
||||
visibleCtx.drawImage(baseCanvas, 0, 0);
|
||||
visibleCtx.restore();
|
||||
} catch (e) {
|
||||
console.error('Error rendering preview', e);
|
||||
}
|
||||
@@ -116,6 +172,11 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
|
||||
if (!isOpen || !pdf || pageIndex == null) return null;
|
||||
|
||||
const positionLabel =
|
||||
visualIndex != null && visualIndex >= 0
|
||||
? `${visualIndex + 1} / ${totalPages}`
|
||||
: `Page ${pageIndex + 1}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
@@ -133,6 +194,7 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: '#111827',
|
||||
borderRadius: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
@@ -142,25 +204,107 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<div style={{ alignSelf: 'flex-end' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '999px',
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontSize: '0.8rem',
|
||||
background: '#374151',
|
||||
color: '#e5e7eb',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
✕ Close
|
||||
</button>
|
||||
</div>
|
||||
{/* Previous page */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPrevious();
|
||||
}}
|
||||
disabled={!canGoPrevious}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '2.5rem',
|
||||
height: '2.5rem',
|
||||
borderRadius: '999px',
|
||||
border: 'none',
|
||||
background: canGoPrevious ? '#374151' : '#1f2937',
|
||||
color: canGoPrevious ? '#e5e7eb' : '#6b7280',
|
||||
cursor: canGoPrevious ? 'pointer' : 'default',
|
||||
fontSize: '1.35rem',
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2,
|
||||
}}
|
||||
title="Previous page (←)"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
◀
|
||||
</button>
|
||||
|
||||
{/* Next page */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNext();
|
||||
}}
|
||||
disabled={!canGoNext}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: '50%',
|
||||
transform: 'translate(50%, -50%)',
|
||||
width: '2.5rem',
|
||||
height: '2.5rem',
|
||||
borderRadius: '999px',
|
||||
border: 'none',
|
||||
background: canGoNext ? '#374151' : '#1f2937',
|
||||
color: canGoNext ? '#e5e7eb' : '#6b7280',
|
||||
cursor: canGoNext ? 'pointer' : 'default',
|
||||
fontSize: '1.35rem',
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2,
|
||||
}}
|
||||
title="Next page (→)"
|
||||
aria-label="Next page"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
|
||||
{/* Close */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
transform: 'translate(50%, -50%)',
|
||||
width: '2.25rem',
|
||||
height: '2.25rem',
|
||||
borderRadius: '999px',
|
||||
border: 'none',
|
||||
background: '#374151',
|
||||
color: '#e5e7eb',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.2rem',
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 3,
|
||||
}}
|
||||
title="Close preview (Esc)"
|
||||
aria-label="Close preview"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
@@ -170,12 +314,13 @@ const PagePreviewModal: React.FC<PagePreviewModalProps> = ({
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ color: '#e5e7eb', fontSize: '0.85rem' }}>
|
||||
Page {pageIndex + 1} · Rot {rotation}°
|
||||
{positionLabel} · Original page {pageIndex + 1} · Rot {rotation}°
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PagePreviewModal;
|
||||
export default PagePreviewModal;
|
||||
Reference in New Issue
Block a user