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-03-24 23:45:06 +01:00
|
|
|
import { RefObject, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
2025-04-05 12:45:52 +02:00
|
|
|
import { ReaderPageScaleMode, 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-03-27 13:08:58 +01:00
|
|
|
import { isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
2025-02-17 20:19:28 +01:00
|
|
|
|
|
|
|
|
export const useReaderPreserveScrollPosition = (
|
|
|
|
|
scrollElementRef: RefObject<HTMLElement | null>,
|
2025-03-24 23:45:06 +01:00
|
|
|
pageIndex: number,
|
2025-03-24 23:24:36 +01:00
|
|
|
readingMode: ReadingMode,
|
2025-02-17 20:19:28 +01:00
|
|
|
isContinuousReadingModeActive: boolean,
|
2025-03-24 23:45:06 +01:00
|
|
|
readerNavBarWidth: number,
|
2025-03-24 23:24:36 +01:00
|
|
|
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
2025-03-27 13:08:58 +01:00
|
|
|
pageScaleMode: ReaderPageScaleMode,
|
2025-02-17 20:19:28 +01:00
|
|
|
) => {
|
2025-04-05 12:45:52 +02:00
|
|
|
const scrollPosition = useRef({
|
|
|
|
|
left: 0,
|
|
|
|
|
top: 0,
|
|
|
|
|
active: undefined as HTMLElement | undefined,
|
|
|
|
|
activeLeft: 0,
|
|
|
|
|
activeTop: 0,
|
|
|
|
|
});
|
2025-03-24 23:45:06 +01:00
|
|
|
const readerNavBarWidthRef = useRef<number>(readerNavBarWidth);
|
2025-02-17 20:19:28 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const element = scrollElementRef.current;
|
|
|
|
|
|
|
|
|
|
if (!element) {
|
|
|
|
|
return () => {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onScroll = () => {
|
2025-04-05 12:45:52 +02:00
|
|
|
const { active } = scrollPosition.current;
|
2025-02-17 20:19:28 +01:00
|
|
|
scrollPosition.current = {
|
|
|
|
|
...scrollPosition.current,
|
|
|
|
|
left: element.scrollLeft,
|
|
|
|
|
top: element.scrollTop,
|
2025-04-05 12:45:52 +02:00
|
|
|
activeLeft: active?.offsetLeft ?? 0,
|
|
|
|
|
activeTop: active?.offsetTop ?? 0,
|
2025-02-17 20:19:28 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
element.addEventListener('scroll', onScroll);
|
|
|
|
|
|
|
|
|
|
return () => element.removeEventListener('scroll', onScroll);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-03-27 13:08:58 +01:00
|
|
|
// on rendering previous chapter (infinite scroll in continuous reading modes)
|
2025-04-05 12:45:52 +02:00
|
|
|
const onDoPreserveScroll = useCallback(() => {
|
2025-02-17 20:19:28 +01:00
|
|
|
const scrollElement = scrollElementRef.current;
|
2025-04-05 12:45:52 +02:00
|
|
|
const { left, top, active, activeLeft, activeTop } = scrollPosition.current;
|
2025-02-17 20:19:28 +01:00
|
|
|
|
|
|
|
|
if (!scrollElement || !isContinuousReadingModeActive) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-05 12:45:52 +02:00
|
|
|
if (!active) return;
|
2025-02-17 20:19:28 +01:00
|
|
|
|
2025-04-05 12:45:52 +02:00
|
|
|
const newLeft = left - activeLeft + active.offsetLeft;
|
|
|
|
|
const newTop = top - activeTop + active.offsetTop;
|
|
|
|
|
scrollElement.scrollTo(newLeft, newTop);
|
|
|
|
|
}, [scrollElementRef, isContinuousReadingModeActive]);
|
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
const element = scrollElementRef.current;
|
|
|
|
|
|
|
|
|
|
if (!element) {
|
|
|
|
|
return () => {};
|
2025-02-17 20:19:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-04-05 12:45:52 +02:00
|
|
|
const updateObservation = (
|
|
|
|
|
nodes: NodeList,
|
|
|
|
|
resizeAction: (n: Element) => void,
|
|
|
|
|
intersectionAction: (n: Element) => void,
|
|
|
|
|
) =>
|
|
|
|
|
Array.from(nodes)
|
|
|
|
|
.filter((n) => n instanceof HTMLElement)
|
|
|
|
|
.flatMap((n) => {
|
|
|
|
|
resizeAction(n);
|
|
|
|
|
return Array.from(n.querySelectorAll('img'));
|
|
|
|
|
})
|
|
|
|
|
.forEach(intersectionAction);
|
|
|
|
|
|
|
|
|
|
const resizeObserver = new ResizeObserver(onDoPreserveScroll);
|
|
|
|
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
|
|
|
// find the first visible image inside the viewport
|
|
|
|
|
const first = entries.filter((e) => e.isIntersecting).shift();
|
|
|
|
|
if (!first || !(first.target instanceof HTMLElement)) return;
|
|
|
|
|
scrollPosition.current = {
|
|
|
|
|
...scrollPosition.current,
|
|
|
|
|
active: first.target,
|
|
|
|
|
activeLeft: first.target.offsetLeft,
|
|
|
|
|
activeTop: first.target.offsetTop,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const mutationObserver = new MutationObserver((entries) => {
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
updateObservation(
|
|
|
|
|
entry.addedNodes,
|
|
|
|
|
(n) => resizeObserver.observe(n),
|
|
|
|
|
(n) => intersectionObserver.observe(n),
|
|
|
|
|
);
|
|
|
|
|
updateObservation(
|
|
|
|
|
entry.removedNodes,
|
|
|
|
|
(n) => resizeObserver.unobserve(n),
|
|
|
|
|
(n) => intersectionObserver.unobserve(n),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
mutationObserver.observe(element, {
|
|
|
|
|
childList: true,
|
|
|
|
|
subtree: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
mutationObserver.disconnect();
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
intersectionObserver.disconnect();
|
|
|
|
|
};
|
|
|
|
|
}, [scrollElementRef, onDoPreserveScroll]);
|
2025-03-24 23:24:36 +01:00
|
|
|
|
2025-03-27 13:08:58 +01:00
|
|
|
const onAvailableReaderWidthChange = useCallback(() => {
|
|
|
|
|
if (!isContinuousReadingModeActive) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isReaderWidthEditable(pageScaleMode)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 23:45:06 +01:00
|
|
|
setPageToScrollToIndex(pageIndex);
|
2025-03-27 13:08:58 +01:00
|
|
|
}, [isContinuousReadingModeActive, pageIndex, pageScaleMode]);
|
2025-03-24 23:45:06 +01:00
|
|
|
|
2025-03-27 13:08:58 +01:00
|
|
|
// on window resize
|
2025-03-24 23:45:06 +01:00
|
|
|
useEffect(() => {
|
2025-03-27 13:08:58 +01:00
|
|
|
window.addEventListener('resize', onAvailableReaderWidthChange);
|
|
|
|
|
return () => window.removeEventListener('resize', onAvailableReaderWidthChange);
|
|
|
|
|
}, [onAvailableReaderWidthChange]);
|
2025-03-24 23:45:06 +01:00
|
|
|
|
2025-03-27 13:08:58 +01:00
|
|
|
// on reader nav bar static setting change
|
2025-03-24 23:45:06 +01:00
|
|
|
useEffect(() => {
|
2025-03-27 13:08:58 +01:00
|
|
|
if (readerNavBarWidthRef.current === readerNavBarWidth) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 23:45:06 +01:00
|
|
|
readerNavBarWidthRef.current = readerNavBarWidth;
|
2025-03-27 13:08:58 +01:00
|
|
|
onAvailableReaderWidthChange();
|
|
|
|
|
}, [onAvailableReaderWidthChange, readerNavBarWidth]);
|
2025-03-24 23:45:06 +01:00
|
|
|
|
2025-04-05 01:06:59 +02:00
|
|
|
// on "reading mode" or "reading direction" change
|
2025-03-24 23:24:36 +01:00
|
|
|
useLayoutEffect(() => {
|
2025-03-24 23:45:06 +01:00
|
|
|
setPageToScrollToIndex(pageIndex);
|
2025-04-05 01:06:59 +02:00
|
|
|
}, [readingMode, readingDirection]);
|
2025-02-17 20:19:28 +01:00
|
|
|
};
|