Use HOC for reader context usage

This commit is contained in:
schroda
2024-12-21 03:46:55 +01:00
parent 34f484a946
commit f5642af219
27 changed files with 997 additions and 459 deletions

View File

@@ -9,7 +9,7 @@
import { ComponentProps, ForwardedRef, forwardRef } from 'react';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderCustomFilter } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderCustomFilter, TReaderScrollbarContext } from '@/modules/reader/types/Reader.types.ts';
import {
getImageMarginStyling,
getImagePlaceholderStyling,
@@ -18,6 +18,7 @@ import {
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
Object.keys(customFilter)
@@ -48,7 +49,7 @@ const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
})
.join(' ');
export const ReaderPage = forwardRef(
const BaseReaderPage = forwardRef(
(
{
display,
@@ -56,18 +57,27 @@ export const ReaderPage = forwardRef(
position,
marginTop,
shouldLoad,
readingMode,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
scrollbarXSize,
scrollbarYSize,
...props
}: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle'> & {
display: boolean;
doublePage?: boolean;
position?: 'left' | 'right';
marginTop?: number;
},
}: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle'> &
Pick<
IReaderSettings,
'readingMode' | 'customFilter' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth'
> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & {
display: boolean;
doublePage?: boolean;
position?: 'left' | 'right';
marginTop?: number;
},
ref: ForwardedRef<HTMLImageElement | null>,
) => {
const { readingMode, customFilter, pageScaleMode, shouldStretchPage, readerWidth } =
ReaderService.useSettings();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const isTabletWidth = MediaQuery.useIsTabletWidth();
if (!display && !shouldLoad) {
@@ -83,10 +93,10 @@ export const ReaderPage = forwardRef(
spinnerStyle={{
backgroundColor: 'background.paper',
...getImagePlaceholderStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
readerWidth.value,
readingMode,
shouldStretchPage,
pageScaleMode,
readerWidth,
scrollbarXSize,
scrollbarYSize,
doublePage,
@@ -99,11 +109,11 @@ export const ReaderPage = forwardRef(
}}
imgStyle={{
...getImageWidthStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
readingMode,
shouldStretchPage,
pageScaleMode,
doublePage,
readerWidth.value,
readerWidth,
true,
),
display: 'block',
@@ -130,3 +140,17 @@ export const ReaderPage = forwardRef(
);
},
);
export const ReaderPage = withPropsFrom(
BaseReaderPage,
[ReaderService.useSettingsWithoutDefaultFlag, useReaderScrollbarContext],
[
'readingMode',
'customFilter',
'pageScaleMode',
'shouldStretchPage',
'readerWidth',
'scrollbarXSize',
'scrollbarYSize',
],
);

View File

@@ -18,12 +18,14 @@ import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
import {
IReaderSettings,
ReaderPageScaleMode,
ReaderStateChapters,
ReaderTransitionPageMode,
ReadingMode,
TReaderScrollbarContext,
TReaderStateMangaContext,
} from '@/modules/reader/types/Reader.types.ts';
import { isTransitionPageVisible } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import {
isContinuousReadingMode,
@@ -31,6 +33,8 @@ import {
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const ChapterInfo = ({
title,
@@ -58,22 +62,29 @@ const ChapterInfo = ({
);
};
export const ReaderTransitionPage = ({
const BaseReaderTransitionPage = ({
type,
mode,
readingMode,
pageScaleMode,
}: Pick<IReaderSettings, 'readingMode' | 'pageScaleMode'> & {
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
mode: ReaderTransitionPageMode;
}) => {
manga,
currentChapter,
previousChapter,
nextChapter,
scrollbarXSize,
scrollbarYSize,
readerNavBarWidth,
}: Pick<IReaderSettings, 'readingMode' | 'pageScaleMode'> &
Pick<TReaderStateMangaContext, 'manga'> &
Pick<ReaderStateChapters, 'currentChapter' | 'previousChapter' | 'nextChapter'> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
Pick<NavbarContextType, 'readerNavBarWidth'> & {
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
mode: ReaderTransitionPageMode;
}) => {
const { t } = useTranslation();
const handleBack = useBackButton();
const { manga } = useReaderStateMangaContext();
const { currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const { readerNavBarWidth } = useNavBarContext();
const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS;
const isNextType = type === ReaderTransitionPageMode.NEXT;
@@ -191,3 +202,17 @@ export const ReaderTransitionPage = ({
</Stack>
);
};
export const ReaderTransitionPage = withPropsFrom(
BaseReaderTransitionPage,
[useReaderStateChaptersContext, useReaderScrollbarContext, useNavBarContext],
[
'manga',
'currentChapter',
'previousChapter',
'nextChapter',
'scrollbarXSize',
'scrollbarYSize',
'readerNavBarWidth',
],
);

View File

@@ -19,7 +19,13 @@ import {
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { PageInViewportType, ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import {
IReaderSettings,
PageInViewportType,
ReadingDirection,
ReadingMode,
TReaderScrollbarContext,
} from '@/modules/reader/types/Reader.types.ts';
import { ReaderPagedPager } from '@/modules/reader/components/viewer/pager/ReaderPagedPager.tsx';
import { ReaderDoublePagedPager } from '@/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx';
import { ReaderHorizontalPager } from '@/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx';
@@ -43,6 +49,9 @@ import { useMouseDragScroll } from '@/modules/core/hooks/useMouseDragScroll.tsx'
import { DirectionOffset } from '@/Base.types.ts';
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
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';
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
@@ -52,251 +61,308 @@ const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType>
[ReadingMode.WEBTOON]: PageInViewportType.Y,
};
export const ReaderViewer = forwardRef((_, ref: ForwardedRef<HTMLDivElement | null>) => {
const {
currentPageIndex,
pageToScrollToIndex,
pages,
setPages,
totalPages,
pageUrls,
pageLoadStates,
setPageLoadStates,
transitionPageMode,
retryFailedPagesKeyPrefix,
} = userReaderStatePagesContext();
const { direction: themeDirection } = useTheme();
const { readingMode, shouldOffsetDoubleSpreads, readingDirection } = ReaderService.useSettings();
const { setScrollbarXSize, setScrollbarYSize } = useReaderScrollbarContext();
const { isVisible: isOverlayVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
const updateCurrentPageIndex = ReaderControls.useUpdateCurrentPageIndex();
const BaseReaderViewer = forwardRef(
(
{
currentPageIndex,
pageToScrollToIndex,
pages,
setPages,
totalPages,
pageUrls,
pageLoadStates,
setPageLoadStates,
transitionPageMode,
retryFailedPagesKeyPrefix,
readingMode,
shouldOffsetDoubleSpreads,
readingDirection,
setScrollbarXSize,
setScrollbarYSize,
isVisible: isOverlayVisible,
setIsVisible: setIsOverlayVisible,
updateCurrentPageIndex,
}: Pick<
ReaderStatePages,
| 'currentPageIndex'
| 'pageToScrollToIndex'
| 'pages'
| 'setPages'
| 'totalPages'
| 'pageUrls'
| 'pageLoadStates'
| 'setPageLoadStates'
| 'transitionPageMode'
| 'retryFailedPagesKeyPrefix'
> &
Pick<IReaderSettings, 'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection'> &
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> & {
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
},
const scrollElementRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => scrollElementRef.current!);
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const { direction: themeDirection } = useTheme();
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode.value);
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
const scrollElementRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => scrollElementRef.current!);
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
useLayoutEffect(() => {
setScrollbarXSize(scrollbarXSize);
setScrollbarYSize(scrollbarYSize);
}, [scrollbarXSize, scrollbarYSize]);
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
useLayoutEffect(() => {
setScrollbarXSize(scrollbarXSize);
setScrollbarYSize(scrollbarYSize);
}, [scrollbarXSize, scrollbarYSize]);
const [wasDoublePageMode, setWasDoublePageMode] = useState(readingMode.value === ReadingMode.DOUBLE_PAGE);
const [pagesToSpreadState, setPagesToSpreadState] = useState(Array(totalPages).fill(false));
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const [wasDoublePageMode, setWasDoublePageMode] = useState(readingMode === ReadingMode.DOUBLE_PAGE);
const [pagesToSpreadState, setPagesToSpreadState] = useState(Array(totalPages).fill(false));
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const actualPages = useMemo(() => {
const arePagesLoaded = pageUrls.length;
if (!arePagesLoaded) {
return pages;
}
if (readingMode === ReadingMode.DOUBLE_PAGE) {
return getDoublePageModePages(
pageUrls,
pagesToSpreadState,
shouldOffsetDoubleSpreads,
readingDirection,
);
}
const actualPages = useMemo(() => {
const arePagesLoaded = pageUrls.length;
if (!arePagesLoaded) {
return pages;
}
}, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection]);
if (readingMode.value === ReadingMode.DOUBLE_PAGE) {
return getDoublePageModePages(
pageUrls,
pagesToSpreadState,
shouldOffsetDoubleSpreads.value,
readingDirection.value,
);
}
const Pager = useMemo(() => {
switch (readingMode) {
case ReadingMode.SINGLE_PAGE:
return ReaderPagedPager;
case ReadingMode.DOUBLE_PAGE:
return ReaderDoublePagedPager;
case ReadingMode.CONTINUOUS_VERTICAL:
case ReadingMode.WEBTOON:
return ReaderVerticalPager;
case ReadingMode.CONTINUOUS_HORIZONTAL:
return ReaderHorizontalPager;
default:
throw new Error(`Unexpected "ReadingMode" (${readingMode})`);
}
}, [readingMode]);
return pages;
}, [pagesToSpreadState, readingMode.value, shouldOffsetDoubleSpreads.value, readingDirection.value]);
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
const Pager = useMemo(() => {
switch (readingMode.value) {
case ReadingMode.SINGLE_PAGE:
return ReaderPagedPager;
case ReadingMode.DOUBLE_PAGE:
return ReaderDoublePagedPager;
case ReadingMode.CONTINUOUS_VERTICAL:
case ReadingMode.WEBTOON:
return ReaderVerticalPager;
case ReadingMode.CONTINUOUS_HORIZONTAL:
return ReaderHorizontalPager;
default:
throw new Error(`Unexpected "ReadingMode" (${readingMode.value})`);
}
}, [readingMode.value]);
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode.value];
// reset spread state
useLayoutEffect(() => {
setPagesToSpreadState(Array(totalPages).fill(false));
}, [totalPages]);
// optionally convert pages to normal or double page mode
useLayoutEffect(() => {
const convertPagesToNormalPageMode = wasDoublePageMode && readingMode.value !== ReadingMode.DOUBLE_PAGE;
if (convertPagesToNormalPageMode) {
setWasDoublePageMode(false);
setPages(createPagesData(pageUrls));
// reset spread state
useLayoutEffect(() => {
setPagesToSpreadState(Array(totalPages).fill(false));
return;
}
}, [totalPages]);
const convertPagesToDoublePageMode = readingMode.value === ReadingMode.DOUBLE_PAGE;
if (convertPagesToDoublePageMode) {
if (!wasDoublePageMode) {
updateCurrentPageIndex(getNextIndexFromPage(getPage(currentPageIndex, actualPages)));
}
setPages(actualPages);
setWasDoublePageMode(readingMode.value === ReadingMode.DOUBLE_PAGE);
}
}, [actualPages, readingMode.value]);
// handle user page selection
useLayoutEffect(() => {
const pageToScrollTo = getPage(pageToScrollToIndex, pages);
if (isContinuousReadingModeActive) {
const directionOffset =
pageToScrollToIndex > currentPageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT;
const imageRef = imageRefs.current[pageToScrollTo.pagesIndex];
imageRef?.scrollIntoView({
block: 'start',
inline: getScrollIntoViewInlineOption(directionOffset, themeDirection, readingDirection.value),
});
}
const newPageIndex = getNextIndexFromPage(pageToScrollTo);
const isLastPage = newPageIndex === totalPages - 1;
updateCurrentPageIndex(newPageIndex, !isLastPage);
}, [pageToScrollToIndex]);
// hide cursor on mouse inactivity
const mouseInactiveTimeout = useRef<NodeJS.Timeout>();
useEffect(() => {
const setCursorVisibility = (visible: boolean) => {
const scrollElement = scrollElementRef.current;
if (!scrollElement) {
// optionally convert pages to normal or double page mode
useLayoutEffect(() => {
const convertPagesToNormalPageMode = wasDoublePageMode && readingMode !== ReadingMode.DOUBLE_PAGE;
if (convertPagesToNormalPageMode) {
setWasDoublePageMode(false);
setPages(createPagesData(pageUrls));
setPagesToSpreadState(Array(totalPages).fill(false));
return;
}
scrollElement.style.cursor = visible ? 'default' : 'none';
};
const convertPagesToDoublePageMode = readingMode === ReadingMode.DOUBLE_PAGE;
if (convertPagesToDoublePageMode) {
if (!wasDoublePageMode) {
updateCurrentPageIndex(getNextIndexFromPage(getPage(currentPageIndex, actualPages)));
}
setPages(actualPages);
setWasDoublePageMode(readingMode === ReadingMode.DOUBLE_PAGE);
}
}, [actualPages, readingMode]);
const handleMouseMove = () => {
setCursorVisibility(true);
clearTimeout(mouseInactiveTimeout.current);
mouseInactiveTimeout.current = setTimeout(() => {
setCursorVisibility(false);
}, 5000);
};
// handle user page selection
useLayoutEffect(() => {
const pageToScrollTo = getPage(pageToScrollToIndex, pages);
handleMouseMove();
window.addEventListener('mousemove', handleMouseMove);
return () => {
setCursorVisibility(true);
window.removeEventListener('mousemove', handleMouseMove);
clearTimeout(mouseInactiveTimeout.current);
};
}, []);
if (isContinuousReadingModeActive) {
const directionOffset =
pageToScrollToIndex > currentPageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT;
const imageRef = imageRefs.current[pageToScrollTo.pagesIndex];
// invert x and y scrolling for the continuous horizontal reading mode
useEffect(() => {
if (readingMode.value !== ReadingMode.CONTINUOUS_HORIZONTAL) {
return () => {};
}
if (!scrollElementRef.current) {
return () => {};
}
const handleScroll = (e: WheelEvent) => {
e.preventDefault();
if (e.shiftKey) {
scrollElementRef.current?.scrollBy({
top: e.deltaY,
imageRef?.scrollIntoView({
block: 'start',
inline: getScrollIntoViewInlineOption(directionOffset, themeDirection, readingDirection),
});
return;
}
scrollElementRef.current?.scrollBy({
left: readingDirection.value === ReadingDirection.LTR ? e.deltaY : e.deltaY * -1,
});
};
const newPageIndex = getNextIndexFromPage(pageToScrollTo);
const isLastPage = newPageIndex === totalPages - 1;
scrollElementRef.current.addEventListener('wheel', handleScroll);
return () => scrollElementRef.current?.removeEventListener('wheel', handleScroll);
}, [readingMode.value, readingDirection.value]);
updateCurrentPageIndex(newPageIndex, !isLastPage);
}, [pageToScrollToIndex]);
// hide overlay on user triggered scroll
useEffect(() => {
const handleScroll = () => {
if (isOverlayVisible) {
setIsOverlayVisible(false);
// hide cursor on mouse inactivity
const mouseInactiveTimeout = useRef<NodeJS.Timeout>();
useEffect(() => {
const setCursorVisibility = (visible: boolean) => {
const scrollElement = scrollElementRef.current;
if (!scrollElement) {
return;
}
scrollElement.style.cursor = visible ? 'default' : 'none';
};
const handleMouseMove = () => {
setCursorVisibility(true);
clearTimeout(mouseInactiveTimeout.current);
mouseInactiveTimeout.current = setTimeout(() => {
setCursorVisibility(false);
}, 5000);
};
handleMouseMove();
window.addEventListener('mousemove', handleMouseMove);
return () => {
setCursorVisibility(true);
window.removeEventListener('mousemove', handleMouseMove);
clearTimeout(mouseInactiveTimeout.current);
};
}, []);
// invert x and y scrolling for the continuous horizontal reading mode
useEffect(() => {
if (readingMode !== ReadingMode.CONTINUOUS_HORIZONTAL) {
return () => {};
}
};
scrollElementRef.current?.addEventListener('wheel', handleScroll);
scrollElementRef.current?.addEventListener('touchmove', handleScroll);
return () => {
scrollElementRef.current?.removeEventListener('wheel', handleScroll);
scrollElementRef.current?.removeEventListener('touchmove', handleScroll);
};
}, [isOverlayVisible]);
return (
<Stack
ref={scrollElementRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
...applyStyles(isContinuousVerticalReadingMode(readingMode.value), { alignItems: 'center' }),
}}
onClick={(e) => !isDragging && handleClick(e)}
onScroll={() =>
ReaderControls.updateCurrentPageOnScroll(
imageRefs,
totalPages - 1,
updateCurrentPageIndex,
inViewportType,
readingDirection.value,
)
if (!scrollElementRef.current) {
return () => {};
}
>
<Pager
totalPages={totalPages}
currentPageIndex={currentPageIndex}
pages={actualPages}
transitionPageMode={transitionPageMode}
pageLoadStates={pageLoadStates}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
imageRefs={imageRefs}
onLoad={(pagesIndex, isPrimary = true) => {
const page = actualPages[pagesIndex];
const { index, url } = isPrimary ? page.primary : page.secondary!;
if (readingMode.value === ReadingMode.DOUBLE_PAGE) {
const img = new Image();
img.onload = () => {
setPagesToSpreadState((prevState) => prevState.toSpliced(index, 1, isSpreadPage(img)));
};
img.src = url;
}
const handleScroll = (e: WheelEvent) => {
e.preventDefault();
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(index, 1, { loaded: true }),
);
if (e.shiftKey) {
scrollElementRef.current?.scrollBy({
top: e.deltaY,
});
return;
}
scrollElementRef.current?.scrollBy({
left: readingDirection === ReadingDirection.LTR ? e.deltaY : e.deltaY * -1,
});
};
scrollElementRef.current.addEventListener('wheel', handleScroll);
return () => scrollElementRef.current?.removeEventListener('wheel', handleScroll);
}, [readingMode, readingDirection]);
// hide overlay on user triggered scroll
useEffect(() => {
const handleScroll = () => {
if (isOverlayVisible) {
setIsOverlayVisible(false);
}
};
scrollElementRef.current?.addEventListener('wheel', handleScroll);
scrollElementRef.current?.addEventListener('touchmove', handleScroll);
return () => {
scrollElementRef.current?.removeEventListener('wheel', handleScroll);
scrollElementRef.current?.removeEventListener('touchmove', handleScroll);
};
}, [isOverlayVisible]);
return (
<Stack
ref={scrollElementRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
...applyStyles(isContinuousVerticalReadingMode(readingMode), { alignItems: 'center' }),
}}
onError={(pageIndex) => {
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(pageIndex, 1, { loaded: false, error: true }),
);
}}
/>
</Stack>
);
});
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}
onLoad={(pagesIndex, isPrimary = true) => {
const page = actualPages[pagesIndex];
const { index, url } = isPrimary ? page.primary : page.secondary!;
if (readingMode === ReadingMode.DOUBLE_PAGE) {
const img = new Image();
img.onload = () => {
setPagesToSpreadState((prevState) => prevState.toSpliced(index, 1, isSpreadPage(img)));
};
img.src = url;
}
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(index, 1, { loaded: true }),
);
}}
onError={(pageIndex) => {
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(pageIndex, 1, { loaded: false, error: true }),
);
}}
/>
</Stack>
);
},
);
export const ReaderViewer = withPropsFrom(
BaseReaderViewer,
[
userReaderStatePagesContext,
ReaderService.useSettingsWithoutDefaultFlag,
useReaderScrollbarContext,
useReaderOverlayContext,
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
],
[
'currentPageIndex',
'pageToScrollToIndex',
'pages',
'setPages',
'totalPages',
'pageUrls',
'pageLoadStates',
'setPageLoadStates',
'transitionPageMode',
'retryFailedPagesKeyPrefix',
'readingMode',
'shouldOffsetDoubleSpreads',
'readingDirection',
'setScrollbarXSize',
'setScrollbarYSize',
'isVisible',
'setIsVisible',
'updateCurrentPageIndex',
],
);

View File

@@ -11,29 +11,36 @@ import Box, { BoxProps } from '@mui/material/Box';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { getImageWidthStyling, getPageIndexesToLoad } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { ReaderPagerProps, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts';
import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const BasePager = ({
const BaseBasePager = ({
currentPageIndex,
pages,
transitionPageMode,
imageRefs,
createPage,
slots,
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
shouldDisplay: boolean,
setRef: (element: HTMLElement | null) => void,
) => ReactNode;
slots?: { boxProps?: BoxProps };
}) => {
const { readingMode, pageScaleMode, shouldStretchPage, readerWidth, imagePreLoadAmount } =
ReaderService.useSettings();
readingMode,
pageScaleMode,
shouldStretchPage,
readerWidth,
imagePreLoadAmount,
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
Pick<
IReaderSettings,
'readingMode' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth' | 'imagePreLoadAmount'
> & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
shouldDisplay: boolean,
setRef: (element: HTMLElement | null) => void,
) => ReactNode;
slots?: { boxProps?: BoxProps };
}) => {
const previousCurrentPageIndex = useRef(-1);
const pagesIndexesToRender = useMemo(
() => getPageIndexesToLoad(currentPageIndex, pages, previousCurrentPageIndex.current, imagePreLoadAmount),
@@ -48,20 +55,14 @@ export const BasePager = ({
{...slots?.boxProps}
sx={[
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
getImageWidthStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
false,
readerWidth.value,
),
getImageWidthStyling(readingMode, shouldStretchPage, pageScaleMode, false, readerWidth),
]}
>
<ReaderTransitionPage
type={ReaderTransitionPageMode.PREVIOUS}
mode={transitionPageMode}
readingMode={readingMode.value}
pageScaleMode={pageScaleMode.value}
readingMode={readingMode}
pageScaleMode={pageScaleMode}
/>
{pages.map((page, pagesIndex) =>
createPage(
@@ -78,9 +79,15 @@ export const BasePager = ({
<ReaderTransitionPage
type={ReaderTransitionPageMode.NEXT}
mode={transitionPageMode}
readingMode={readingMode.value}
pageScaleMode={pageScaleMode.value}
readingMode={readingMode}
pageScaleMode={pageScaleMode}
/>
</Box>
);
};
export const BasePager = withPropsFrom(
BaseBasePager,
[ReaderService.useSettingsWithoutDefaultFlag],
['readingMode', 'pageScaleMode', 'shouldStretchPage', 'readerWidth', 'imagePreLoadAmount'],
);

View File

@@ -10,10 +10,11 @@ import { Direction, useTheme } from '@mui/material/styles';
import { Fragment, useMemo } from 'react';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { getNextIndexFromPage, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const getPagePosition = (
pageType: 'first' | 'second',
@@ -37,20 +38,20 @@ const getPagePosition = (
return isLtrReadingDirection ? 'right' : 'left';
};
export const ReaderDoublePagedPager = ({
const BaseReaderDoublePagedPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
readingDirection,
...props
}: ReaderPagerProps) => {
}: ReaderPagerProps & Pick<IReaderSettings, 'readingDirection'>) => {
const { currentPageIndex, pages, totalPages } = props;
const { readingDirection } = ReaderService.useSettings();
const { direction: themeDirection } = useTheme();
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
const isLtrReadingDirection = readingDirection.value === ReadingDirection.LTR;
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
return (
<BasePager
@@ -75,9 +76,7 @@ export const ReaderDoublePagedPager = ({
currentPage.primary.index,
totalPages,
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
hasSecondaryPage
? getPagePosition('first', themeDirection, readingDirection.value)
: undefined,
hasSecondaryPage ? getPagePosition('first', themeDirection, readingDirection) : undefined,
hasSecondaryPage,
)}
{hasSecondaryPage &&
@@ -90,7 +89,7 @@ export const ReaderDoublePagedPager = ({
currentSecondaryPageIndex,
totalPages,
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
getPagePosition('second', themeDirection, readingDirection.value),
getPagePosition('second', themeDirection, readingDirection),
true,
)}
</Fragment>
@@ -115,3 +114,9 @@ export const ReaderDoublePagedPager = ({
/>
);
};
export const ReaderDoublePagedPager = withPropsFrom(
BaseReaderDoublePagedPager,
[ReaderService.useSettingsWithoutDefaultFlag],
['readingDirection'],
);

View File

@@ -10,22 +10,24 @@ import { useTheme } from '@mui/material/styles';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderHorizontalPager = ({
const BaseReaderHorizontalPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
pageGap,
readingDirection,
...props
}: ReaderPagerProps) => {
}: ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>) => {
const { currentPageIndex, totalPages } = props;
const { pageGap, readingDirection } = ReaderService.useSettings();
const { direction: themeDirection } = useTheme();
const isLtrReadingDirection = readingDirection.value === ReadingDirection.LTR;
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
return (
<BasePager
@@ -53,7 +55,7 @@ export const ReaderHorizontalPager = ({
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
gap: `${pageGap.value}px`,
gap: `${pageGap}px`,
...applyStyles(themeDirection === 'ltr', {
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
justifyContent: isLtrReadingDirection ? 'flex-start' : 'flex-end',
@@ -68,3 +70,9 @@ export const ReaderHorizontalPager = ({
/>
);
};
export const ReaderHorizontalPager = withPropsFrom(
BaseReaderHorizontalPager,
[ReaderService.useSettingsWithoutDefaultFlag],
['pageGap', 'readingDirection'],
);

View File

@@ -7,22 +7,24 @@
*/
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderPagerProps, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderVerticalPager = ({
const BaseReaderVerticalPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
readingMode,
pageGap,
...props
}: ReaderPagerProps) => {
}: ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingMode'>) => {
const { currentPageIndex, totalPages } = props;
const { readingMode, pageGap } = ReaderService.useSettings();
const isWebtoonMode = readingMode.value === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap.value;
const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap;
return (
<BasePager
@@ -46,3 +48,9 @@ export const ReaderVerticalPager = ({
/>
);
};
export const ReaderVerticalPager = withPropsFrom(
BaseReaderVerticalPager,
[ReaderService.useSettingsWithoutDefaultFlag],
['pageGap', 'readingMode'],
);