Files
suwayomi-material-you-webui/src/modules/reader/components/viewer/ReaderViewer.tsx

436 lines
18 KiB
TypeScript
Raw Normal View History

2024-11-06 13:00:36 +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/.
*/
import {
ForwardedRef,
forwardRef,
2024-12-22 00:41:56 +01:00
memo,
useCallback,
2024-11-06 13:00:36 +01:00
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
2024-11-06 13:00:36 +01:00
} from 'react';
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
2025-01-28 02:16:00 +01:00
import { useLocation } from 'react-router-dom';
2024-11-06 13:00:36 +01:00
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
2024-12-21 03:46:55 +01:00
import {
IReaderSettings,
PageInViewportType,
2025-01-28 02:16:00 +01:00
ReaderOpenChapterLocationState,
ReaderResumeMode,
ReaderStateChapters,
ReadingDirection,
2024-12-21 03:46:55 +01:00
ReadingMode,
TReaderScrollbarContext,
} from '@/modules/reader/types/Reader.types.ts';
2024-11-06 13:00:36 +01:00
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
import {
isContinuousReadingMode,
isContinuousVerticalReadingMode,
shouldApplyReaderWidth,
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { useMouseDragScroll } from '@/modules/core/hooks/useMouseDragScroll.tsx';
2024-12-11 21:04:44 +01:00
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
2024-12-21 03:46:55 +01:00
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
2024-12-27 00:30:19 +01:00
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
2025-01-18 22:07:59 +01:00
import { useReaderAutoScroll } from '@/modules/reader/hooks/useReaderAutoScroll.ts';
import { useReaderHideOverlayOnUserScroll } from '@/modules/reader/hooks/useReaderHideOverlayOnUserScroll.ts';
import { useReaderHorizontalModeInvertXYScrolling } from '@/modules/reader/hooks/useReaderHorizontalModeInvertXYScrolling.ts';
import { useReaderHideCursorOnInactivity } from '@/modules/reader/hooks/useReaderHideCursorOnInactivity.ts';
import { useReaderScrollToStartOnPageChange } from '@/modules/reader/hooks/useReaderScrollToStartOnPageChange.ts';
import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHandlePageSelection.ts';
2025-01-28 02:16:00 +01:00
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { ReaderChapterViewer } from '@/modules/reader/components/viewer/ReaderChapterViewer.tsx';
import {
getReaderChapterViewerCurrentPageIndex,
getReaderChapterViewResumeMode,
} from '@/modules/reader/utils/Reader.utils.ts';
import { noOp } from '@/lib/HelperFunctions.ts';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
2024-11-06 13:00:36 +01:00
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
[ReadingMode.DOUBLE_PAGE]: PageInViewportType.X,
[ReadingMode.CONTINUOUS_VERTICAL]: PageInViewportType.Y,
[ReadingMode.CONTINUOUS_HORIZONTAL]: PageInViewportType.X,
2024-12-16 16:26:32 +01:00
[ReadingMode.WEBTOON]: PageInViewportType.Y,
2024-11-06 13:00:36 +01:00
};
2024-12-21 03:46:55 +01:00
const BaseReaderViewer = forwardRef(
(
{
currentPageIndex,
pageToScrollToIndex,
setPageToScrollToIndex,
2024-12-21 03:46:55 +01:00
pages,
totalPages,
2025-01-28 02:16:00 +01:00
setPages,
2024-12-21 03:46:55 +01:00
setPageLoadStates,
2025-01-28 02:16:00 +01:00
setTotalPages,
setCurrentPageIndex,
2024-12-21 03:46:55 +01:00
transitionPageMode,
retryFailedPagesKeyPrefix,
2025-01-28 02:16:00 +01:00
setTransitionPageMode,
2024-12-21 03:46:55 +01:00
readingMode,
readingDirection,
readerWidth,
pageScaleMode,
2025-01-28 02:16:00 +01:00
shouldOffsetDoubleSpreads,
imagePreLoadAmount,
pageGap,
customFilter,
shouldStretchPage,
readerNavBarWidth,
2024-12-21 03:46:55 +01:00
setScrollbarXSize,
setScrollbarYSize,
isVisible: isOverlayVisible,
setIsVisible: setIsOverlayVisible,
updateCurrentPageIndex,
showPreview,
setShowPreview,
2025-01-28 02:16:00 +01:00
initialChapter,
currentChapter,
chapters,
visibleChapters,
setReaderStateChapters,
isCurrentChapterReady,
2024-12-21 03:46:55 +01:00
}: Pick<
ReaderStatePages,
| 'currentPageIndex'
| 'pageToScrollToIndex'
| 'setPageToScrollToIndex'
2024-12-21 03:46:55 +01:00
| 'pages'
| 'totalPages'
2025-01-28 02:16:00 +01:00
| 'setPages'
2024-12-21 03:46:55 +01:00
| 'setPageLoadStates'
2025-01-28 02:16:00 +01:00
| 'setTotalPages'
| 'setCurrentPageIndex'
2024-12-21 03:46:55 +01:00
| 'transitionPageMode'
| 'retryFailedPagesKeyPrefix'
2025-01-28 02:16:00 +01:00
| 'setTransitionPageMode'
2024-12-21 03:46:55 +01:00
> &
Pick<
IReaderSettings,
| 'readingMode'
| 'readingDirection'
| 'readerWidth'
| 'pageScaleMode'
| 'shouldOffsetDoubleSpreads'
| 'imagePreLoadAmount'
| 'pageGap'
| 'customFilter'
| 'shouldStretchPage'
> &
2024-12-21 03:46:55 +01:00
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
Pick<NavbarContextType, 'readerNavBarWidth'> &
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
2025-01-28 02:16:00 +01:00
Pick<
ReaderStateChapters,
| 'initialChapter'
| 'currentChapter'
| 'chapters'
| 'visibleChapters'
| 'setReaderStateChapters'
| 'isCurrentChapterReady'
> &
TReaderTapZoneContext & {
2024-12-21 03:46:55 +01:00
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
},
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const { direction: themeDirection } = useTheme();
2025-01-28 02:16:00 +01:00
const { resumeMode } = useLocation<ReaderOpenChapterLocationState>().state ?? {
resumeMode: ReaderResumeMode.START,
};
2024-12-21 03:46:55 +01:00
const scrollElementRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => scrollElementRef.current!);
2025-01-28 02:16:00 +01:00
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
2024-12-21 03:46:55 +01:00
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
const isDragging = useMouseDragScroll(scrollElementRef);
2024-12-21 03:46:55 +01:00
2024-12-27 00:30:19 +01:00
const automaticScrolling = useReaderAutoScrollContext();
useEffect(() => automaticScrolling.setScrollRef(scrollElementRef), []);
2024-12-21 03:46:55 +01:00
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
useLayoutEffect(() => {
setScrollbarXSize(scrollbarXSize);
setScrollbarYSize(scrollbarYSize);
}, [scrollbarXSize, scrollbarYSize]);
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const chapterViewerSize = useRef({ minChapterViewWidth: 0, minChapterViewHeight: 0 });
const { minChapterViewWidth, minChapterViewHeight } = chapterViewerSize.current;
const [, setTriggerReRender] = useState({});
2024-12-21 03:46:55 +01:00
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
2025-01-28 02:16:00 +01:00
const initialChapterIndex = useMemo(
() => chapters.findIndex((chapter) => chapter.id === initialChapter?.id),
[initialChapter?.id],
);
const chaptersToRender = useMemo(
() =>
2025-01-28 02:16:00 +01:00
chapters.slice(
Math.max(0, initialChapterIndex - visibleChapters.trailing),
Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1),
),
2025-01-28 02:16:00 +01:00
[chapters, initialChapterIndex, visibleChapters.trailing, visibleChapters.leading, chapters.length],
2024-12-22 00:42:33 +01:00
);
2025-01-28 02:16:00 +01:00
const currentChapterIndex = useMemo(
() => chaptersToRender.findIndex((chapter) => chapter.id === currentChapter?.id),
[currentChapter, chaptersToRender],
);
2025-01-28 02:16:00 +01:00
const onChapterViewSizeChange = useCallback(
(width: number, height: number) => {
if (!isContinuousReadingModeActive) {
return;
}
if (isContinuousVerticalReadingModeActive && minChapterViewWidth < width) {
chapterViewerSize.current.minChapterViewWidth = width;
setTriggerReRender({});
}
if (minChapterViewHeight < height) {
chapterViewerSize.current.minChapterViewHeight = height;
setTriggerReRender({});
}
},
[isContinuousReadingModeActive, isContinuousVerticalReadingModeActive],
);
useReaderHandlePageSelection(
pageToScrollToIndex,
currentPageIndex,
pages,
totalPages,
setPageToScrollToIndex,
updateCurrentPageIndex,
isContinuousReadingModeActive,
imageRefs,
themeDirection,
readingDirection,
);
useReaderScrollToStartOnPageChange(
currentPageIndex,
isContinuousReadingModeActive,
themeDirection,
readingDirection,
scrollElementRef,
);
useReaderHideCursorOnInactivity(scrollElementRef);
useReaderHorizontalModeInvertXYScrolling(readingMode, readingDirection, scrollElementRef);
useReaderHideOverlayOnUserScroll(
isOverlayVisible,
setIsOverlayVisible,
showPreview,
setShowPreview,
scrollElementRef,
);
useReaderAutoScroll(isOverlayVisible, automaticScrolling);
2024-12-27 00:30:19 +01:00
useLayoutEffect(() => {
chapterViewerSize.current = { minChapterViewWidth: 0, minChapterViewHeight: 0 };
setTriggerReRender({});
}, [readingMode]);
2025-01-28 02:16:00 +01:00
if (!initialChapter || !currentChapter) {
throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined');
}
2024-12-21 03:46:55 +01:00
return (
<Stack
ref={scrollElementRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
flexWrap: 'nowrap',
...applyStyles(
2025-01-28 02:16:00 +01:00
isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
{ alignItems: 'center' },
),
2025-01-28 02:16:00 +01:00
...applyStyles(!isContinuousVerticalReadingModeActive, {
...applyStyles(themeDirection === 'ltr', {
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
}),
...applyStyles(themeDirection === 'rtl', {
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
}),
}),
2024-11-06 13:00:36 +01:00
}}
2024-12-21 03:46:55 +01:00
onClick={(e) => !isDragging && handleClick(e)}
onScroll={() =>
ReaderControls.updateCurrentPageOnScroll(
imageRefs,
totalPages - 1,
updateCurrentPageIndex,
inViewportType,
readingDirection,
)
}
>
2025-01-28 02:16:00 +01:00
{chaptersToRender.map((_, index) => {
// chapters are sorted by latest to oldest, thus, loop over it in reversed order
const chapterIndex = Math.max(0, chaptersToRender.length - index - 1);
const chapter = chaptersToRender[chapterIndex];
const previousChapter =
chaptersToRender[chapterIndex + 1] ??
chapters[initialChapterIndex + visibleChapters.leading + 1];
const nextChapter =
chaptersToRender[chapterIndex - 1] ??
chapters[initialChapterIndex - visibleChapters.trailing - 1];
const isInitialChapter = chapter.id === initialChapter.id;
const isCurrentChapter = chapter.id === currentChapter.id;
const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id;
const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id;
const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder;
const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder;
return (
<ReaderChapterViewer
key={chapter.id}
chapterId={chapter.id}
previousChapterId={previousChapter?.id}
nextChapterId={nextChapter?.id}
isPreviousChapterVisible={!!chaptersToRender[chapterIndex + 1]}
isNextChapterVisible={!!chaptersToRender[chapterIndex - 1]}
lastPageRead={chapter.lastPageRead}
currentPageIndex={getReaderChapterViewerCurrentPageIndex(
currentPageIndex,
chapter,
currentChapter,
isCurrentChapter,
isCurrentChapterReady,
isLeadingChapter,
isTrailingChapter,
visibleChapters,
)}
isInitialChapter={isInitialChapter}
isCurrentChapter={isCurrentChapter}
isPreviousChapter={isPreviousChapter}
isNextChapter={isNextChapter}
isLeadingChapter={isLeadingChapter}
isTrailingChapter={isTrailingChapter}
imageRefs={imageRefs}
setPages={setPages}
setPageLoadStates={setPageLoadStates}
setTotalPages={setTotalPages}
setCurrentPageIndex={setCurrentPageIndex}
setPageToScrollToIndex={setPageToScrollToIndex}
transitionPageMode={transitionPageMode}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
readingMode={readingMode}
readerWidth={readerWidth}
pageScaleMode={pageScaleMode}
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads}
readingDirection={readingDirection}
updateCurrentPageIndex={isCurrentChapter ? updateCurrentPageIndex : noOp}
scrollIntoView={isCurrentChapter && visibleChapters.scrollIntoView}
resumeMode={getReaderChapterViewResumeMode(
isCurrentChapter,
isInitialChapter,
isLeadingChapter,
isTrailingChapter,
visibleChapters.resumeMode,
resumeMode,
)}
setReaderStateChapters={setReaderStateChapters}
setTransitionPageMode={setTransitionPageMode}
pageGap={pageGap}
imagePreLoadAmount={imagePreLoadAmount}
customFilter={customFilter}
shouldStretchPage={shouldStretchPage}
scrollbarXSize={scrollbarXSize}
scrollbarYSize={scrollbarYSize}
readerNavBarWidth={readerNavBarWidth}
onSizeChange={onChapterViewSizeChange}
minWidth={minChapterViewWidth}
minHeight={minChapterViewHeight}
2025-01-28 02:16:00 +01:00
/>
);
})}
2024-12-21 03:46:55 +01:00
</Stack>
);
},
);
export const ReaderViewer = withPropsFrom(
2024-12-22 00:41:56 +01:00
memo(BaseReaderViewer),
2024-12-21 03:46:55 +01:00
[
userReaderStatePagesContext,
ReaderService.useSettingsWithoutDefaultFlag,
useReaderScrollbarContext,
useReaderOverlayContext,
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
useReaderTapZoneContext,
2025-01-28 02:16:00 +01:00
useReaderStateChaptersContext,
useNavBarContext,
2024-12-21 03:46:55 +01:00
],
[
'currentPageIndex',
'pageToScrollToIndex',
'setPageToScrollToIndex',
2024-12-21 03:46:55 +01:00
'pages',
'totalPages',
2025-01-28 02:16:00 +01:00
'setPages',
2024-12-21 03:46:55 +01:00
'setPageLoadStates',
2025-01-28 02:16:00 +01:00
'setTotalPages',
'setCurrentPageIndex',
2024-12-21 03:46:55 +01:00
'retryFailedPagesKeyPrefix',
2025-01-28 02:16:00 +01:00
'setTransitionPageMode',
2024-12-21 03:46:55 +01:00
'readingMode',
'readingDirection',
'readerWidth',
'pageScaleMode',
2025-01-28 02:16:00 +01:00
'shouldOffsetDoubleSpreads',
'imagePreLoadAmount',
'pageGap',
'customFilter',
'shouldStretchPage',
'readerNavBarWidth',
2025-01-28 02:16:00 +01:00
'transitionPageMode',
2024-12-21 03:46:55 +01:00
'setScrollbarXSize',
'setScrollbarYSize',
'isVisible',
'setIsVisible',
'updateCurrentPageIndex',
'showPreview',
'setShowPreview',
2025-01-28 02:16:00 +01:00
'initialChapter',
'currentChapter',
'chapters',
'visibleChapters',
'setReaderStateChapters',
'isCurrentChapterReady',
2024-12-21 03:46:55 +01:00
],
);