merge: fix/42-reader-vertical-deep-pages dans develop (préchargeur CBZ vertical concurrent pour les pages profondes — 3e correctif #42)
This commit is contained in:
@ -11,6 +11,7 @@ const VERTICAL_ANCHOR_TOLERANCE_PX = 24;
|
|||||||
const VERTICAL_ANCHOR_STABLE_FRAMES = 18;
|
const VERTICAL_ANCHOR_STABLE_FRAMES = 18;
|
||||||
const VERTICAL_ANCHOR_MAX_ATTEMPTS = 180;
|
const VERTICAL_ANCHOR_MAX_ATTEMPTS = 180;
|
||||||
const VERTICAL_INITIAL_SYNC_MAX_ATTEMPTS = 180;
|
const VERTICAL_INITIAL_SYNC_MAX_ATTEMPTS = 180;
|
||||||
|
const VERTICAL_IMAGE_PRELOAD_CONCURRENCY = 24;
|
||||||
|
|
||||||
export function CbzReader({
|
export function CbzReader({
|
||||||
bookId,
|
bookId,
|
||||||
@ -39,6 +40,9 @@ export function CbzReader({
|
|||||||
const verticalInitialSyncDoneRef = useRef(false);
|
const verticalInitialSyncDoneRef = useRef(false);
|
||||||
const verticalAnchorAttemptRef = useRef(0);
|
const verticalAnchorAttemptRef = useRef(0);
|
||||||
const verticalAnchorStableFramesRef = useRef(0);
|
const verticalAnchorStableFramesRef = useRef(0);
|
||||||
|
const preloadedImagesRef = useRef(new Map<number, HTMLImageElement>());
|
||||||
|
const pendingImageSizesRef = useRef<Record<number, ReaderSize>>({});
|
||||||
|
const imageSizeFlushFrameRef = useRef<number | null>(null);
|
||||||
const [pages, setPages] = useState<CbzPagesDto | null>(null);
|
const [pages, setPages] = useState<CbzPagesDto | null>(null);
|
||||||
const [documentError, setDocumentError] = useState<string>();
|
const [documentError, setDocumentError] = useState<string>();
|
||||||
const [pageError, setPageError] = useState<string>();
|
const [pageError, setPageError] = useState<string>();
|
||||||
@ -91,6 +95,27 @@ export function CbzReader({
|
|||||||
if (verticalInitialSyncFrameRef.current !== null) cancelAnimationFrame(verticalInitialSyncFrameRef.current);
|
if (verticalInitialSyncFrameRef.current !== null) cancelAnimationFrame(verticalInitialSyncFrameRef.current);
|
||||||
verticalInitialSyncFrameRef.current = null;
|
verticalInitialSyncFrameRef.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
const queueImageSize = useCallback((pageNumber: number, size: ReaderSize) => {
|
||||||
|
pendingImageSizesRef.current[pageNumber] = size;
|
||||||
|
if (imageSizeFlushFrameRef.current !== null) return;
|
||||||
|
imageSizeFlushFrameRef.current = requestAnimationFrame(() => {
|
||||||
|
imageSizeFlushFrameRef.current = null;
|
||||||
|
const pending = pendingImageSizesRef.current;
|
||||||
|
pendingImageSizesRef.current = {};
|
||||||
|
setImageSizes((current) => {
|
||||||
|
let changed = false;
|
||||||
|
const next = { ...current };
|
||||||
|
for (const [pageKey, nextSize] of Object.entries(pending)) {
|
||||||
|
const pageNumber = Number(pageKey);
|
||||||
|
const currentSize = current[pageNumber];
|
||||||
|
if (currentSize?.width === nextSize.width && currentSize.height === nextSize.height) continue;
|
||||||
|
next[pageNumber] = nextSize;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed ? next : current;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
const commitVisiblePage = useCallback(
|
const commitVisiblePage = useCallback(
|
||||||
(stage: HTMLElement, allowInitialCommit = false) => {
|
(stage: HTMLElement, allowInitialCommit = false) => {
|
||||||
if (!pages || !verticalTrackingReadyRef.current) return;
|
if (!pages || !verticalTrackingReadyRef.current) return;
|
||||||
@ -192,8 +217,49 @@ export function CbzReader({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImageSizes({});
|
setImageSizes({});
|
||||||
|
pendingImageSizesRef.current = {};
|
||||||
|
preloadedImagesRef.current.clear();
|
||||||
}, [bookId, retryAttempt]);
|
}, [bookId, retryAttempt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode !== "vertical" || !pages || typeof Image === "undefined") return;
|
||||||
|
let cancelled = false;
|
||||||
|
let nextIndex = 0;
|
||||||
|
let active = 0;
|
||||||
|
const pageItems = pages.pages;
|
||||||
|
|
||||||
|
const preloadNext = () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
while (active < VERTICAL_IMAGE_PRELOAD_CONCURRENCY && nextIndex < pageItems.length) {
|
||||||
|
const item = pageItems[nextIndex];
|
||||||
|
nextIndex += 1;
|
||||||
|
if (preloadedImagesRef.current.has(item.page)) continue;
|
||||||
|
const image = new Image();
|
||||||
|
preloadedImagesRef.current.set(item.page, image);
|
||||||
|
active += 1;
|
||||||
|
image.decoding = "async";
|
||||||
|
image.loading = "eager";
|
||||||
|
image.onload = () => {
|
||||||
|
active -= 1;
|
||||||
|
if (!cancelled && image.naturalWidth > 0 && image.naturalHeight > 0) {
|
||||||
|
queueImageSize(item.page, { width: image.naturalWidth, height: image.naturalHeight });
|
||||||
|
}
|
||||||
|
preloadNext();
|
||||||
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
active -= 1;
|
||||||
|
preloadNext();
|
||||||
|
};
|
||||||
|
image.src = api.cbzPageUrl(bookId, item.page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
preloadNext();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [bookId, mode, pages, queueImageSize, retryAttempt]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const frame = frameRef.current;
|
const frame = frameRef.current;
|
||||||
const stage = frame?.closest(".reader-stage") as HTMLElement | null;
|
const stage = frame?.closest(".reader-stage") as HTMLElement | null;
|
||||||
@ -275,6 +341,7 @@ export function CbzReader({
|
|||||||
commitVisiblePage(stage);
|
commitVisiblePage(stage);
|
||||||
};
|
};
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
|
if (verticalTrackingReadyRef.current) verticalUserScrollRef.current = true;
|
||||||
if (frameId) return;
|
if (frameId) return;
|
||||||
frameId = requestAnimationFrame(updateVisiblePage);
|
frameId = requestAnimationFrame(updateVisiblePage);
|
||||||
};
|
};
|
||||||
@ -297,6 +364,12 @@ export function CbzReader({
|
|||||||
|
|
||||||
useEffect(() => () => clearVerticalAnchorFrame(), [clearVerticalAnchorFrame]);
|
useEffect(() => () => clearVerticalAnchorFrame(), [clearVerticalAnchorFrame]);
|
||||||
useEffect(() => () => clearVerticalInitialSync(), [clearVerticalInitialSync]);
|
useEffect(() => () => clearVerticalInitialSync(), [clearVerticalInitialSync]);
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (imageSizeFlushFrameRef.current !== null) cancelAnimationFrame(imageSizeFlushFrameRef.current);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
if (documentError) {
|
if (documentError) {
|
||||||
return (
|
return (
|
||||||
@ -352,10 +425,7 @@ export function CbzReader({
|
|||||||
style={verticalImageStyle(item.page)}
|
style={verticalImageStyle(item.page)}
|
||||||
onLoad={(event) => {
|
onLoad={(event) => {
|
||||||
const { naturalWidth, naturalHeight } = event.currentTarget;
|
const { naturalWidth, naturalHeight } = event.currentTarget;
|
||||||
setImageSizes((current) => ({
|
queueImageSize(item.page, { width: naturalWidth, height: naturalHeight });
|
||||||
...current,
|
|
||||||
[item.page]: { width: naturalWidth, height: naturalHeight }
|
|
||||||
}));
|
|
||||||
}}
|
}}
|
||||||
onError={() => setPageError(`Page ${item.page} indisponible.`)}
|
onError={() => setPageError(`Page ${item.page} indisponible.`)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -222,6 +222,16 @@ describe("reader runtime helpers", () => {
|
|||||||
expect(verticalBranch).not.toContain('hidden={!imageSizes[item.page]}');
|
expect(verticalBranch).not.toContain('hidden={!imageSizes[item.page]}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preloads deep CBZ vertical pages without relying only on browser lazy loading", () => {
|
||||||
|
const source = readFileSync(new URL("./CbzReader.tsx", import.meta.url), "utf8");
|
||||||
|
|
||||||
|
expect(source).toContain("VERTICAL_IMAGE_PRELOAD_CONCURRENCY");
|
||||||
|
expect(source).toContain("new Image()");
|
||||||
|
expect(source).toContain('image.loading = "eager"');
|
||||||
|
expect(source).toContain("queueImageSize");
|
||||||
|
expect(source).toContain("if (verticalTrackingReadyRef.current) verticalUserScrollRef.current = true");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps unloaded PDF and CBZ vertical pages visually covered by a page-sized fallback", () => {
|
it("keeps unloaded PDF and CBZ vertical pages visually covered by a page-sized fallback", () => {
|
||||||
const styles = readFileSync(new URL("../styles/app.css", import.meta.url), "utf8");
|
const styles = readFileSync(new URL("../styles/app.css", import.meta.url), "utf8");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user