2025-02-17 20:19:28 +01:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
import { RefObject, useEffect, useLayoutEffect, useRef } from 'react';
|
2025-04-05 12:47:44 +02:00
|
|
|
import { ReaderPageScaleMode, ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
2025-03-24 23:24:36 +01:00
|
|
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types';
|
2025-04-28 02:21:23 +02:00
|
|
|
import {
|
2025-05-02 14:52:25 +02:00
|
|
|
isContinuousReadingMode,
|
2025-04-28 02:21:23 +02:00
|
|
|
isContinuousVerticalReadingMode,
|
|
|
|
|
isHeightPageScaleMode,
|
|
|
|
|
isReaderWidthEditable,
|
|
|
|
|
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
2025-02-17 20:19:28 +01:00
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
const shouldPreserveOnResizeChange = (
|
2025-04-28 02:21:23 +02:00
|
|
|
readingMode: ReadingMode,
|
2025-04-27 13:49:24 +02:00
|
|
|
pageScaleMode: ReaderPageScaleMode,
|
2025-04-28 02:21:23 +02:00
|
|
|
previousWidth: number,
|
|
|
|
|
previousHeight: number,
|
|
|
|
|
): boolean => {
|
|
|
|
|
const isEditableReaderWidth = isReaderWidthEditable(pageScaleMode);
|
|
|
|
|
const isHeightPageScaleModeActive = isHeightPageScaleMode(pageScaleMode);
|
|
|
|
|
|
|
|
|
|
const didWidthChange = previousWidth !== window.innerWidth;
|
|
|
|
|
const didHeightChange = previousHeight !== window.innerHeight;
|
|
|
|
|
|
|
|
|
|
const handleWidthChange = isEditableReaderWidth && didWidthChange;
|
|
|
|
|
const handleHeightChange = isHeightPageScaleModeActive && didHeightChange;
|
|
|
|
|
|
|
|
|
|
if (!isContinuousVerticalReadingMode(readingMode) && didWidthChange) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return handleWidthChange || handleHeightChange;
|
|
|
|
|
};
|
2025-04-27 13:49:24 +02:00
|
|
|
|
|
|
|
|
const usePreserveOnValueChange = (
|
|
|
|
|
value: unknown,
|
|
|
|
|
currentPageIndex: number,
|
2025-03-24 23:24:36 +01:00
|
|
|
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
2025-04-27 13:49:24 +02:00
|
|
|
) => {
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
setPageToScrollToIndex(currentPageIndex);
|
|
|
|
|
}, [value]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const usePreserveOnWindowResize = (
|
2025-04-28 02:21:23 +02:00
|
|
|
readingMode: ReadingMode,
|
2025-03-27 13:08:58 +01:00
|
|
|
pageScaleMode: ReaderPageScaleMode,
|
2025-04-27 13:49:24 +02:00
|
|
|
setPageToScrollToIndex: React.Dispatch<React.SetStateAction<number | null>>,
|
|
|
|
|
pageIndex: number,
|
|
|
|
|
) => {
|
2025-04-28 02:21:23 +02:00
|
|
|
const previousDimensionsRef = useRef({ width: window.innerWidth, height: window.innerHeight });
|
|
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleResize = () => {
|
2025-04-28 02:21:23 +02:00
|
|
|
const { width, height } = previousDimensionsRef.current;
|
|
|
|
|
previousDimensionsRef.current = { width: window.innerWidth, height: window.innerHeight };
|
|
|
|
|
|
|
|
|
|
if (!shouldPreserveOnResizeChange(readingMode, pageScaleMode, width, height)) {
|
2025-04-27 13:49:24 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPageToScrollToIndex(pageIndex);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
return () => window.removeEventListener('resize', handleResize);
|
2025-04-28 02:21:23 +02:00
|
|
|
}, [readingMode, pageScaleMode, pageIndex]);
|
2025-04-27 13:49:24 +02:00
|
|
|
};
|
|
|
|
|
|
2025-05-02 16:04:00 +02:00
|
|
|
interface ScrollPreservationInfo {
|
|
|
|
|
left: number;
|
|
|
|
|
top: number;
|
|
|
|
|
visibleElement: HTMLElement | undefined;
|
|
|
|
|
visibleElementLeft: number;
|
|
|
|
|
visibleElementTop: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const useScrollPreservationData = (
|
|
|
|
|
scrollElementRef: RefObject<HTMLElement | null>,
|
|
|
|
|
): RefObject<ScrollPreservationInfo> => {
|
|
|
|
|
const dataRef = useRef<ScrollPreservationInfo>({
|
2025-04-05 12:45:52 +02:00
|
|
|
left: 0,
|
|
|
|
|
top: 0,
|
2025-04-05 12:55:46 +02:00
|
|
|
visibleElement: undefined,
|
|
|
|
|
visibleElementLeft: 0,
|
|
|
|
|
visibleElementTop: 0,
|
2025-04-05 12:45:52 +02:00
|
|
|
});
|
2025-02-17 20:19:28 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-04-05 12:55:46 +02:00
|
|
|
const scrollElement = scrollElementRef.current;
|
2025-02-17 20:19:28 +01:00
|
|
|
|
2025-04-05 12:55:46 +02:00
|
|
|
if (!scrollElement) {
|
2025-02-17 20:19:28 +01:00
|
|
|
return () => {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onScroll = () => {
|
2025-05-02 16:04:00 +02:00
|
|
|
const { visibleElement } = dataRef.current;
|
|
|
|
|
dataRef.current = {
|
|
|
|
|
...dataRef.current,
|
2025-04-05 12:55:46 +02:00
|
|
|
left: scrollElement.scrollLeft,
|
|
|
|
|
top: scrollElement.scrollTop,
|
|
|
|
|
visibleElementLeft: visibleElement?.offsetLeft ?? 0,
|
|
|
|
|
visibleElementTop: visibleElement?.offsetTop ?? 0,
|
2025-02-17 20:19:28 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-05 12:55:46 +02:00
|
|
|
scrollElement.addEventListener('scroll', onScroll);
|
2025-02-17 20:19:28 +01:00
|
|
|
|
2025-04-05 12:55:46 +02:00
|
|
|
return () => scrollElement.removeEventListener('scroll', onScroll);
|
2025-02-17 20:19:28 +01:00
|
|
|
}, []);
|
|
|
|
|
|
2025-05-02 16:04:00 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
const scrollElement = scrollElementRef.current;
|
|
|
|
|
if (!scrollElement) {
|
|
|
|
|
return () => {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateObservation = (nodes: NodeList, intersectionAction: (element: Element) => void) =>
|
|
|
|
|
Array.from(nodes)
|
|
|
|
|
.filter((node) => node instanceof HTMLElement)
|
|
|
|
|
.flatMap((element) =>
|
|
|
|
|
element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
|
|
|
|
|
)
|
|
|
|
|
.forEach(intersectionAction);
|
|
|
|
|
|
|
|
|
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
|
|
|
const firstVisibleElement = entries.find((entry) => entry.isIntersecting);
|
|
|
|
|
|
|
|
|
|
if (!(firstVisibleElement?.target instanceof HTMLElement)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dataRef.current = {
|
|
|
|
|
...dataRef.current,
|
|
|
|
|
visibleElement: firstVisibleElement.target,
|
|
|
|
|
visibleElementLeft: firstVisibleElement.target.offsetLeft,
|
|
|
|
|
visibleElementTop: firstVisibleElement.target.offsetTop,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const mutationObserver = new MutationObserver((entries) => {
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
updateObservation(entry.addedNodes, (element) => intersectionObserver.observe(element));
|
|
|
|
|
updateObservation(entry.removedNodes, (element) => intersectionObserver.unobserve(element));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
mutationObserver.observe(scrollElement, {
|
|
|
|
|
childList: true,
|
|
|
|
|
subtree: true,
|
|
|
|
|
});
|
|
|
|
|
return () => {
|
|
|
|
|
mutationObserver.disconnect();
|
|
|
|
|
intersectionObserver.disconnect();
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return dataRef;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const usePreserveOnLeadingPageRender = (scrollElementRef: RefObject<HTMLElement | null>, readingMode: ReadingMode) => {
|
|
|
|
|
const preservationData = useScrollPreservationData(scrollElementRef);
|
|
|
|
|
|
|
|
|
|
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
|
|
|
|
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
|
|
|
|
|
|
2025-05-02 14:52:25 +02:00
|
|
|
useEffect(() => {
|
2025-02-17 20:19:28 +01:00
|
|
|
const scrollElement = scrollElementRef.current;
|
|
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
if (!scrollElement || !isContinuousReadingModeActive) {
|
|
|
|
|
return () => {};
|
2025-02-17 20:19:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-02 14:52:25 +02:00
|
|
|
const preserveScrollPosition: ResizeObserverCallback = (entries) => {
|
2025-05-02 16:04:00 +02:00
|
|
|
const { left, top, visibleElement, visibleElementLeft, visibleElementTop } = preservationData.current;
|
2025-02-17 20:19:28 +01:00
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
if (!visibleElement) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-05 12:45:52 +02:00
|
|
|
|
2025-05-02 14:52:25 +02:00
|
|
|
const entriesBeforeScrollPosition = entries.filter((entry) => {
|
|
|
|
|
if (!(entry.target instanceof HTMLElement)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isPreloadPage = !entry.target.clientWidth && !entry.target.clientHeight;
|
|
|
|
|
if (isPreloadPage) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isContinuousVerticalReadingModeActive) {
|
|
|
|
|
return entry.target.offsetTop < top;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.abs(entry.target.offsetLeft) < Math.abs(left);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const includesElementsBeforeScrollPosition = !!entriesBeforeScrollPosition.length;
|
|
|
|
|
if (!includesElementsBeforeScrollPosition) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
const newLeft = left - visibleElementLeft + visibleElement.offsetLeft;
|
|
|
|
|
const newTop = top - visibleElementTop + visibleElement.offsetTop;
|
2025-04-05 12:45:52 +02:00
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
scrollElement.scrollTo(newLeft, newTop);
|
|
|
|
|
};
|
2025-02-17 20:19:28 +01:00
|
|
|
|
2025-05-02 16:04:00 +02:00
|
|
|
const updateObservation = (nodes: NodeList, resizeAction: (element: Element) => void) =>
|
2025-04-05 12:45:52 +02:00
|
|
|
Array.from(nodes)
|
2025-04-05 12:55:46 +02:00
|
|
|
.filter((node) => node instanceof HTMLElement)
|
2025-05-02 14:52:25 +02:00
|
|
|
.flatMap((element) =>
|
|
|
|
|
element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
|
|
|
|
|
)
|
2025-05-02 16:04:00 +02:00
|
|
|
.forEach(resizeAction);
|
2025-04-05 12:45:52 +02:00
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
const resizeObserver = new ResizeObserver(preserveScrollPosition);
|
2025-04-05 12:45:52 +02:00
|
|
|
const mutationObserver = new MutationObserver((entries) => {
|
|
|
|
|
for (const entry of entries) {
|
2025-05-02 16:04:00 +02:00
|
|
|
updateObservation(entry.addedNodes, (element) => resizeObserver.observe(element));
|
|
|
|
|
updateObservation(entry.removedNodes, (element) => resizeObserver.unobserve(element));
|
2025-04-05 12:45:52 +02:00
|
|
|
}
|
|
|
|
|
});
|
2025-04-05 12:55:46 +02:00
|
|
|
|
|
|
|
|
mutationObserver.observe(scrollElement, {
|
2025-04-05 12:45:52 +02:00
|
|
|
childList: true,
|
2025-05-02 14:52:25 +02:00
|
|
|
subtree: true,
|
2025-04-05 12:45:52 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
mutationObserver.disconnect();
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
};
|
2025-05-02 14:52:25 +02:00
|
|
|
}, [isContinuousReadingModeActive, isContinuousVerticalReadingModeActive]);
|
2025-04-27 13:49:24 +02:00
|
|
|
};
|
2025-03-24 23:45:06 +01:00
|
|
|
|
2025-04-27 13:49:24 +02:00
|
|
|
export const useReaderPreserveScrollPosition = (
|
|
|
|
|
scrollElementRef: RefObject<HTMLElement | null>,
|
|
|
|
|
pageIndex: number,
|
|
|
|
|
readingMode: ReadingMode,
|
|
|
|
|
readingDirection: ReadingDirection,
|
|
|
|
|
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
|
|
|
|
pageScaleMode: ReaderPageScaleMode,
|
|
|
|
|
) => {
|
2025-05-02 14:52:25 +02:00
|
|
|
usePreserveOnLeadingPageRender(scrollElementRef, readingMode);
|
2025-04-28 02:21:23 +02:00
|
|
|
usePreserveOnWindowResize(readingMode, pageScaleMode, setPageToScrollToIndex, pageIndex);
|
2025-04-27 13:49:24 +02:00
|
|
|
usePreserveOnValueChange(readingDirection, pageIndex, setPageToScrollToIndex);
|
|
|
|
|
usePreserveOnValueChange(readingMode, pageIndex, setPageToScrollToIndex);
|
2025-02-17 20:19:28 +01:00
|
|
|
};
|