2024-11-06 13:00:36 +01:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
|
|
|
|
|
2024-12-09 15:07:52 +01:00
|
|
|
import { Direction, Theme } from '@mui/material/styles';
|
2024-12-22 00:42:33 +01:00
|
|
|
import { ComponentProps, ReactNode } from 'react';
|
2024-11-06 13:00:36 +01:00
|
|
|
import {
|
|
|
|
|
IReaderSettings,
|
|
|
|
|
PageInViewportType,
|
|
|
|
|
ReaderPageScaleMode,
|
2024-12-30 21:02:12 +01:00
|
|
|
ReaderPageSpreadState,
|
2024-12-03 18:00:46 +01:00
|
|
|
ReaderTransitionPageMode,
|
2024-12-07 03:43:18 +01:00
|
|
|
ReadingDirection,
|
2024-11-06 13:00:36 +01:00
|
|
|
ReadingMode,
|
|
|
|
|
} from '@/modules/reader/types/Reader.types.ts';
|
|
|
|
|
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
2024-12-16 16:26:32 +01:00
|
|
|
import {
|
2025-01-20 01:29:04 +01:00
|
|
|
getSetReaderWidth,
|
2024-12-16 16:26:32 +01:00
|
|
|
isContinuousReadingMode,
|
|
|
|
|
isContinuousVerticalReadingMode,
|
2025-01-09 02:11:44 +01:00
|
|
|
shouldApplyReaderWidth,
|
2024-12-16 16:26:32 +01:00
|
|
|
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
2024-11-06 13:00:36 +01:00
|
|
|
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';
|
2024-12-09 15:07:52 +01:00
|
|
|
import { DirectionOffset } from '@/Base.types.ts';
|
2024-12-11 13:59:50 +01:00
|
|
|
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
|
2024-12-09 15:07:52 +01:00
|
|
|
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
2025-01-20 01:29:04 +01:00
|
|
|
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
2024-11-06 13:00:36 +01:00
|
|
|
|
|
|
|
|
type CSSObject = ReturnType<Theme['applyStyles']>;
|
|
|
|
|
|
|
|
|
|
const getPageWidth = (
|
|
|
|
|
pageScaleMode: IReaderSettings['pageScaleMode'],
|
2025-01-19 13:21:46 +01:00
|
|
|
isDoublePage: boolean,
|
|
|
|
|
readerWidth: IReaderSettings['readerWidth'],
|
|
|
|
|
isImage: boolean,
|
2024-11-06 13:00:36 +01:00
|
|
|
): string => {
|
2025-01-19 20:19:45 +01:00
|
|
|
if (shouldApplyReaderWidth(readerWidth, pageScaleMode)) {
|
|
|
|
|
const width = readerWidth.value;
|
|
|
|
|
if (isImage && isDoublePage) {
|
|
|
|
|
return `${width / 2}vw`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${width}vw`;
|
2024-11-06 13:00:36 +01:00
|
|
|
}
|
|
|
|
|
|
2025-01-19 20:19:45 +01:00
|
|
|
if (isImage && isDoublePage) {
|
|
|
|
|
return `50vw`;
|
2024-11-06 13:00:36 +01:00
|
|
|
}
|
|
|
|
|
|
2025-01-19 20:19:45 +01:00
|
|
|
return `100vw`;
|
2024-11-06 13:00:36 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getImagePlaceholderStyling = (
|
|
|
|
|
readingMode: IReaderSettings['readingMode'],
|
|
|
|
|
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
|
|
|
|
pageScaleMode: IReaderSettings['pageScaleMode'],
|
|
|
|
|
readerWidth: IReaderSettings['readerWidth'],
|
2025-01-20 01:29:04 +01:00
|
|
|
widthOffset: number,
|
|
|
|
|
heightOffset: number,
|
2024-11-06 13:00:36 +01:00
|
|
|
isDoublePage?: boolean,
|
|
|
|
|
isTabletWidth?: boolean,
|
|
|
|
|
): CSSObject => {
|
2025-01-20 01:29:04 +01:00
|
|
|
const OVER_9000 = 9000;
|
|
|
|
|
|
|
|
|
|
const getMaxWidth = (width: string) => {
|
|
|
|
|
if (width === '100vw') {
|
|
|
|
|
return `calc(${width} - ${widthOffset}px)`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return width;
|
|
|
|
|
};
|
|
|
|
|
const getDesktopWidth = (width: number, readerWidthValue?: number) =>
|
|
|
|
|
getMaxWidth(`${coerceIn(readerWidthValue ?? width, Math.min(width, readerWidthValue ?? OVER_9000), width)}vw`);
|
|
|
|
|
|
|
|
|
|
const setReaderWidth = getSetReaderWidth(readerWidth, pageScaleMode);
|
|
|
|
|
const fullWidth = getMaxWidth(`${Math.min(100, setReaderWidth ?? OVER_9000)}vw`);
|
|
|
|
|
const fullHeight = `calc(100vh - ${heightOffset}px)`;
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SINGLE_PAGE_WIDTH = isTabletWidth ? fullWidth : getDesktopWidth(40, setReaderWidth);
|
|
|
|
|
const DEFAULT_SINGLE_PAGE_HEIGHT = isTabletWidth ? fullHeight : '85vh';
|
2024-11-06 13:00:36 +01:00
|
|
|
|
|
|
|
|
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 },
|
2025-01-20 01:29:04 +01:00
|
|
|
[ReadingMode.DOUBLE_PAGE]: {
|
|
|
|
|
minWidth: isTabletWidth ? '50vw' : getDesktopWidth(35, setReaderWidth ? setReaderWidth / 2 : undefined),
|
|
|
|
|
minHeight: DEFAULT_SINGLE_PAGE_HEIGHT,
|
|
|
|
|
},
|
2024-11-06 13:00:36 +01:00
|
|
|
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
2025-01-20 01:29:04 +01:00
|
|
|
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
2024-11-06 13:00:36 +01:00
|
|
|
minHeight: '100vh',
|
|
|
|
|
},
|
|
|
|
|
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
|
|
|
|
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
2025-01-20 01:29:04 +01:00
|
|
|
minHeight: DEFAULT_SINGLE_PAGE_HEIGHT,
|
2024-11-06 13:00:36 +01:00
|
|
|
},
|
2024-12-16 16:26:32 +01:00
|
|
|
[ReadingMode.WEBTOON]: {
|
2025-01-20 01:29:04 +01:00
|
|
|
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
2024-12-16 16:26:32 +01:00
|
|
|
minHeight: '100vh',
|
|
|
|
|
},
|
2024-11-06 13:00:36 +01:00
|
|
|
};
|
2025-01-20 01:29:04 +01:00
|
|
|
const defaultStyling = {
|
|
|
|
|
...READING_MODE_TO_PLACEHOLDER_SIZE[readingMode],
|
|
|
|
|
// the SpinnerImage placeholder has a default height of 100%, this causes the placeholder to take always take up 100% of the viewport
|
|
|
|
|
height: 'unset',
|
|
|
|
|
};
|
|
|
|
|
const minWidthForStretch = isDoublePage ? `calc(${getMaxWidth(`${setReaderWidth ?? 100}vw`)} / 2)` : fullWidth;
|
2024-11-06 13:00:36 +01:00
|
|
|
|
|
|
|
|
switch (pageScaleMode) {
|
|
|
|
|
case ReaderPageScaleMode.WIDTH:
|
|
|
|
|
return {
|
|
|
|
|
...defaultStyling,
|
|
|
|
|
...applyStyles(shouldStretchPage, {
|
|
|
|
|
minWidth: minWidthForStretch,
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
case ReaderPageScaleMode.HEIGHT:
|
|
|
|
|
return {
|
|
|
|
|
...defaultStyling,
|
|
|
|
|
...applyStyles(shouldStretchPage, {
|
2025-01-20 01:29:04 +01:00
|
|
|
minHeight: fullHeight,
|
2024-12-16 16:26:32 +01:00
|
|
|
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
2024-11-06 13:00:36 +01:00
|
|
|
minHeight: '100vh',
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
case ReaderPageScaleMode.SCREEN:
|
|
|
|
|
return {
|
|
|
|
|
...defaultStyling,
|
|
|
|
|
...applyStyles(shouldStretchPage, {
|
|
|
|
|
minWidth: minWidthForStretch,
|
2025-01-20 01:29:04 +01:00
|
|
|
minHeight: fullHeight,
|
2024-12-16 16:26:32 +01:00
|
|
|
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
2024-11-06 13:00:36 +01:00
|
|
|
minHeight: '100vh',
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
case ReaderPageScaleMode.ORIGINAL:
|
2024-12-22 16:07:55 +01:00
|
|
|
return {
|
|
|
|
|
...defaultStyling,
|
|
|
|
|
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
|
|
|
|
height: undefined,
|
|
|
|
|
}),
|
|
|
|
|
};
|
2024-11-06 13:00:36 +01:00
|
|
|
default:
|
|
|
|
|
throw new Error(`Unexpected "PageScaleMode" (${pageScaleMode})`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-19 13:21:46 +01:00
|
|
|
const getReaderDimensionStyling = (
|
|
|
|
|
width: string,
|
2024-11-06 13:00:36 +01:00
|
|
|
readingMode: IReaderSettings['readingMode'],
|
|
|
|
|
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
|
|
|
|
pageScaleMode: IReaderSettings['pageScaleMode'],
|
2025-01-19 20:19:45 +01:00
|
|
|
widthOffset: number,
|
|
|
|
|
heightOffset: number,
|
2024-11-06 13:00:36 +01:00
|
|
|
): CSSObject => {
|
2025-01-19 20:19:45 +01:00
|
|
|
const finalWidthOffset = readingMode === ReadingMode.DOUBLE_PAGE ? widthOffset / 2 : widthOffset;
|
|
|
|
|
const fullWidth = `calc(${width} - ${finalWidthOffset}px)`;
|
|
|
|
|
const fullHeight = `calc(100vh - ${heightOffset}px)`;
|
|
|
|
|
|
2024-11-06 13:00:36 +01:00
|
|
|
switch (pageScaleMode) {
|
|
|
|
|
case ReaderPageScaleMode.WIDTH:
|
|
|
|
|
return {
|
2024-12-17 00:59:18 +01:00
|
|
|
minWidth: 0,
|
|
|
|
|
...applyStyles(isContinuousReadingMode(readingMode), {
|
|
|
|
|
minWidth: 'unset',
|
|
|
|
|
}),
|
2024-11-06 13:00:36 +01:00
|
|
|
...applyStyles(shouldStretchPage, {
|
2025-01-19 20:19:45 +01:00
|
|
|
minWidth: fullWidth,
|
2024-11-06 13:00:36 +01:00
|
|
|
}),
|
2025-01-19 20:19:45 +01:00
|
|
|
maxWidth: fullWidth,
|
2024-11-06 13:00:36 +01:00
|
|
|
};
|
|
|
|
|
case ReaderPageScaleMode.HEIGHT:
|
|
|
|
|
return {
|
2024-12-17 00:59:18 +01:00
|
|
|
minHeight: 0,
|
|
|
|
|
...applyStyles(isContinuousReadingMode(readingMode), {
|
|
|
|
|
minHeight: 'unset',
|
|
|
|
|
}),
|
2024-11-06 13:00:36 +01:00
|
|
|
...applyStyles(shouldStretchPage, {
|
2025-01-19 20:19:45 +01:00
|
|
|
minHeight: fullHeight,
|
2024-11-06 13:00:36 +01:00
|
|
|
}),
|
2025-01-19 20:19:45 +01:00
|
|
|
maxHeight: fullHeight,
|
2024-11-06 13:00:36 +01:00
|
|
|
};
|
|
|
|
|
case ReaderPageScaleMode.SCREEN:
|
|
|
|
|
return {
|
2024-12-17 00:59:18 +01:00
|
|
|
minWidth: 0,
|
|
|
|
|
minHeight: 0,
|
|
|
|
|
...applyStyles(isContinuousReadingMode(readingMode), {
|
|
|
|
|
minWidth: 'unset',
|
|
|
|
|
minHeight: 'unset',
|
|
|
|
|
}),
|
2024-11-06 13:00:36 +01:00
|
|
|
...applyStyles(shouldStretchPage, {
|
2025-01-19 20:19:45 +01:00
|
|
|
minWidth: fullWidth,
|
|
|
|
|
minHeight: fullHeight,
|
2024-11-06 13:00:36 +01:00
|
|
|
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
|
|
|
|
minWidth: 'unset',
|
2025-01-19 20:19:45 +01:00
|
|
|
minHeight: fullHeight,
|
2024-11-06 13:00:36 +01:00
|
|
|
}),
|
2024-12-16 16:26:32 +01:00
|
|
|
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
2025-01-19 20:19:45 +01:00
|
|
|
minWidth: fullWidth,
|
2024-11-06 13:00:36 +01:00
|
|
|
minHeight: 'unset',
|
|
|
|
|
}),
|
|
|
|
|
}),
|
2025-01-19 20:19:45 +01:00
|
|
|
maxWidth: fullWidth,
|
|
|
|
|
maxHeight: fullHeight,
|
2024-11-06 13:00:36 +01:00
|
|
|
};
|
|
|
|
|
case ReaderPageScaleMode.ORIGINAL:
|
|
|
|
|
return {};
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Unexpected "PageScaleMode" (${pageScaleMode})`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-19 13:21:46 +01:00
|
|
|
export const getReaderImageStyling = (
|
|
|
|
|
readingMode: IReaderSettings['readingMode'],
|
|
|
|
|
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
|
|
|
|
pageScaleMode: IReaderSettings['pageScaleMode'],
|
|
|
|
|
isDoublePage: boolean,
|
|
|
|
|
readerWidth: IReaderSettings['readerWidth'],
|
2025-01-19 20:19:45 +01:00
|
|
|
widthOffset: number,
|
|
|
|
|
heightOffset: number,
|
2025-01-19 13:21:46 +01:00
|
|
|
): CSSObject => {
|
2025-01-19 20:19:45 +01:00
|
|
|
const width = getPageWidth(pageScaleMode, isDoublePage, readerWidth, true);
|
|
|
|
|
return getReaderDimensionStyling(width, readingMode, shouldStretchPage, pageScaleMode, widthOffset, heightOffset);
|
2025-01-19 13:21:46 +01:00
|
|
|
};
|
|
|
|
|
|
2024-12-17 01:01:58 +01:00
|
|
|
export const getImageMarginStyling = (doublePage: boolean, objectFitPosition?: 'left' | 'right'): CSSObject => ({
|
|
|
|
|
m: 'auto',
|
|
|
|
|
...applyStyles(doublePage, {
|
2025-01-19 20:19:45 +01:00
|
|
|
// the margin on the object fit position needs to be removed so that there is no space between both images
|
|
|
|
|
...applyStyles(objectFitPosition === 'right', { mr: 'unset ' }),
|
|
|
|
|
...applyStyles(objectFitPosition === 'left', { ml: 'unset ' }),
|
2024-12-16 21:13:07 +01:00
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
2024-11-06 13:00:36 +01:00
|
|
|
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);
|
|
|
|
|
|
2024-12-22 00:42:33 +01:00
|
|
|
const getPageDownloadPriority = (
|
|
|
|
|
currentPageIndex: number,
|
|
|
|
|
pageIndex: number,
|
|
|
|
|
totalPages: number,
|
|
|
|
|
shouldLoad: boolean,
|
|
|
|
|
): number => {
|
|
|
|
|
if (!shouldLoad) {
|
|
|
|
|
return Number.MAX_SAFE_INTEGER;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-06 13:00:36 +01:00
|
|
|
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],
|
2024-12-22 00:42:33 +01:00
|
|
|
pagesIndex: number,
|
|
|
|
|
isPrimaryPage: boolean,
|
2025-01-20 01:29:04 +01:00
|
|
|
isLoaded: boolean,
|
2024-12-22 00:42:33 +01:00
|
|
|
onLoad: ComponentProps<typeof ReaderPage>['onLoad'],
|
|
|
|
|
onError: ComponentProps<typeof ReaderPage>['onError'],
|
2024-11-06 13:00:36 +01:00
|
|
|
shouldLoad: boolean,
|
|
|
|
|
display: boolean,
|
|
|
|
|
currentPageIndex: number,
|
|
|
|
|
totalPages: number,
|
2024-12-07 04:47:28 +01:00
|
|
|
retryKeyPrefix?: string,
|
2024-11-06 13:00:36 +01:00
|
|
|
position?: 'left' | 'right',
|
|
|
|
|
isDoublePage?: boolean,
|
2024-12-16 21:14:18 +01:00
|
|
|
marginTop?: number,
|
2024-12-22 00:42:33 +01:00
|
|
|
setRef?: (pagesIndex: number, ref: HTMLElement | null) => void,
|
2024-11-06 13:00:36 +01:00
|
|
|
): ReactNode => (
|
|
|
|
|
<ReaderPage
|
2024-12-22 00:42:33 +01:00
|
|
|
setRef={setRef}
|
|
|
|
|
pageIndex={index}
|
|
|
|
|
pagesIndex={pagesIndex}
|
|
|
|
|
isPrimaryPage={isPrimaryPage}
|
2024-11-06 13:00:36 +01:00
|
|
|
key={url}
|
|
|
|
|
src={url}
|
|
|
|
|
alt={alt}
|
|
|
|
|
display={display}
|
2024-12-22 00:42:33 +01:00
|
|
|
priority={getPageDownloadPriority(currentPageIndex, index, totalPages, shouldLoad)}
|
2024-11-06 13:00:36 +01:00
|
|
|
position={position}
|
|
|
|
|
onLoad={onLoad}
|
2024-12-07 04:47:28 +01:00
|
|
|
onError={onError}
|
2024-11-06 13:00:36 +01:00
|
|
|
doublePage={isDoublePage}
|
|
|
|
|
shouldLoad={shouldLoad}
|
2024-12-07 04:47:28 +01:00
|
|
|
retryKeyPrefix={retryKeyPrefix}
|
2024-12-16 21:14:18 +01:00
|
|
|
marginTop={marginTop}
|
2025-01-20 01:29:04 +01:00
|
|
|
isLoaded={isLoaded}
|
2024-11-06 13:00:36 +01:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
2024-12-31 16:43:46 +01:00
|
|
|
type InViewportThresholds = {
|
|
|
|
|
top?: number;
|
|
|
|
|
bottom?: number;
|
|
|
|
|
left?: number;
|
|
|
|
|
right?: number;
|
|
|
|
|
};
|
2024-12-07 03:43:18 +01:00
|
|
|
const getIsPageInViewportInfo = (
|
|
|
|
|
element: HTMLElement,
|
2024-12-31 16:50:19 +01:00
|
|
|
/**
|
|
|
|
|
* Thresholds are not considered for the detection of an image filling the whole viewport.
|
|
|
|
|
* They are only used for detecting if a specific side of an image is inside the viewport
|
|
|
|
|
*/
|
2024-12-31 16:43:46 +01:00
|
|
|
argThresholds?: InViewportThresholds,
|
2024-12-07 03:43:18 +01:00
|
|
|
): {
|
|
|
|
|
isLeftInViewport: boolean;
|
|
|
|
|
isRightInViewport: boolean;
|
|
|
|
|
isFillingWidthViewportCompletely: boolean;
|
|
|
|
|
isTopInViewport: boolean;
|
|
|
|
|
isBottomInViewport: boolean;
|
|
|
|
|
isFillingHeightViewportCompletely: boolean;
|
|
|
|
|
} => {
|
2024-11-06 13:00:36 +01:00
|
|
|
const { top, bottom, left, right } = element.getBoundingClientRect();
|
|
|
|
|
|
2024-12-31 16:43:46 +01:00
|
|
|
const thresholds = {
|
|
|
|
|
top: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
...argThresholds,
|
|
|
|
|
};
|
2024-11-06 13:00:36 +01:00
|
|
|
|
2024-12-31 16:43:46 +01:00
|
|
|
const isLeftInViewport = left >= thresholds.left && left <= window.innerWidth;
|
|
|
|
|
const isRightInViewport = right >= thresholds.right && right <= window.innerWidth;
|
2024-12-31 16:50:19 +01:00
|
|
|
const isFillingWidthViewportCompletely = left <= 0 && right >= window.innerWidth;
|
2024-11-06 13:00:36 +01:00
|
|
|
|
2024-12-31 16:43:46 +01:00
|
|
|
const isTopInViewport = top >= thresholds.top && top <= window.innerHeight;
|
|
|
|
|
const isBottomInViewport = bottom >= thresholds.bottom && bottom <= window.innerHeight;
|
2024-12-31 16:50:19 +01:00
|
|
|
const isFillingHeightViewportCompletely = top <= 0 && bottom >= window.innerHeight;
|
2024-11-06 13:00:36 +01:00
|
|
|
|
2024-12-07 03:43:18 +01:00
|
|
|
return {
|
|
|
|
|
isLeftInViewport,
|
|
|
|
|
isRightInViewport,
|
|
|
|
|
isFillingWidthViewportCompletely,
|
|
|
|
|
isTopInViewport,
|
|
|
|
|
isBottomInViewport,
|
|
|
|
|
isFillingHeightViewportCompletely,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-31 16:43:46 +01:00
|
|
|
export const isPageInViewport = (
|
|
|
|
|
element: HTMLElement,
|
|
|
|
|
type: PageInViewportType,
|
|
|
|
|
thresholds?: InViewportThresholds,
|
|
|
|
|
): boolean => {
|
2024-12-07 03:43:18 +01:00
|
|
|
const {
|
|
|
|
|
isLeftInViewport,
|
|
|
|
|
isRightInViewport,
|
|
|
|
|
isFillingWidthViewportCompletely,
|
|
|
|
|
isTopInViewport,
|
|
|
|
|
isBottomInViewport,
|
|
|
|
|
isFillingHeightViewportCompletely,
|
2024-12-31 16:43:46 +01:00
|
|
|
} = getIsPageInViewportInfo(element, thresholds);
|
2024-12-07 03:43:18 +01:00
|
|
|
|
2024-11-06 13:00:36 +01:00
|
|
|
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})`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-07 03:43:18 +01:00
|
|
|
export const isEndOfPageInViewport = (
|
|
|
|
|
element: HTMLElement,
|
|
|
|
|
type: PageInViewportType,
|
|
|
|
|
direction: ReadingDirection,
|
|
|
|
|
): boolean => {
|
|
|
|
|
const { isLeftInViewport, isRightInViewport, isBottomInViewport } = getIsPageInViewportInfo(element);
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case PageInViewportType.X:
|
|
|
|
|
return direction === ReadingDirection.LTR ? isRightInViewport : isLeftInViewport;
|
|
|
|
|
case PageInViewportType.Y:
|
|
|
|
|
return isBottomInViewport;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`unexpected "type" (${type})`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-06 13:00:36 +01:00
|
|
|
export const getDoublePageModePages = (
|
|
|
|
|
pageUrls: ReaderStatePages['pageUrls'],
|
2024-12-30 21:02:12 +01:00
|
|
|
pagesToSpreadState: ReaderPageSpreadState[],
|
2024-11-06 13:00:36 +01:00
|
|
|
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(
|
2024-12-31 17:17:54 +01:00
|
|
|
(_, page) => page + pagesToSpreadState.slice(0, page).filter(({ isSpread }) => isSpread).length,
|
2024-11-06 13:00:36 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2024-12-31 17:17:54 +01:00
|
|
|
const isPrimaryPageSpreadPage = pagesToSpreadState[index].isSpread;
|
|
|
|
|
const isSecondPageSpreadPage = !!pagesToSpreadState[secondPageIndex]?.isSpread;
|
2024-11-06 13:00:36 +01:00
|
|
|
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,
|
2024-12-31 02:42:17 +01:00
|
|
|
readingMode: ReadingMode,
|
2024-11-06 13:00:36 +01:00
|
|
|
): 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
|
2024-12-31 02:42:17 +01:00
|
|
|
const startPagesIndexTrailingIncluded = Math.max(
|
2024-11-06 13:00:36 +01:00
|
|
|
0,
|
|
|
|
|
currentPagesIndex - Math.min(PREVIOUS_IMAGE_LOAD_AMOUNT, imagePreLoadAmount) * directionInvert,
|
|
|
|
|
);
|
2024-12-31 02:42:17 +01:00
|
|
|
// do not load previous pages for continuous pagers to prevent layout shifts due to leading pages getting loaded
|
|
|
|
|
const startPagesIndex = !isContinuousReadingMode(readingMode) ? startPagesIndexTrailingIncluded : currentPageIndex;
|
2024-11-06 13:00:36 +01:00
|
|
|
const endPagesIndex = currentPagesIndex + imagePreLoadAmount * directionInvert;
|
|
|
|
|
const pagesToRenderLength = Math.abs(endPagesIndex - startPagesIndex) + 1;
|
|
|
|
|
|
|
|
|
|
return Array(pagesToRenderLength)
|
|
|
|
|
.fill(1)
|
|
|
|
|
.map((_, index) => startPagesIndex + index * directionInvert);
|
|
|
|
|
};
|
2024-12-03 18:00:46 +01:00
|
|
|
|
|
|
|
|
export const isTransitionPageVisible = (
|
2024-12-31 04:24:47 +01:00
|
|
|
type: ReaderTransitionPageMode,
|
2024-12-03 18:00:46 +01:00
|
|
|
activeMode: ReaderTransitionPageMode,
|
|
|
|
|
readingMode: IReaderSettings['readingMode'],
|
2024-12-31 04:24:47 +01:00
|
|
|
): boolean => [ReaderTransitionPageMode.BOTH, type].includes(activeMode) || isContinuousReadingMode(readingMode);
|
|
|
|
|
|
2024-12-31 17:03:42 +01:00
|
|
|
export const isATransitionPageVisible = (activeMode: ReaderTransitionPageMode, readingMode: ReadingMode): boolean =>
|
|
|
|
|
activeMode !== ReaderTransitionPageMode.NONE || isContinuousReadingMode(readingMode);
|
2024-12-09 15:07:52 +01:00
|
|
|
|
|
|
|
|
export const getScrollIntoViewInlineOption = (
|
|
|
|
|
offset: DirectionOffset,
|
|
|
|
|
themeDirection: Direction,
|
|
|
|
|
readingDirection: ReadingDirection,
|
|
|
|
|
): ScrollIntoViewOptions['inline'] => {
|
|
|
|
|
const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
|
|
|
|
|
|
|
|
|
if (themeDirection === 'ltr') {
|
|
|
|
|
if (offset === DirectionOffset.PREVIOUS) {
|
|
|
|
|
return getOptionForDirection('start', 'end', themeDirectionForReadingDirection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getOptionForDirection('start', 'end', themeDirectionForReadingDirection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (offset === DirectionOffset.PREVIOUS) {
|
|
|
|
|
return getOptionForDirection('end', 'start', themeDirectionForReadingDirection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getOptionForDirection('end', 'start', themeDirectionForReadingDirection);
|
|
|
|
|
};
|
2024-12-25 02:59:53 +01:00
|
|
|
|
|
|
|
|
export const getScrollToXForReadingDirection = (
|
|
|
|
|
element: HTMLElement,
|
|
|
|
|
themeDirection: Direction,
|
|
|
|
|
readingDirection: ReadingDirection,
|
|
|
|
|
): number => {
|
|
|
|
|
const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
|
|
|
|
|
|
|
|
|
if (themeDirection === 'ltr') {
|
|
|
|
|
return getOptionForDirection(0, element.scrollWidth, themeDirectionForReadingDirection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getOptionForDirection(-element.scrollWidth, 0, themeDirectionForReadingDirection);
|
|
|
|
|
};
|
2024-12-30 20:51:29 +01:00
|
|
|
|
|
|
|
|
export const isPageOfOutdatedPageLoadStates = (
|
|
|
|
|
url: string,
|
|
|
|
|
pageLoadState: ReaderStatePages['pageLoadStates'][number] | undefined,
|
|
|
|
|
): boolean => pageLoadState === undefined || pageLoadState.url !== url;
|