Reader page rendering

This commit is contained in:
schroda
2024-11-06 13:00:36 +01:00
parent 0b1c67ce9f
commit 7cf78e771a
28 changed files with 1243 additions and 69 deletions

View File

@@ -11,12 +11,13 @@ import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ProgressBarType } from '@/modules/reader/types/Reader.types.ts';
import { ProgressBarType, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { reverseString } from '@/util/Strings.ts';
export const ReaderPageNumber = () => {
const { isDesktop } = ReaderService.useOverlayMode();
@@ -24,9 +25,15 @@ export const ReaderPageNumber = () => {
const { readerNavBarWidth } = useNavBarContext();
const { isMaximized } = useReaderProgressBarContext();
const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext();
const { progressBarType, shouldShowPageNumber } = ReaderService.useSettings();
const { progressBarType, shouldShowPageNumber, readingDirection } = ReaderService.useSettings();
const pageName = useMemo(() => getPage(currentPageIndex, pages).name, [currentPageIndex, pages]);
const pageName = useMemo(() => {
const currentPageName = getPage(currentPageIndex, pages).name;
const SEPARATOR = '/';
const tmpPageName = `${currentPageName}${SEPARATOR}${totalPages}`;
return readingDirection.value === ReadingDirection.LTR ? tmpPageName : reverseString(tmpPageName, SEPARATOR);
}, [currentPageIndex, pages, totalPages, readingDirection.value]);
if (!shouldShowPageNumber) {
return null;
@@ -62,9 +69,7 @@ export const ReaderPageNumber = () => {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
}}
>
<Typography sx={{ color: 'white' }}>
{pageName}/{totalPages}
</Typography>
<Typography sx={{ color: 'white' }}>{pageName}</Typography>
</Box>
</Stack>
);

View File

@@ -57,7 +57,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
const { setReaderNavBarWidth } = useNavBarContext();
const { manga } = useReaderStateMangaContext();
const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
const { pages, currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext();
const { pages, currentPageIndex, setPageToScrollToIndex } = userReaderStatePagesContext();
const getOptionForDirection = useGetOptionForDirection();
@@ -128,7 +128,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
<Stack sx={{ gap: 1 }}>
<ReaderNavBarDesktopPageNavigation
currentPageIndex={currentPageIndex}
setCurrentPageIndex={setCurrentPageIndex}
setPageToScrollToIndex={setPageToScrollToIndex}
pages={pages}
/>
<ReaderNavBarDesktopChapterNavigation

View File

@@ -23,9 +23,9 @@ import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants
export const ReaderNavBarDesktopPageNavigation = ({
currentPageIndex,
setCurrentPageIndex,
setPageToScrollToIndex,
pages,
}: Pick<ReaderStatePages, 'currentPageIndex' | 'setCurrentPageIndex' | 'pages'>) => {
}: Pick<ReaderStatePages, 'currentPageIndex' | 'setPageToScrollToIndex' | 'pages'>) => {
const { t } = useTranslation();
const openPage = ReaderControls.useOpenPage();
const { readingDirection } = ReaderService.useSettings();
@@ -52,7 +52,7 @@ export const ReaderNavBarDesktopPageNavigation = ({
labelId="reader-nav-bar-desktop-page-select"
label={t('reader.page_info.label.page')}
value={currentPage.primary.index}
onChange={(e) => setCurrentPageIndex(e.target.value as number)}
onChange={(e) => setPageToScrollToIndex(e.target.value as number)}
>
{pages.map(({ primary: { index }, name }) => (
<MenuItem key={index} value={index}>

View File

@@ -35,7 +35,7 @@ export const ReaderProgressBar = ({
pages,
pageLoadStates,
currentPageIndex,
setCurrentPageIndex,
setPageToScrollToIndex,
slotProps,
slots,
createProgressBarSlot,
@@ -81,7 +81,7 @@ export const ReaderProgressBar = ({
progressBarRef,
isDragging,
currentPage,
setCurrentPageIndex,
setPageToScrollToIndex,
pages,
progressBarPosition,
getOptionForDirection,
@@ -101,7 +101,7 @@ export const ReaderProgressBar = ({
? (slotProps?.progressBarPageTexts?.current?.sx ?? [])
: [slotProps?.progressBarPageTexts?.current?.sx]),
]}
onClick={() => setCurrentPageIndex(getNextPageIndex('previous', currentPage.pagesIndex, pages))}
onClick={() => setPageToScrollToIndex(getNextPageIndex('previous', currentPage.pagesIndex, pages))}
>
{currentPage.name}
</ReaderProgressBarPageNumber>
@@ -115,7 +115,7 @@ export const ReaderProgressBar = ({
return;
}
setCurrentPageIndex(
setPageToScrollToIndex(
getPageForMousePos(
event.touches[0],
progressBarRef.current.getBoundingClientRect(),
@@ -132,7 +132,7 @@ export const ReaderProgressBar = ({
return;
}
setCurrentPageIndex(
setPageToScrollToIndex(
getPageForMousePos(
event,
progressBarRef.current.getBoundingClientRect(),
@@ -195,7 +195,7 @@ export const ReaderProgressBar = ({
? (slotProps?.progressBarPageTexts?.total?.sx ?? [])
: [slotProps?.progressBarPageTexts?.total?.sx]),
]}
onClick={() => setCurrentPageIndex(getNextPageIndex('next', currentPage.pagesIndex, pages))}
onClick={() => setPageToScrollToIndex(getNextPageIndex('next', currentPage.pagesIndex, pages))}
>
{totalPages}
</ReaderProgressBarPageNumber>

View File

@@ -13,6 +13,8 @@ import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.ty
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
import { ReaderSettingExitMode } from '@/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx';
import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
export const ReaderBehaviourSettings = ({
settings,
@@ -43,6 +45,25 @@ export const ReaderBehaviourSettings = ({
onChange={(_, checked) => updateSetting('shouldOffsetDoubleSpreads', checked)}
/>
)}
<SliderInput
label={t('reader.settings.image_preload_amount')}
value={settings.imagePreLoadAmount}
slotProps={{
slider: {
defaultValue: DEFAULT_READER_SETTINGS.imagePreLoadAmount,
value: settings.imagePreLoadAmount,
step: 1,
min: 1,
max: 20,
onChange: (_, value) => {
updateSetting('imagePreLoadAmount', value as number, false);
},
onChangeCommitted: (_, value) => {
updateSetting('imagePreLoadAmount', value as number, true);
},
},
}}
/>
</Stack>
);
};

View File

@@ -7,9 +7,10 @@
*/
import { useTranslation } from 'react-i18next';
import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.types.ts';
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
export const ReaderSettingPageGap = ({
pageGap,
@@ -20,9 +21,7 @@ export const ReaderSettingPageGap = ({
}) => {
const { t } = useTranslation();
const isChangeable = [ReadingMode.CONTINUOUS_HORIZONTAL, ReadingMode.CONTINUOUS_VERTICAL].includes(
readingMode.value,
);
const isChangeable = isContinuousReadingMode(readingMode.value);
if (!isChangeable) {
return null;
}

View File

@@ -0,0 +1,115 @@
/*
* 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 { 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 { getImagePlaceholderStyling, getImageWidthStyling } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
Object.keys(customFilter)
.map((key) => {
const filter = key as keyof ReaderCustomFilter;
const value = customFilter[filter];
switch (filter) {
case 'brightness':
case 'contrast':
case 'saturate':
return (value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).enabled
? `${filter}(${(value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).value / 100})`
: '';
case 'hue':
return (value as ReaderCustomFilter['hue']).enabled
? `hue-rotate(${(value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).value}deg)`
: '';
case 'rgba':
return '';
case 'sepia':
case 'grayscale':
case 'invert':
return value ? `${filter}(${Number(value)})` : '';
default:
throw new Error(`Unexpected "CustomFilter" (${filter})`);
}
})
.join(' ');
export const ReaderPage = forwardRef(
(
{
display,
doublePage = false,
position,
shouldLoad,
...props
}: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle'> & {
display: boolean;
doublePage?: boolean;
position?: 'left' | 'right';
onLoad?: () => void;
},
ref: ForwardedRef<HTMLImageElement | null>,
) => {
const { readingMode, customFilter, pageScaleMode, shouldStretchPage, readerWidth } =
ReaderService.useSettings();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const isTabletWidth = MediaQuery.useIsTabletWidth();
if (!display && !shouldLoad) {
return null;
}
return (
<SpinnerImage
{...props}
shouldLoad={shouldLoad}
shouldDecode
ref={ref}
spinnerStyle={{
backgroundColor: 'background.paper',
...getImagePlaceholderStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
readerWidth.value,
scrollbarXSize,
scrollbarYSize,
doublePage,
isTabletWidth,
),
...applyStyles(!display, {
display: 'none',
}),
}}
imgStyle={{
...getImageWidthStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
doublePage,
readerWidth.value,
true,
),
display: 'block',
...applyStyles(!display, {
display: 'none',
}),
filter: getCustomFilterString(customFilter),
objectFit: 'contain',
objectPosition: position,
userSelect: 'none',
}}
/>
);
},
);

View File

@@ -0,0 +1,208 @@
/*
* 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,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import Stack from '@mui/material/Stack';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { PageInViewportType, ReadingDirection, ReadingMode } 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';
import { ReaderVerticalPager } from '@/modules/reader/components/viewer/pager/ReaderVerticalPager.tsx';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { createPagesData, getDoublePageModePages, isSpreadPage } from '@/modules/reader/utils/ReaderPager.utils.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 { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
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,
};
export const ReaderViewer = forwardRef((_, ref: ForwardedRef<HTMLDivElement | null>) => {
const {
currentPageIndex,
setCurrentPageIndex,
pageToScrollToIndex,
pages,
setPages,
totalPages,
pageUrls,
setPageLoadStates,
} = userReaderStatePagesContext();
const { readingMode, shouldOffsetDoubleSpreads, readingDirection } = ReaderService.useSettings();
const { setScrollbarXSize, setScrollbarYSize } = useReaderScrollbarContext();
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 handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
const [wasDoublePageMode, setWasDoublePageMode] = useState(readingMode.value === 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.value === ReadingMode.DOUBLE_PAGE) {
return getDoublePageModePages(
pageUrls,
pagesToSpreadState,
shouldOffsetDoubleSpreads.value,
readingDirection.value,
);
}
return pages;
}, [pagesToSpreadState, readingMode.value, shouldOffsetDoubleSpreads.value, readingDirection.value]);
const Pager = useMemo(() => {
switch (readingMode.value) {
case ReadingMode.SINGLE_PAGE:
return ReaderPagedPager;
case ReadingMode.DOUBLE_PAGE:
return ReaderDoublePagedPager;
case ReadingMode.CONTINUOUS_VERTICAL:
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));
setPagesToSpreadState(Array(totalPages).fill(false));
return;
}
const convertPagesToDoublePageMode = readingMode.value === ReadingMode.DOUBLE_PAGE;
if (convertPagesToDoublePageMode) {
setPages(actualPages);
setWasDoublePageMode(readingMode.value === ReadingMode.DOUBLE_PAGE);
}
}, [actualPages, readingMode.value]);
// optionally scroll selected page into view in continues reading modes
useLayoutEffect(() => {
if (isContinuousReadingMode(readingMode.value)) {
const pageToScrollToPagesIndex = getPage(pageToScrollToIndex, pages).pagesIndex;
const imageRef = imageRefs.current[pageToScrollToPagesIndex];
imageRef?.scrollIntoView({
block: 'start',
inline: 'start',
});
setCurrentPageIndex(pageToScrollToPagesIndex);
}
}, [pageToScrollToIndex]);
// 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,
});
return;
}
scrollElementRef.current?.scrollBy({
left: readingDirection.value === ReadingDirection.LTR ? e.deltaY : e.deltaY * -1,
});
};
scrollElementRef.current.addEventListener('wheel', handleScroll);
return () => scrollElementRef.current?.removeEventListener('wheel', handleScroll);
}, [readingMode.value, readingDirection.value]);
return (
<Stack
ref={scrollElementRef}
sx={{ width: '100%', height: '100%', overflow: 'auto' }}
onClick={handleClick}
onScroll={() =>
ReaderControls.updateCurrentPageOnScroll(
imageRefs,
currentPageIndex,
setCurrentPageIndex,
inViewportType,
)
}
>
<Pager
totalPages={totalPages}
currentPageIndex={currentPageIndex}
pages={actualPages}
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;
}
setPageLoadStates((pageLoadStates) => pageLoadStates.toSpliced(index, 1, true));
}}
/>
</Stack>
);
});

View File

@@ -0,0 +1,75 @@
/*
* 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 Stack, { StackProps } from '@mui/material/Stack';
import { ReactNode, useEffect, useMemo, useRef } from 'react';
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 { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { isContinuousReadingMode, isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { ReaderPagerProps } from '@/modules/reader/types/Reader.types.ts';
export const BasePager = ({
currentPageIndex,
pages,
imageRefs,
createPage,
slots,
}: ReaderPagerProps & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
setRef: (element: HTMLElement | null) => void,
) => ReactNode;
slots?: { stackProps?: StackProps };
}) => {
const { readingMode, pageScaleMode, shouldStretchPage, readerWidth, imagePreLoadAmount } =
ReaderService.useSettings();
const previousCurrentPageIndex = useRef(-1);
const pagesIndexesToRender = useMemo(
() => getPageIndexesToLoad(currentPageIndex, pages, previousCurrentPageIndex.current, imagePreLoadAmount),
[currentPageIndex, pages, imagePreLoadAmount],
);
useEffect(() => {
previousCurrentPageIndex.current = currentPageIndex;
}, [pagesIndexesToRender]);
return (
<Stack
{...slots?.stackProps}
sx={[
...(Array.isArray(slots?.stackProps?.sx) ? (slots?.stackProps?.sx ?? []) : [slots?.stackProps?.sx]),
getImageWidthStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
false,
readerWidth.value,
),
applyStyles(!!readerWidth?.value.enabled && isReaderWidthEditable(pageScaleMode.value), {
alignItems: 'center',
// both continuous pagers have content that causes scrollbars, centering this content causes content
// to be cut off
...applyStyles(!isContinuousReadingMode(readingMode.value), {
justifyContent: 'center',
}),
}),
]}
>
{pages.map((page, pagesIndex) =>
createPage(page, pagesIndex, pagesIndexesToRender.includes(pagesIndex), (element) => {
// eslint-disable-next-line no-param-reassign
imageRefs.current[pagesIndex] = element;
}),
)}
</Stack>
);
};

View File

@@ -0,0 +1,108 @@
/*
* 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 { 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 { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
const getPagePosition = (
pageType: 'first' | 'second',
themeDirection: Direction,
readingDirection: ReadingDirection,
): 'left' | 'right' => {
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
if (pageType === 'first') {
if (themeDirection === 'ltr') {
return isLtrReadingDirection ? 'right' : 'left';
}
return isLtrReadingDirection ? 'left' : 'right';
}
if (themeDirection === 'ltr') {
return isLtrReadingDirection ? 'left' : 'right';
}
return isLtrReadingDirection ? 'right' : 'left';
};
export const ReaderDoublePagedPager = ({ onLoad, ...props }: ReaderPagerProps) => {
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;
return (
<BasePager
{...props}
createPage={(page, pagesIndex, shouldLoad) => {
const { primary, secondary } = page;
const currentSecondaryPageIndex = currentPage.secondary?.index ?? currentPage.primary.index;
const hasSecondaryPage = !!secondary;
const isPrimaryPage = currentPage.primary.index === primary.index;
const isSecondaryPage = !!secondary && currentSecondaryPageIndex === secondary.index;
return (
<Fragment key={`${primary.url}_${secondary?.url}`}>
{createReaderPage(
page,
() => onLoad?.(pagesIndex),
shouldLoad,
isPrimaryPage,
currentPage.primary.index,
totalPages,
hasSecondaryPage
? getPagePosition('first', themeDirection, readingDirection.value)
: undefined,
hasSecondaryPage,
)}
{hasSecondaryPage &&
createReaderPage(
{ ...page, primary: { ...page.secondary! } },
() => onLoad?.(pagesIndex, false),
shouldLoad,
isSecondaryPage,
currentSecondaryPageIndex,
totalPages,
getPagePosition('second', themeDirection, readingDirection.value),
true,
)}
</Fragment>
);
}}
slots={{
stackProps: {
sx: {
margin: 'auto',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
...applyStyles(themeDirection === 'ltr', {
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
}),
...applyStyles(themeDirection === 'rtl', {
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
}),
flexWrap: 'nowrap',
},
},
}}
/>
);
};

View File

@@ -0,0 +1,60 @@
/*
* 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 { 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 { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
export const ReaderHorizontalPager = ({ onLoad, ...props }: ReaderPagerProps) => {
const { currentPageIndex, totalPages } = props;
const { pageGap, readingDirection } = ReaderService.useSettings();
const { direction: themeDirection } = useTheme();
const isLtrReadingDirection = readingDirection.value === ReadingDirection.LTR;
return (
<BasePager
{...props}
createPage={(page, pagesIndex, shouldLoad, setRef) =>
createReaderPage(
page,
() => onLoad?.(pagesIndex),
shouldLoad,
true,
currentPageIndex,
totalPages,
undefined,
undefined,
setRef,
)
}
slots={{
stackProps: {
sx: {
my: 'auto',
...applyStyles(themeDirection === 'ltr', {
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
justifyContent: isLtrReadingDirection ? 'flex-start' : 'flex-end',
}),
...applyStyles(themeDirection === 'rtl', {
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
justifyContent: isLtrReadingDirection ? 'flex-end' : 'flex-start',
}),
flexWrap: 'nowrap',
alignItems: 'center',
gap: `${pageGap.value}px`,
},
},
}}
/>
);
};

View File

@@ -0,0 +1,38 @@
/*
* 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 { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderPagerProps } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
export const ReaderPagedPager = ({ onLoad, ...props }: ReaderPagerProps) => {
const { currentPageIndex, totalPages } = props;
return (
<BasePager
{...props}
createPage={(page, pagesIndex, shouldLoad) =>
createReaderPage(
page,
() => onLoad?.(pagesIndex),
shouldLoad,
currentPageIndex === page.primary.index,
currentPageIndex,
totalPages,
)
}
slots={{
stackProps: {
sx: {
margin: 'auto',
},
},
}}
/>
);
};

View File

@@ -0,0 +1,47 @@
/*
* 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 { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderPagerProps } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
export const ReaderVerticalPager = ({ onLoad, ...props }: ReaderPagerProps) => {
const { currentPageIndex, totalPages } = props;
const { pageGap } = ReaderService.useSettings();
return (
<BasePager
{...props}
createPage={(page, pagesIndex, shouldLoad, setRef) =>
createReaderPage(
page,
() => onLoad?.(pagesIndex),
shouldLoad,
true,
currentPageIndex,
totalPages,
undefined,
undefined,
setRef,
)
}
slots={{
stackProps: {
sx: {
margin: 'auto',
flexWrap: 'nowrap',
alignItems: 'center',
gap: `${pageGap.value}px`,
},
},
}}
/>
);
};