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

315 lines
11 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,
2024-12-22 00:42:33 +01:00
useCallback,
2024-11-06 13:00:36 +01:00
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
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,
ReaderPageSpreadState,
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 { getDoublePageModePages, isPageOfOutdatedPageLoadStates } from '@/modules/reader/utils/ReaderPager.utils.tsx';
2024-11-06 13:00:36 +01:00
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 {
getPagerForReadingMode,
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 {
createUpdateReaderPageLoadState,
useReaderAutoScroll,
useReaderConvertPagesForReadingMode,
useReaderHandlePageSelection,
useReaderHideCursorOnInactivity,
useReaderHideOverlayOnUserScroll,
useReaderHorizontalModeInvertXYScrolling,
} from '@/modules/reader/utils/Reader.utils.ts';
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
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,
setPages,
totalPages,
pageUrls,
pageLoadStates,
setPageLoadStates,
transitionPageMode,
retryFailedPagesKeyPrefix,
readingMode,
shouldOffsetDoubleSpreads,
readingDirection,
readerWidth,
pageScaleMode,
2024-12-21 03:46:55 +01:00
setScrollbarXSize,
setScrollbarYSize,
isVisible: isOverlayVisible,
setIsVisible: setIsOverlayVisible,
updateCurrentPageIndex,
showPreview,
setShowPreview,
2024-12-21 03:46:55 +01:00
}: Pick<
ReaderStatePages,
| 'currentPageIndex'
| 'pageToScrollToIndex'
| 'setPageToScrollToIndex'
2024-12-21 03:46:55 +01:00
| 'pages'
| 'setPages'
| 'totalPages'
| 'pageUrls'
| 'pageLoadStates'
| 'setPageLoadStates'
| 'transitionPageMode'
| 'retryFailedPagesKeyPrefix'
> &
Pick<
IReaderSettings,
'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode'
> &
2024-12-21 03:46:55 +01:00
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
TReaderTapZoneContext & {
2024-12-21 03:46:55 +01:00
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
},
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const { direction: themeDirection } = useTheme();
const scrollElementRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => scrollElementRef.current!);
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
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 previousTotalPages = useRef(totalPages);
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
pageLoadStates.map(({ url }) => ({ url, isSpread: false })),
);
2024-12-21 03:46:55 +01:00
const resetPagesSpreadState = previousTotalPages.current !== totalPages;
if (resetPagesSpreadState) {
previousTotalPages.current = totalPages;
setPagesToSpreadState(pageLoadStates.map(({ url }) => ({ url, isSpread: false })));
}
2024-12-21 03:46:55 +01:00
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const actualPages = useMemo(() => {
const arePagesLoaded = !!totalPages;
2024-12-21 03:46:55 +01:00
if (!arePagesLoaded) {
return pages;
}
2024-11-06 13:00:36 +01:00
2024-12-21 03:46:55 +01:00
if (readingMode === ReadingMode.DOUBLE_PAGE) {
return getDoublePageModePages(
pageUrls,
pagesToSpreadState,
shouldOffsetDoubleSpreads,
readingDirection,
);
}
2024-12-21 03:46:55 +01:00
return pages;
}, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]);
2024-12-21 03:46:55 +01:00
const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]);
2024-12-21 03:46:55 +01:00
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
const onLoad = useMemo(
() =>
createUpdateReaderPageLoadState(
actualPages,
pagesToSpreadState,
setPagesToSpreadState,
pageLoadStates,
setPageLoadStates,
readingMode,
),
// do not add "pagesToSpreadState" and "pageLoadStates" as a dependency, otherwise, every page gets re-rendered
// when they change which impacts the performance massively (depending on the total page count)
[actualPages, readingMode],
2024-12-22 00:42:33 +01:00
);
const onError = useCallback((pageIndex: number, url: string) => {
setPageLoadStates((statePageLoadStates) => {
const pageLoadState = statePageLoadStates[pageIndex];
if (isPageOfOutdatedPageLoadStates(url, pageLoadState)) {
return statePageLoadStates;
}
return statePageLoadStates.toSpliced(pageIndex, 1, {
...pageLoadState,
loaded: false,
error: true,
});
});
2024-12-22 00:42:33 +01:00
}, []);
useReaderConvertPagesForReadingMode(
currentPageIndex,
actualPages,
pageUrls,
setPages,
setPagesToSpreadState,
updateCurrentPageIndex,
readingMode,
);
useReaderHandlePageSelection(
pageToScrollToIndex,
currentPageIndex,
pages,
totalPages,
setPageToScrollToIndex,
updateCurrentPageIndex,
isContinuousReadingModeActive,
imageRefs,
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
2024-12-21 03:46:55 +01:00
return (
<Stack
ref={scrollElementRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
...applyStyles(
isContinuousVerticalReadingMode(readingMode) &&
shouldApplyReaderWidth(readerWidth, pageScaleMode),
{ alignItems: 'center' },
),
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,
)
}
>
<Pager
totalPages={totalPages}
currentPageIndex={currentPageIndex}
pages={actualPages}
transitionPageMode={transitionPageMode}
pageLoadStates={pageLoadStates}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
imageRefs={imageRefs}
2024-12-22 00:42:33 +01:00
onLoad={onLoad}
onError={onError}
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,
2024-12-21 03:46:55 +01:00
],
[
'currentPageIndex',
'pageToScrollToIndex',
'setPageToScrollToIndex',
2024-12-21 03:46:55 +01:00
'pages',
'setPages',
'totalPages',
'pageUrls',
'pageLoadStates',
'setPageLoadStates',
'transitionPageMode',
'retryFailedPagesKeyPrefix',
'readingMode',
'shouldOffsetDoubleSpreads',
'readingDirection',
'readerWidth',
'pageScaleMode',
2024-12-21 03:46:55 +01:00
'setScrollbarXSize',
'setScrollbarYSize',
'isVisible',
'setIsVisible',
'updateCurrentPageIndex',
'showPreview',
'setShowPreview',
2024-12-21 03:46:55 +01:00
],
);