Reader page rendering
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useState, CSSProperties, useEffect, forwardRef, ForwardedRef } from 'react';
|
||||
import { useState, useEffect, forwardRef, ForwardedRef } from 'react';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -18,15 +18,18 @@ import ImageIcon from '@mui/icons-material/Image';
|
||||
import { SxProps, Theme } from '@mui/material/styles';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { Priority } from '@/lib/Queue.ts';
|
||||
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
||||
|
||||
interface IProps {
|
||||
shouldLoad?: boolean;
|
||||
|
||||
src: string;
|
||||
alt: string;
|
||||
|
||||
spinnerStyle?: SxProps<Theme> & { small?: boolean };
|
||||
imgStyle?: CSSProperties;
|
||||
imgStyle?: SxProps<Theme>;
|
||||
|
||||
onImageLoad?: () => void;
|
||||
onLoad?: () => void;
|
||||
|
||||
shouldDecode?: boolean;
|
||||
useFetchApi?: boolean;
|
||||
@@ -36,11 +39,12 @@ interface IProps {
|
||||
|
||||
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
|
||||
const {
|
||||
shouldLoad = true,
|
||||
shouldDecode,
|
||||
useFetchApi,
|
||||
src,
|
||||
alt,
|
||||
onImageLoad,
|
||||
onLoad,
|
||||
spinnerStyle: { small, ...spinnerStyle } = {},
|
||||
imgStyle,
|
||||
priority,
|
||||
@@ -60,12 +64,12 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
||||
setHasError(error);
|
||||
|
||||
if (!loading && !error && !aborted) {
|
||||
onImageLoad?.();
|
||||
onLoad?.();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showMissingImageIcon) {
|
||||
if (showMissingImageIcon || !shouldLoad) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
@@ -110,12 +114,51 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
||||
clearTimeout(cacheTimeout);
|
||||
imageRequest.abortRequest(new Error('Component was unmounted'));
|
||||
};
|
||||
}, [src, imgLoadRetryKey]);
|
||||
}, [src, imgLoadRetryKey, showMissingImageIcon, shouldLoad]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoading || hasError) && (
|
||||
<Box sx={{ height: '100%', ...spinnerStyle }}>
|
||||
{showMissingImageIcon ? (
|
||||
<Stack
|
||||
ref={imgRef}
|
||||
sx={{
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
...spinnerStyle,
|
||||
}}
|
||||
>
|
||||
<ImageIcon fontSize="large" />
|
||||
</Stack>
|
||||
) : (
|
||||
<Box
|
||||
component="img"
|
||||
key={`${src}_${imgLoadRetryKey}`}
|
||||
sx={[
|
||||
...(Array.isArray(imgStyle) ? (imgStyle ?? []) : [imgStyle]),
|
||||
applyStyles(!imageSourceUrl || isLoading || hasError, {
|
||||
visibility: 'hidden',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
]}
|
||||
ref={imgRef}
|
||||
src={imageSourceUrl}
|
||||
alt={alt}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isLoading || (src && !imageSourceUrl) || hasError) && (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
...spinnerStyle,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100%',
|
||||
@@ -123,7 +166,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{isLoading && <CircularProgress thickness={5} />}
|
||||
{(isLoading || (src && !imageSourceUrl && !hasError)) && <CircularProgress thickness={5} />}
|
||||
{hasError && isLoading === false && (
|
||||
<>
|
||||
<BrokenImageIcon />
|
||||
@@ -143,32 +186,6 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showMissingImageIcon ? (
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
...spinnerStyle,
|
||||
}}
|
||||
>
|
||||
<ImageIcon fontSize="large" />
|
||||
</Stack>
|
||||
) : (
|
||||
<img
|
||||
key={`${src}_${imgLoadRetryKey}`}
|
||||
style={{
|
||||
...imgStyle,
|
||||
display: !imageSourceUrl || isLoading || hasError ? 'none' : imgStyle?.display,
|
||||
}}
|
||||
ref={imgRef}
|
||||
src={imageSourceUrl}
|
||||
alt={alt}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,16 +15,24 @@ import { AppStorage } from '@/lib/storage/AppStorage.ts';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
export class MediaQuery {
|
||||
static readonly MOBILE_WIDTH: Breakpoint | number = 'sm';
|
||||
|
||||
static readonly TABLET_WIDTH: Breakpoint | number = 1025;
|
||||
|
||||
static useIsTouchDevice(): boolean {
|
||||
return useMediaQuery('not (pointer: fine)');
|
||||
}
|
||||
|
||||
static useIsBelowWidth(breakpoint: Breakpoint): boolean {
|
||||
static useIsBelowWidth(breakpoint: Breakpoint | number): boolean {
|
||||
return useMediaQuery(getCurrentTheme().breakpoints.down(breakpoint));
|
||||
}
|
||||
|
||||
static useIsMobileWidth(): boolean {
|
||||
return this.useIsBelowWidth('sm');
|
||||
return this.useIsBelowWidth(this.MOBILE_WIDTH);
|
||||
}
|
||||
|
||||
static useIsTabletWidth(): boolean {
|
||||
return this.useIsBelowWidth(this.TABLET_WIDTH);
|
||||
}
|
||||
|
||||
private static getScrollbarSize(type: 'height' | 'width'): number {
|
||||
|
||||
@@ -164,7 +164,7 @@ const Thumbnail = ({ manga }: { manga: Partial<MangaThumbnailInfo> }) => {
|
||||
<SpinnerImage
|
||||
src={Mangas.getThumbnailUrl(manga)}
|
||||
alt="Manga Thumbnail"
|
||||
onImageLoad={() => setIsImageReady(true)}
|
||||
onLoad={() => setIsImageReady(true)}
|
||||
imgStyle={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
{isImageReady && (
|
||||
|
||||
@@ -70,6 +70,7 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
readingModesDefaultProfile: undefined,
|
||||
defaultProfile: undefined,
|
||||
hotkeys: undefined,
|
||||
imagePreLoadAmount: undefined,
|
||||
};
|
||||
|
||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
115
src/modules/reader/components/viewer/ReaderPage.tsx
Normal file
115
src/modules/reader/components/viewer/ReaderPage.tsx
Normal 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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
208
src/modules/reader/components/viewer/ReaderViewer.tsx
Normal file
208
src/modules/reader/components/viewer/ReaderViewer.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
75
src/modules/reader/components/viewer/pager/BasePager.tsx
Normal file
75
src/modules/reader/components/viewer/pager/BasePager.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -55,6 +55,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
|
||||
profiles: undefined,
|
||||
readingModesDefaultProfile: undefined,
|
||||
hotkeys: undefined,
|
||||
imagePreLoadAmount: undefined,
|
||||
};
|
||||
|
||||
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
|
||||
@@ -128,6 +129,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
[ReaderHotkey.STRETCH_IMAGE]: ['f'],
|
||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
||||
},
|
||||
imagePreLoadAmount: 5,
|
||||
};
|
||||
|
||||
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
||||
|
||||
@@ -13,6 +13,8 @@ export const ReaderStatePagesContext = createContext<ReaderStatePages>({
|
||||
totalPages: 0,
|
||||
currentPageIndex: 0,
|
||||
setCurrentPageIndex: () => undefined,
|
||||
pageToScrollToIndex: 0,
|
||||
setPageToScrollToIndex: () => undefined,
|
||||
setTotalPages: () => undefined,
|
||||
pageUrls: [],
|
||||
setPageUrls: () => undefined,
|
||||
|
||||
@@ -15,6 +15,7 @@ type TContext = ContextType<typeof ReaderStatePagesContext>;
|
||||
export const ReaderStatePagesContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [totalPages, setTotalPages] = useState<TContext['totalPages']>(0);
|
||||
const [currentPageIndex, setCurrentPageIndex] = useState<TContext['currentPageIndex']>(0);
|
||||
const [pageToScrollToIndex, setPageToScrollToIndex] = useState<TContext['pageToScrollToIndex']>(0);
|
||||
const [pageUrls, setPageUrls] = useState<TContext['pageUrls']>([]);
|
||||
const [pageLoadStates, setPageLoadStates] = useState<TContext['pageLoadStates']>([]);
|
||||
const [pages, setPages] = useState<TContext['pages']>([createPageData('', 0)]);
|
||||
@@ -25,6 +26,8 @@ export const ReaderStatePagesContextProvider = ({ children }: { children: ReactN
|
||||
setTotalPages,
|
||||
currentPageIndex,
|
||||
setCurrentPageIndex,
|
||||
pageToScrollToIndex,
|
||||
setPageToScrollToIndex,
|
||||
pageUrls,
|
||||
setPageUrls,
|
||||
pageLoadStates,
|
||||
@@ -32,7 +35,7 @@ export const ReaderStatePagesContextProvider = ({ children }: { children: ReactN
|
||||
pages,
|
||||
setPages,
|
||||
}),
|
||||
[totalPages, pages, currentPageIndex, pageUrls, pageLoadStates],
|
||||
[totalPages, pages, currentPageIndex, pageToScrollToIndex, pageUrls, pageLoadStates],
|
||||
);
|
||||
|
||||
return <ReaderStatePagesContext.Provider value={value}>{children}</ReaderStatePagesContext.Provider>;
|
||||
|
||||
@@ -6,16 +6,22 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import { Direction } from '@mui/material/styles';
|
||||
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { getNextPageIndex, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection } from '@/theme.tsx';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { PageInViewportType, ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts';
|
||||
import { ReaderScrollAmount } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { isPageInViewport } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
||||
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
|
||||
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
import { TapZoneRegionType } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
import { ReaderTapZoneService } from '@/modules/reader/services/ReaderTapZoneService.ts';
|
||||
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
|
||||
const READING_DIRECTION_TO_DIRECTION: Record<ReadingDirection, Direction> = {
|
||||
[ReadingDirection.LTR]: 'ltr',
|
||||
@@ -95,7 +101,7 @@ export class ReaderControls {
|
||||
}
|
||||
|
||||
static useOpenPage(): (offset: 'previous' | 'next') => void {
|
||||
const { currentPageIndex, setCurrentPageIndex, pages } = userReaderStatePagesContext();
|
||||
const { currentPageIndex, setPageToScrollToIndex, pages } = userReaderStatePagesContext();
|
||||
const { readingDirection } = ReaderService.useSettings();
|
||||
|
||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||
@@ -113,10 +119,10 @@ export class ReaderControls {
|
||||
(offset) => {
|
||||
switch (offset) {
|
||||
case 'previous':
|
||||
setCurrentPageIndex(getOptionForDirection(previousPageIndex, nextPageIndex, direction));
|
||||
setPageToScrollToIndex(getOptionForDirection(previousPageIndex, nextPageIndex, direction));
|
||||
break;
|
||||
case 'next':
|
||||
setCurrentPageIndex(getOptionForDirection(nextPageIndex, previousPageIndex, direction));
|
||||
setPageToScrollToIndex(getOptionForDirection(nextPageIndex, previousPageIndex, direction));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected "offset" (${offset})`);
|
||||
@@ -125,4 +131,69 @@ export class ReaderControls {
|
||||
[direction, previousPageIndex, nextPageIndex],
|
||||
);
|
||||
}
|
||||
|
||||
static updateCurrentPageOnScroll(
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>,
|
||||
currentPageIndex: number,
|
||||
setCurrentPageIndex: (index: number) => void,
|
||||
type: PageInViewportType,
|
||||
) {
|
||||
const firstVisibleImageIndex = imageRefs.current.findIndex((image) => image && isPageInViewport(image, type));
|
||||
|
||||
if (firstVisibleImageIndex !== -1 && firstVisibleImageIndex !== currentPageIndex) {
|
||||
setCurrentPageIndex(firstVisibleImageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
static useHandleClick(
|
||||
scrollElement: HTMLElement | null,
|
||||
): (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void {
|
||||
const { setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
|
||||
const { currentPageIndex, pages } = userReaderStatePagesContext();
|
||||
const { setShowPreview } = useReaderTapZoneContext();
|
||||
const { readingMode, readingDirection, isStaticNav } = ReaderService.useSettings();
|
||||
const openPage = ReaderControls.useOpenPage();
|
||||
|
||||
return useCallback(
|
||||
(e) => {
|
||||
if (!scrollElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const rectRelativeX = e.clientX - rect.left;
|
||||
const rectRelativeY = e.clientY - rect.top;
|
||||
const action = ReaderTapZoneService.getAction(rectRelativeX, rectRelativeY);
|
||||
|
||||
setShowPreview(false);
|
||||
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode.value);
|
||||
const scrollDirection =
|
||||
readingMode.value === ReadingMode.CONTINUOUS_HORIZONTAL ? ScrollDirection.X : ScrollDirection.Y;
|
||||
|
||||
switch (action) {
|
||||
case TapZoneRegionType.MENU:
|
||||
setIsOverlayVisible((isVisible) => isStaticNav || !isVisible);
|
||||
break;
|
||||
case TapZoneRegionType.PREVIOUS:
|
||||
if (isContinuousReadingModeActive) {
|
||||
this.scroll(ScrollOffset.BACKWARD, scrollDirection, readingDirection.value, scrollElement);
|
||||
} else {
|
||||
openPage('previous');
|
||||
}
|
||||
break;
|
||||
case TapZoneRegionType.NEXT:
|
||||
if (isContinuousReadingModeActive) {
|
||||
this.scroll(ScrollOffset.FORWARD, scrollDirection, readingDirection.value, scrollElement);
|
||||
} else {
|
||||
openPage('next');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected "TapZoneRegionType" (${action})`);
|
||||
}
|
||||
},
|
||||
[scrollElement, currentPageIndex, pages, readingMode.value, openPage, readingDirection.value],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MutableRefObject } from 'react';
|
||||
import { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
|
||||
export enum ProgressBarType {
|
||||
HIDDEN,
|
||||
@@ -120,6 +122,7 @@ export interface IReaderSettingsGlobal {
|
||||
profiles: string[];
|
||||
readingModesDefaultProfile: Record<ReadingMode, string>;
|
||||
hotkeys: Record<ReaderHotkey, string[]>;
|
||||
imagePreLoadAmount: number;
|
||||
}
|
||||
|
||||
export interface IReaderSettingsManga {
|
||||
@@ -193,3 +196,13 @@ export enum ReaderHotkey {
|
||||
STRETCH_IMAGE,
|
||||
OFFSET_SPREAD_PAGES,
|
||||
}
|
||||
|
||||
export interface ReaderPagerProps extends Pick<ReaderStatePages, 'currentPageIndex' | 'pages' | 'totalPages'> {
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
||||
onLoad?: (pagesIndex: number, isPrimary?: boolean) => void;
|
||||
}
|
||||
|
||||
export enum PageInViewportType {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface ReaderStatePages {
|
||||
setTotalPages: React.Dispatch<React.SetStateAction<number>>;
|
||||
currentPageIndex: number;
|
||||
setCurrentPageIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
pageToScrollToIndex: number;
|
||||
setPageToScrollToIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
pageUrls: string[];
|
||||
setPageUrls: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
pageLoadStates: boolean[];
|
||||
|
||||
369
src/modules/reader/utils/ReaderPager.utils.tsx
Normal file
369
src/modules/reader/utils/ReaderPager.utils.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* 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 { Theme } from '@mui/material/styles';
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
IReaderSettings,
|
||||
PageInViewportType,
|
||||
ReaderPageScaleMode,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
||||
import { isReaderWidthEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ReaderPage } from '@/modules/reader/components/viewer/ReaderPage.tsx';
|
||||
import { reverseString } from '@/util/Strings.ts';
|
||||
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
|
||||
type CSSObject = ReturnType<Theme['applyStyles']>;
|
||||
|
||||
const getPageWidth = (
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
isDoublePage?: boolean,
|
||||
readerWidth?: IReaderSettings['readerWidth'],
|
||||
isImage?: boolean,
|
||||
): string => {
|
||||
if (isImage && isDoublePage) {
|
||||
return '50%';
|
||||
}
|
||||
|
||||
// the image wrapper gets applied the reader width limit and therefore the images can take up 100%
|
||||
if (!isImage && readerWidth?.enabled && isReaderWidthEditable(pageScaleMode)) {
|
||||
return `${readerWidth.value}%`;
|
||||
}
|
||||
|
||||
return '100%';
|
||||
};
|
||||
|
||||
export const getImagePlaceholderStyling = (
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
readerWidth: IReaderSettings['readerWidth'],
|
||||
scrollbarXSize: number,
|
||||
scrollbarYSize: number,
|
||||
isDoublePage?: boolean,
|
||||
isTabletWidth?: boolean,
|
||||
): CSSObject => {
|
||||
const DEFAULT_SINGLE_PAGE_WIDTH = isTabletWidth ? '100vw' : '40vw';
|
||||
const DEFAULT_SINGLE_PAGE_HEIGHT = isTabletWidth ? '100vh' : '85vh';
|
||||
|
||||
const READING_MODE_TO_PLACEHOLDER_SIZE: Record<ReadingMode, { minWidth: string; minHeight: string }> = {
|
||||
[ReadingMode.SINGLE_PAGE]: { minWidth: DEFAULT_SINGLE_PAGE_WIDTH, minHeight: DEFAULT_SINGLE_PAGE_HEIGHT },
|
||||
[ReadingMode.DOUBLE_PAGE]: { minWidth: isTabletWidth ? '50vw' : '35vw', minHeight: DEFAULT_SINGLE_PAGE_HEIGHT },
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
||||
minWidth: `calc(${DEFAULT_SINGLE_PAGE_WIDTH} - ${scrollbarYSize}px)`,
|
||||
minHeight: '100vh',
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
||||
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
||||
minHeight: `calc(${DEFAULT_SINGLE_PAGE_HEIGHT} - ${scrollbarXSize}px)`,
|
||||
},
|
||||
};
|
||||
const defaultStyling = READING_MODE_TO_PLACEHOLDER_SIZE[readingMode];
|
||||
const minWidthForStretch = isDoublePage ? '50%' : '100%';
|
||||
|
||||
switch (pageScaleMode) {
|
||||
case ReaderPageScaleMode.WIDTH:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(readerWidth.enabled && isReaderWidthEditable(pageScaleMode), {
|
||||
minWidth: minWidthForStretch,
|
||||
}),
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: minWidthForStretch,
|
||||
}),
|
||||
};
|
||||
case ReaderPageScaleMode.HEIGHT:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_VERTICAL, {
|
||||
minHeight: '100vh',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
case ReaderPageScaleMode.SCREEN:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(readerWidth.enabled && isReaderWidthEditable(pageScaleMode), {
|
||||
minWidth: minWidthForStretch,
|
||||
}),
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: minWidthForStretch,
|
||||
minHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_VERTICAL, {
|
||||
minHeight: '100vh',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
case ReaderPageScaleMode.ORIGINAL:
|
||||
return defaultStyling;
|
||||
default:
|
||||
throw new Error(`Unexpected "PageScaleMode" (${pageScaleMode})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getImageWidthStyling = (
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
isDoublePage?: boolean,
|
||||
readerWidth?: IReaderSettings['readerWidth'],
|
||||
isImage?: boolean,
|
||||
): CSSObject => {
|
||||
const width = getPageWidth(pageScaleMode, isDoublePage, readerWidth, isImage);
|
||||
|
||||
// setting the "width" of the wrapper is required for being able to properly size the image placeholders
|
||||
const staticReaderWidthForWrapper = applyStyles(
|
||||
!isImage && !!readerWidth?.enabled && isReaderWidthEditable(pageScaleMode),
|
||||
{ width },
|
||||
);
|
||||
const readerWidthStretchForWrapper = applyStyles(!isImage && shouldStretchPage, { width: '100%' });
|
||||
|
||||
switch (pageScaleMode) {
|
||||
case ReaderPageScaleMode.WIDTH:
|
||||
return {
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: width,
|
||||
...readerWidthStretchForWrapper,
|
||||
}),
|
||||
...staticReaderWidthForWrapper,
|
||||
maxWidth: width,
|
||||
};
|
||||
case ReaderPageScaleMode.HEIGHT:
|
||||
return {
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minHeight: '100%',
|
||||
}),
|
||||
maxHeight: `100%`,
|
||||
};
|
||||
case ReaderPageScaleMode.SCREEN:
|
||||
return {
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: width,
|
||||
minHeight: '100%',
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
minWidth: 'unset',
|
||||
minHeight: '100%',
|
||||
}),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_VERTICAL, {
|
||||
minWidth: width,
|
||||
minHeight: 'unset',
|
||||
}),
|
||||
...readerWidthStretchForWrapper,
|
||||
}),
|
||||
...staticReaderWidthForWrapper,
|
||||
maxWidth: width,
|
||||
maxHeight: `100%`,
|
||||
};
|
||||
case ReaderPageScaleMode.ORIGINAL:
|
||||
return {};
|
||||
default:
|
||||
throw new Error(`Unexpected "PageScaleMode" (${pageScaleMode})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const createSinglePageData = (url: string, index: number): ReaderStatePages['pages'][number]['primary'] => ({
|
||||
index,
|
||||
alt: `Page #${index + 1}`,
|
||||
url: `${requestManager.getBaseUrl()}${url}`,
|
||||
});
|
||||
|
||||
export const createPageData = (url: string, index: number): ReaderStatePages['pages'][number] => ({
|
||||
name: `${index + 1}`,
|
||||
primary: createSinglePageData(url, index),
|
||||
});
|
||||
|
||||
export const createPagesData = (pageUrls: string[]): ReaderStatePages['pages'] => pageUrls.map(createPageData);
|
||||
|
||||
const getPageDownloadPriority = (currentPageIndex: number, pageIndex: number, totalPages: number): number => {
|
||||
const distanceToCurrentPage = Math.abs(pageIndex - currentPageIndex);
|
||||
const priorityBasedOnDistance = totalPages - distanceToCurrentPage;
|
||||
|
||||
const isPreviousPage = pageIndex < currentPageIndex;
|
||||
if (isPreviousPage) {
|
||||
return priorityBasedOnDistance - 1;
|
||||
}
|
||||
|
||||
return priorityBasedOnDistance;
|
||||
};
|
||||
|
||||
export const createReaderPage = (
|
||||
{ primary: { index, alt, url } }: ReaderStatePages['pages'][number],
|
||||
onLoad: () => void,
|
||||
shouldLoad: boolean,
|
||||
display: boolean,
|
||||
currentPageIndex: number,
|
||||
totalPages: number,
|
||||
position?: 'left' | 'right',
|
||||
isDoublePage?: boolean,
|
||||
setRef?: (ref: HTMLElement | null) => void,
|
||||
): ReactNode => (
|
||||
<ReaderPage
|
||||
ref={setRef}
|
||||
key={url}
|
||||
src={url}
|
||||
alt={alt}
|
||||
display={display}
|
||||
priority={getPageDownloadPriority(currentPageIndex, index, totalPages)}
|
||||
position={position}
|
||||
onLoad={onLoad}
|
||||
doublePage={isDoublePage}
|
||||
shouldLoad={shouldLoad}
|
||||
/>
|
||||
);
|
||||
|
||||
export const isPageInViewport = (element: HTMLElement, type: PageInViewportType): boolean => {
|
||||
const { top, bottom, left, right } = element.getBoundingClientRect();
|
||||
|
||||
// for some reason using "scrollIntoView" to scroll a page into view does not always result in the page to be at top,
|
||||
// left/right 0px and instead at something like -0.25px
|
||||
const MIN_VISIBLE_PX = 1;
|
||||
|
||||
const isLeftInViewport = left >= MIN_VISIBLE_PX && left <= window.innerWidth;
|
||||
const isRightInViewport = right >= MIN_VISIBLE_PX && right <= window.innerWidth;
|
||||
const isFillingWidthViewportCompletely = left <= MIN_VISIBLE_PX && right >= window.innerWidth;
|
||||
|
||||
const isTopInViewport = top >= MIN_VISIBLE_PX && top <= window.innerHeight;
|
||||
const isBottomInViewport = bottom >= MIN_VISIBLE_PX && bottom <= window.innerHeight;
|
||||
const isFillingHeightViewportCompletely = top <= MIN_VISIBLE_PX && bottom >= window.innerHeight;
|
||||
|
||||
const isInViewportX = isLeftInViewport || isRightInViewport || isFillingWidthViewportCompletely;
|
||||
const isInViewportY = isTopInViewport || isBottomInViewport || isFillingHeightViewportCompletely;
|
||||
|
||||
switch (type) {
|
||||
case PageInViewportType.X:
|
||||
return isInViewportX;
|
||||
case PageInViewportType.Y:
|
||||
return isInViewportY;
|
||||
default:
|
||||
throw new Error(`unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDoublePageModePages = (
|
||||
pageUrls: ReaderStatePages['pageUrls'],
|
||||
pagesToSpreadState: boolean[],
|
||||
shouldOffsetDoubleSpreads: IReaderSettings['shouldOffsetDoubleSpreads'],
|
||||
direction: ReadingDirection,
|
||||
): ReaderStatePages['pages'] => {
|
||||
const doublePageModePages: ReaderStatePages['pages'] = [];
|
||||
|
||||
// each spread page has to be counted as 2 pages and all trailing page numbers have to be increased by the count
|
||||
// of leading spread pages
|
||||
const pageToActualPageIndex = pagesToSpreadState.map(
|
||||
(_, page) => page + pagesToSpreadState.slice(0, page).filter(Boolean).length,
|
||||
);
|
||||
|
||||
pageUrls.forEach((url, index) => {
|
||||
/*
|
||||
|
||||
| = page separator
|
||||
+ = double page
|
||||
_ = double spread
|
||||
|
||||
without double spreads:
|
||||
without double spread offset: | 0 + 1 | 2 + 3 | 4 + 5 | 6 + 7 | 8 |
|
||||
with double spread offset : | 0 | 1 + 2 | 3 + 4 | 5 + 6 | 7 + 8 |
|
||||
|
||||
with double spreads
|
||||
to handle double spreads:
|
||||
each double spread has to count as 2 pages, thus, each page number after a double spread has to increase
|
||||
by the number of leading double spreads
|
||||
|
||||
without double spread offset: | 0 + 1 | 2 | _3/4_ | 5 | 6 + 7 | 8 | _9/10_ | 11 | 12 + 13 | 14 |
|
||||
with double spread offset : | 0 | 1 + 2 | _3/4_ | 5 + 6 | 7 + 8 | _9/10_ | 11 + 12 | 13 + 14 |
|
||||
|
||||
thus, to get the second page:
|
||||
without offset: second page = current page number even ? +1 : -1
|
||||
with offset : second page = current page number even ? -1 : +1
|
||||
|
||||
the second page has to be ignored in case:
|
||||
- double spreads are offset, and the current page is the first page
|
||||
- either the current or second page is a double spread
|
||||
*/
|
||||
|
||||
const normalizedIndex = pageToActualPageIndex[index];
|
||||
const isCurrentPageEven = !(normalizedIndex % 2);
|
||||
|
||||
const secondPageOffset = (() => {
|
||||
const invert = shouldOffsetDoubleSpreads ? -1 : 1;
|
||||
const offset = isCurrentPageEven ? 1 : -1;
|
||||
|
||||
return offset * invert;
|
||||
})();
|
||||
|
||||
const secondPageIndex = index + secondPageOffset;
|
||||
|
||||
const isPrimaryPage = index < secondPageIndex;
|
||||
const isFirstPage = index === 0;
|
||||
const isLastPage = index === pageUrls.length - 1;
|
||||
const hasSecondPage = isPrimaryPage && !isLastPage;
|
||||
|
||||
const isPrimaryPageSpreadPage = pagesToSpreadState[index];
|
||||
const isSecondPageSpreadPage = pagesToSpreadState[secondPageIndex];
|
||||
const hasSpreadPage = isPrimaryPageSpreadPage || isSecondPageSpreadPage;
|
||||
|
||||
const ignoreSecondPageDueToOffset = isFirstPage && shouldOffsetDoubleSpreads;
|
||||
const displaySecondPage = !hasSpreadPage && !ignoreSecondPageDueToOffset;
|
||||
|
||||
if (!isPrimaryPage && displaySecondPage) {
|
||||
const doublePageModePage = doublePageModePages[doublePageModePages.length - 1];
|
||||
|
||||
doublePageModePages[doublePageModePages.length - 1] = {
|
||||
...doublePageModePage,
|
||||
secondary: createSinglePageData(url, index),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const SEPARATOR = '-';
|
||||
const pageName = `${index + 1}${hasSecondPage && displaySecondPage ? `${SEPARATOR}${secondPageIndex + 1}` : ''}`;
|
||||
|
||||
doublePageModePages.push({
|
||||
name: direction === ReadingDirection.LTR ? pageName : reverseString(pageName, SEPARATOR),
|
||||
primary: createSinglePageData(url, index),
|
||||
});
|
||||
});
|
||||
|
||||
return doublePageModePages;
|
||||
};
|
||||
|
||||
export const isSpreadPage = (image: HTMLImageElement): boolean => {
|
||||
const aspectRatio = image.height / image.width;
|
||||
return aspectRatio < 1;
|
||||
};
|
||||
|
||||
const PREVIOUS_IMAGE_LOAD_AMOUNT = 2;
|
||||
export const getPageIndexesToLoad = (
|
||||
currentPageIndex: number,
|
||||
pages: ReaderStatePages['pages'],
|
||||
previousCurrentPageIndex: number,
|
||||
imagePreLoadAmount: number,
|
||||
): number[] => {
|
||||
const currentPagesIndex = getPage(currentPageIndex, pages).pagesIndex;
|
||||
|
||||
const directionInvert = previousCurrentPageIndex <= currentPageIndex ? 1 : -1;
|
||||
// load at most PREVIOUS_IMAGE_LOAD_AMOUNT of the previous pages to ensure that you do not have to wait too long
|
||||
// when going back to the previous pages
|
||||
const startPagesIndex = Math.max(
|
||||
0,
|
||||
currentPagesIndex - Math.min(PREVIOUS_IMAGE_LOAD_AMOUNT, imagePreLoadAmount) * directionInvert,
|
||||
);
|
||||
const endPagesIndex = currentPagesIndex + imagePreLoadAmount * directionInvert;
|
||||
const pagesToRenderLength = Math.abs(endPagesIndex - startPagesIndex) + 1;
|
||||
|
||||
return Array(pagesToRenderLength)
|
||||
.fill(1)
|
||||
.map((_, index) => startPagesIndex + index * directionInvert);
|
||||
};
|
||||
@@ -85,7 +85,7 @@ export const useHandleProgressDragging = (
|
||||
progressBarRef: RefObject<HTMLDivElement | null>,
|
||||
isDragging: boolean,
|
||||
currentPage: TReaderProgressCurrentPage,
|
||||
setCurrentPageIndex: (pageIndex: number) => void,
|
||||
setPageToScrollToIndex: (pageIndex: number) => void,
|
||||
pages: ReaderProgressBarProps['pages'],
|
||||
progressBarPosition: ProgressBarPosition,
|
||||
getOptionForDirection: typeof getOptionForDirectionImpl,
|
||||
@@ -115,7 +115,7 @@ export const useHandleProgressDragging = (
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentPageIndex(newPageIndex);
|
||||
setPageToScrollToIndex(newPageIndex);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
||||
import { IReaderSettings, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { IReaderSettings, ReaderPageScaleMode, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { DEFAULT_READER_PROFILE } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
|
||||
@@ -31,3 +31,9 @@ export const createProfileValueToDisplayData = (
|
||||
|
||||
export const getValidReaderProfile = (profile: string, profiles: string[]): string =>
|
||||
profiles.includes(profile) ? profile : DEFAULT_READER_PROFILE;
|
||||
|
||||
export const isReaderWidthEditable = (pageScaleMode: IReaderSettings['pageScaleMode']): boolean =>
|
||||
[ReaderPageScaleMode.WIDTH, ReaderPageScaleMode.SCREEN].includes(pageScaleMode);
|
||||
|
||||
export const isContinuousReadingMode = (readingMode: IReaderSettings['readingMode']): boolean =>
|
||||
[ReadingMode.CONTINUOUS_VERTICAL, ReadingMode.CONTINUOUS_HORIZONTAL].includes(readingMode);
|
||||
|
||||
@@ -12,3 +12,6 @@ export const enhancedCleanup = (str: string): string =>
|
||||
baseCleanup(str)
|
||||
.replace(/[^a-zA-Z0-9\\s]+/g, ' ')
|
||||
.trim();
|
||||
|
||||
export const reverseString = (str: string, separator: string = ''): string =>
|
||||
str.split(separator).reverse().join(separator);
|
||||
|
||||
Reference in New Issue
Block a user