This commit is contained in:
2025-11-17 00:43:43 +01:00
parent 5ffbcc47e3
commit c56058b939
2 changed files with 121 additions and 43 deletions
+2 -6
View File
@@ -37,16 +37,12 @@ const AppLayout: React.FC = () => {
});
if (['logged-out', 'authenticating', 'selecting-tenant'].includes(appStatus)) {
const shouldRememberLastLocation = appStatus !== 'logged-out';
const redirectTarget = `${location.pathname}${location.search}${location.hash || ''}`;
return (
<Navigate
to="/account/login"
replace
state={
shouldRememberLastLocation
? { from: location.pathname + location.search }
: undefined
}
state={{ from: redirectTarget }}
/>
);
}
+119 -37
View File
@@ -39,6 +39,12 @@ interface PageDescriptor {
scale: number;
}
interface RenderQueueRequest {
pageNumber: number;
resume: () => void;
cancel: () => void;
}
const RERENDER_DELTA = 48;
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
@@ -85,6 +91,50 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const wasmUrlRef = useRef<string | null>(null);
const pageNodeMapRef = useRef<Map<number, HTMLElement>>(new Map());
const intersectionObserverRef = useRef<IntersectionObserver | null>(null);
const renderQueueRef = useRef<RenderQueueRequest[]>([]);
const activeRendersRef = useRef(new Set<number>());
const maxConcurrentRendersRef = useRef(2);
const requestQueueFlush = useCallback(() => {
const queue = renderQueueRef.current;
while (queue.length > 0 && activeRendersRef.current.size < maxConcurrentRendersRef.current) {
const next = queue.pop();
if (!next) {
break;
}
if (activeRendersRef.current.has(next.pageNumber)) {
next.cancel();
continue;
}
activeRendersRef.current.add(next.pageNumber);
next.resume();
}
}, []);
const enqueueRender = useCallback((request: RenderQueueRequest) => {
renderQueueRef.current.push(request);
requestQueueFlush();
}, [requestQueueFlush]);
const releaseRenderSlot = useCallback((pageNumber: number) => {
if (activeRendersRef.current.delete(pageNumber)) {
requestQueueFlush();
}
}, [requestQueueFlush]);
const cancelRenderRequest = useCallback((pageNumber: number) => {
const queue = renderQueueRef.current;
const index = queue.findIndex((entry) => entry.pageNumber === pageNumber);
if (index >= 0) {
const [entry] = queue.splice(index, 1);
entry.cancel();
}
releaseRenderSlot(pageNumber);
}, [releaseRenderSlot]);
const handleRenderFinished = useCallback((pageNumber: number) => {
releaseRenderSlot(pageNumber);
}, [releaseRenderSlot]);
const focusTargetRef = useRef<{
ratioX: number;
ratioY: number;
@@ -428,11 +478,14 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
viewMode={viewMode}
shouldRender={shouldRenderPage(page.number)}
registerPageNode={registerPageNode}
enqueueRender={enqueueRender}
cancelRenderRequest={cancelRenderRequest}
onRenderFinished={handleRenderFinished}
/>
</Fragment>
))
), [handlePageClick, pages, pixelRatio, registerPageNode, shouldRenderPage,
viewportHeight, viewportWidth, viewMode]);
), [cancelRenderRequest, enqueueRender, handlePageClick, handleRenderFinished,
pages, pixelRatio, registerPageNode, shouldRenderPage, viewportHeight, viewportWidth, viewMode]);
return (
<div className={viewerClasses}>
@@ -466,6 +519,9 @@ interface PdfPageCanvasProps {
viewMode: ViewMode;
shouldRender: boolean;
registerPageNode: (pageNumber: number, node: HTMLElement | null) => void;
enqueueRender: (request: RenderQueueRequest) => void;
cancelRenderRequest: (pageNumber: number) => void;
onRenderFinished: (pageNumber: number) => void;
}
function PdfPageCanvas({
@@ -478,6 +534,9 @@ function PdfPageCanvas({
viewMode,
shouldRender,
registerPageNode,
enqueueRender,
cancelRenderRequest,
onRenderFinished,
}: PdfPageCanvasProps): JSX.Element {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -488,6 +547,18 @@ function PdfPageCanvas({
registerPageNode(descriptor.number, node);
}, [descriptor.number, registerPageNode]);
const [canvasMounted, setCanvasMounted] = useState(false);
const [allowRender, setAllowRender] = useState(false);
const resetCanvas = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
canvas.width = 0;
canvas.height = 0;
const context = canvas.getContext('2d');
context?.clearRect(0, 0, context.canvas.width || 0, context.canvas.height || 0);
}, []);
const renderPage = useCallback(async () => {
if (renderTaskRef.current || hasRenderedRef.current) {
@@ -496,51 +567,31 @@ function PdfPageCanvas({
const pdf = pdfRef.current;
const canvas = canvasRef.current;
if (!pdf || !canvas) {
onRenderFinished(descriptor.number);
return;
}
try {
const page = await pdf.getPage(descriptor.number);
const viewport = page.getViewport({ scale: descriptor.scale });
const qualityScale = 1;
const supportsOffscreen = typeof OffscreenCanvas !== 'undefined';
let renderContext: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null = null;
let offscreenCanvas: OffscreenCanvas | null = null;
if (supportsOffscreen) {
offscreenCanvas = new OffscreenCanvas(
viewport.width * pixelRatio * qualityScale,
viewport.height * pixelRatio * qualityScale,
);
renderContext = offscreenCanvas.getContext('2d');
} else {
renderContext = canvas.getContext('2d');
if (!renderContext) {
return;
}
canvas.width = viewport.width * pixelRatio;
canvas.height = viewport.height * pixelRatio;
const renderContext = canvas.getContext('2d');
if (!renderContext) {
return;
}
renderContext?.setTransform(pixelRatio * qualityScale, 0, 0, pixelRatio * qualityScale, 0, 0);
canvas.width = viewport.width * pixelRatio * qualityScale;
canvas.height = viewport.height * pixelRatio * qualityScale;
renderContext.setTransform(pixelRatio * qualityScale, 0, 0, pixelRatio * qualityScale, 0, 0);
const renderTask = page.render({
canvasContext: renderContext as CanvasRenderingContext2D,
canvasContext: renderContext,
viewport,
canvas: supportsOffscreen ? undefined : canvas,
canvas,
});
renderTaskRef.current = renderTask;
await renderTask.promise;
hasRenderedRef.current = true;
if (offscreenCanvas) {
const bitmap = offscreenCanvas.transferToImageBitmap();
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const destContext = canvas.getContext('2d');
destContext?.drawImage(bitmap, 0, 0);
bitmap.close();
}
page.cleanup();
const wrapper = wrapperRef.current;
@@ -554,8 +605,9 @@ function PdfPageCanvas({
console.error('Failed to render PDF page', descriptor.number, error);
} finally {
renderTaskRef.current = null;
onRenderFinished(descriptor.number);
}
}, [descriptor.number, descriptor.scale, pdfRef, pixelRatio]);
}, [descriptor.number, descriptor.scale, onRenderFinished, pdfRef, pixelRatio]);
useEffect(() => {
if (!canvasMounted) {
@@ -579,10 +631,39 @@ function PdfPageCanvas({
setCanvasMounted(false);
hasRenderedRef.current = false;
renderTaskRef.current?.cancel();
}, [descriptor.number]);
resetCanvas();
}, [descriptor.number, resetCanvas]);
useEffect(() => {
if (shouldRender) {
if (!shouldRender) {
setAllowRender(false);
cancelRenderRequest(descriptor.number);
return;
}
let cancelled = false;
const request: RenderQueueRequest = {
pageNumber: descriptor.number,
resume: () => {
if (cancelled) {
return;
}
setAllowRender(true);
},
cancel: () => {
cancelled = true;
setAllowRender(false);
},
};
enqueueRender(request);
return () => {
cancelled = true;
setAllowRender(false);
cancelRenderRequest(descriptor.number);
};
}, [cancelRenderRequest, descriptor.number, enqueueRender, shouldRender]);
useEffect(() => {
if (allowRender) {
setCanvasMounted((prev) => (prev ? prev : true));
return;
}
@@ -593,16 +674,17 @@ function PdfPageCanvas({
hasRenderedRef.current = false;
renderTaskRef.current?.cancel();
wrapperRef.current?.querySelector('.textLayer')?.remove();
resetCanvas();
return false;
});
}, [shouldRender]);
}, [allowRender, resetCanvas]);
useEffect(() => {
if (!canvasMounted) {
if (!canvasMounted || !allowRender) {
return;
}
void renderPage();
}, [canvasMounted, renderPage]);
}, [allowRender, canvasMounted, renderPage]);
const limitAxis: 'width' | 'height' = useMemo(() => {
if (viewMode === 'fit-width') {