Roadmap, robust page refs, copy behaviour

This commit is contained in:
2026-05-16 02:37:20 +02:00
parent a649ede010
commit 2461cf3d64
9 changed files with 1588 additions and 744 deletions

View File

@@ -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;