Reader page rendering
This commit is contained in:
@@ -867,6 +867,7 @@
|
|||||||
"scroll_forward": "Scroll forward",
|
"scroll_forward": "Scroll forward",
|
||||||
"stretch_image": "Toggle stretch image"
|
"stretch_image": "Toggle stretch image"
|
||||||
},
|
},
|
||||||
|
"image_preload_amount": "Preload images",
|
||||||
"label": {
|
"label": {
|
||||||
"behaviour": "Behaviour",
|
"behaviour": "Behaviour",
|
||||||
"fit_page_to_window": "Fit page to window",
|
"fit_page_to_window": "Fit page to window",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* 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 CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
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 { SxProps, Theme } from '@mui/material/styles';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { Priority } from '@/lib/Queue.ts';
|
import { Priority } from '@/lib/Queue.ts';
|
||||||
|
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
shouldLoad?: boolean;
|
||||||
|
|
||||||
src: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
|
|
||||||
spinnerStyle?: SxProps<Theme> & { small?: boolean };
|
spinnerStyle?: SxProps<Theme> & { small?: boolean };
|
||||||
imgStyle?: CSSProperties;
|
imgStyle?: SxProps<Theme>;
|
||||||
|
|
||||||
onImageLoad?: () => void;
|
onLoad?: () => void;
|
||||||
|
|
||||||
shouldDecode?: boolean;
|
shouldDecode?: boolean;
|
||||||
useFetchApi?: boolean;
|
useFetchApi?: boolean;
|
||||||
@@ -36,11 +39,12 @@ interface IProps {
|
|||||||
|
|
||||||
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
|
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
|
||||||
const {
|
const {
|
||||||
|
shouldLoad = true,
|
||||||
shouldDecode,
|
shouldDecode,
|
||||||
useFetchApi,
|
useFetchApi,
|
||||||
src,
|
src,
|
||||||
alt,
|
alt,
|
||||||
onImageLoad,
|
onLoad,
|
||||||
spinnerStyle: { small, ...spinnerStyle } = {},
|
spinnerStyle: { small, ...spinnerStyle } = {},
|
||||||
imgStyle,
|
imgStyle,
|
||||||
priority,
|
priority,
|
||||||
@@ -60,12 +64,12 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
|||||||
setHasError(error);
|
setHasError(error);
|
||||||
|
|
||||||
if (!loading && !error && !aborted) {
|
if (!loading && !error && !aborted) {
|
||||||
onImageLoad?.();
|
onLoad?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showMissingImageIcon) {
|
if (showMissingImageIcon || !shouldLoad) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +114,51 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
|||||||
clearTimeout(cacheTimeout);
|
clearTimeout(cacheTimeout);
|
||||||
imageRequest.abortRequest(new Error('Component was unmounted'));
|
imageRequest.abortRequest(new Error('Component was unmounted'));
|
||||||
};
|
};
|
||||||
}, [src, imgLoadRetryKey]);
|
}, [src, imgLoadRetryKey, showMissingImageIcon, shouldLoad]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(isLoading || hasError) && (
|
{showMissingImageIcon ? (
|
||||||
<Box sx={{ height: '100%', ...spinnerStyle }}>
|
<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
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -123,7 +166,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading && <CircularProgress thickness={5} />}
|
{(isLoading || (src && !imageSourceUrl && !hasError)) && <CircularProgress thickness={5} />}
|
||||||
{hasError && isLoading === false && (
|
{hasError && isLoading === false && (
|
||||||
<>
|
<>
|
||||||
<BrokenImageIcon />
|
<BrokenImageIcon />
|
||||||
@@ -143,32 +186,6 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</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';
|
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||||
|
|
||||||
export class MediaQuery {
|
export class MediaQuery {
|
||||||
|
static readonly MOBILE_WIDTH: Breakpoint | number = 'sm';
|
||||||
|
|
||||||
|
static readonly TABLET_WIDTH: Breakpoint | number = 1025;
|
||||||
|
|
||||||
static useIsTouchDevice(): boolean {
|
static useIsTouchDevice(): boolean {
|
||||||
return useMediaQuery('not (pointer: fine)');
|
return useMediaQuery('not (pointer: fine)');
|
||||||
}
|
}
|
||||||
|
|
||||||
static useIsBelowWidth(breakpoint: Breakpoint): boolean {
|
static useIsBelowWidth(breakpoint: Breakpoint | number): boolean {
|
||||||
return useMediaQuery(getCurrentTheme().breakpoints.down(breakpoint));
|
return useMediaQuery(getCurrentTheme().breakpoints.down(breakpoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
static useIsMobileWidth(): boolean {
|
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 {
|
private static getScrollbarSize(type: 'height' | 'width'): number {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const Thumbnail = ({ manga }: { manga: Partial<MangaThumbnailInfo> }) => {
|
|||||||
<SpinnerImage
|
<SpinnerImage
|
||||||
src={Mangas.getThumbnailUrl(manga)}
|
src={Mangas.getThumbnailUrl(manga)}
|
||||||
alt="Manga Thumbnail"
|
alt="Manga Thumbnail"
|
||||||
onImageLoad={() => setIsImageReady(true)}
|
onLoad={() => setIsImageReady(true)}
|
||||||
imgStyle={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
imgStyle={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
/>
|
/>
|
||||||
{isImageReady && (
|
{isImageReady && (
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
|||||||
readingModesDefaultProfile: undefined,
|
readingModesDefaultProfile: undefined,
|
||||||
defaultProfile: undefined,
|
defaultProfile: undefined,
|
||||||
hotkeys: undefined,
|
hotkeys: undefined,
|
||||||
|
imagePreLoadAmount: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
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 Stack from '@mui/material/Stack';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
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 { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||||
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||||
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
|
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
|
||||||
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
||||||
|
import { reverseString } from '@/util/Strings.ts';
|
||||||
|
|
||||||
export const ReaderPageNumber = () => {
|
export const ReaderPageNumber = () => {
|
||||||
const { isDesktop } = ReaderService.useOverlayMode();
|
const { isDesktop } = ReaderService.useOverlayMode();
|
||||||
@@ -24,9 +25,15 @@ export const ReaderPageNumber = () => {
|
|||||||
const { readerNavBarWidth } = useNavBarContext();
|
const { readerNavBarWidth } = useNavBarContext();
|
||||||
const { isMaximized } = useReaderProgressBarContext();
|
const { isMaximized } = useReaderProgressBarContext();
|
||||||
const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext();
|
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) {
|
if (!shouldShowPageNumber) {
|
||||||
return null;
|
return null;
|
||||||
@@ -62,9 +69,7 @@ export const ReaderPageNumber = () => {
|
|||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography sx={{ color: 'white' }}>
|
<Typography sx={{ color: 'white' }}>{pageName}</Typography>
|
||||||
{pageName}/{totalPages}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
|
|||||||
const { setReaderNavBarWidth } = useNavBarContext();
|
const { setReaderNavBarWidth } = useNavBarContext();
|
||||||
const { manga } = useReaderStateMangaContext();
|
const { manga } = useReaderStateMangaContext();
|
||||||
const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
|
const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
|
||||||
const { pages, currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext();
|
const { pages, currentPageIndex, setPageToScrollToIndex } = userReaderStatePagesContext();
|
||||||
|
|
||||||
const getOptionForDirection = useGetOptionForDirection();
|
const getOptionForDirection = useGetOptionForDirection();
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
|
|||||||
<Stack sx={{ gap: 1 }}>
|
<Stack sx={{ gap: 1 }}>
|
||||||
<ReaderNavBarDesktopPageNavigation
|
<ReaderNavBarDesktopPageNavigation
|
||||||
currentPageIndex={currentPageIndex}
|
currentPageIndex={currentPageIndex}
|
||||||
setCurrentPageIndex={setCurrentPageIndex}
|
setPageToScrollToIndex={setPageToScrollToIndex}
|
||||||
pages={pages}
|
pages={pages}
|
||||||
/>
|
/>
|
||||||
<ReaderNavBarDesktopChapterNavigation
|
<ReaderNavBarDesktopChapterNavigation
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants
|
|||||||
|
|
||||||
export const ReaderNavBarDesktopPageNavigation = ({
|
export const ReaderNavBarDesktopPageNavigation = ({
|
||||||
currentPageIndex,
|
currentPageIndex,
|
||||||
setCurrentPageIndex,
|
setPageToScrollToIndex,
|
||||||
pages,
|
pages,
|
||||||
}: Pick<ReaderStatePages, 'currentPageIndex' | 'setCurrentPageIndex' | 'pages'>) => {
|
}: Pick<ReaderStatePages, 'currentPageIndex' | 'setPageToScrollToIndex' | 'pages'>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const openPage = ReaderControls.useOpenPage();
|
const openPage = ReaderControls.useOpenPage();
|
||||||
const { readingDirection } = ReaderService.useSettings();
|
const { readingDirection } = ReaderService.useSettings();
|
||||||
@@ -52,7 +52,7 @@ export const ReaderNavBarDesktopPageNavigation = ({
|
|||||||
labelId="reader-nav-bar-desktop-page-select"
|
labelId="reader-nav-bar-desktop-page-select"
|
||||||
label={t('reader.page_info.label.page')}
|
label={t('reader.page_info.label.page')}
|
||||||
value={currentPage.primary.index}
|
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 }) => (
|
{pages.map(({ primary: { index }, name }) => (
|
||||||
<MenuItem key={index} value={index}>
|
<MenuItem key={index} value={index}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const ReaderProgressBar = ({
|
|||||||
pages,
|
pages,
|
||||||
pageLoadStates,
|
pageLoadStates,
|
||||||
currentPageIndex,
|
currentPageIndex,
|
||||||
setCurrentPageIndex,
|
setPageToScrollToIndex,
|
||||||
slotProps,
|
slotProps,
|
||||||
slots,
|
slots,
|
||||||
createProgressBarSlot,
|
createProgressBarSlot,
|
||||||
@@ -81,7 +81,7 @@ export const ReaderProgressBar = ({
|
|||||||
progressBarRef,
|
progressBarRef,
|
||||||
isDragging,
|
isDragging,
|
||||||
currentPage,
|
currentPage,
|
||||||
setCurrentPageIndex,
|
setPageToScrollToIndex,
|
||||||
pages,
|
pages,
|
||||||
progressBarPosition,
|
progressBarPosition,
|
||||||
getOptionForDirection,
|
getOptionForDirection,
|
||||||
@@ -101,7 +101,7 @@ export const ReaderProgressBar = ({
|
|||||||
? (slotProps?.progressBarPageTexts?.current?.sx ?? [])
|
? (slotProps?.progressBarPageTexts?.current?.sx ?? [])
|
||||||
: [slotProps?.progressBarPageTexts?.current?.sx]),
|
: [slotProps?.progressBarPageTexts?.current?.sx]),
|
||||||
]}
|
]}
|
||||||
onClick={() => setCurrentPageIndex(getNextPageIndex('previous', currentPage.pagesIndex, pages))}
|
onClick={() => setPageToScrollToIndex(getNextPageIndex('previous', currentPage.pagesIndex, pages))}
|
||||||
>
|
>
|
||||||
{currentPage.name}
|
{currentPage.name}
|
||||||
</ReaderProgressBarPageNumber>
|
</ReaderProgressBarPageNumber>
|
||||||
@@ -115,7 +115,7 @@ export const ReaderProgressBar = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPageIndex(
|
setPageToScrollToIndex(
|
||||||
getPageForMousePos(
|
getPageForMousePos(
|
||||||
event.touches[0],
|
event.touches[0],
|
||||||
progressBarRef.current.getBoundingClientRect(),
|
progressBarRef.current.getBoundingClientRect(),
|
||||||
@@ -132,7 +132,7 @@ export const ReaderProgressBar = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPageIndex(
|
setPageToScrollToIndex(
|
||||||
getPageForMousePos(
|
getPageForMousePos(
|
||||||
event,
|
event,
|
||||||
progressBarRef.current.getBoundingClientRect(),
|
progressBarRef.current.getBoundingClientRect(),
|
||||||
@@ -195,7 +195,7 @@ export const ReaderProgressBar = ({
|
|||||||
? (slotProps?.progressBarPageTexts?.total?.sx ?? [])
|
? (slotProps?.progressBarPageTexts?.total?.sx ?? [])
|
||||||
: [slotProps?.progressBarPageTexts?.total?.sx]),
|
: [slotProps?.progressBarPageTexts?.total?.sx]),
|
||||||
]}
|
]}
|
||||||
onClick={() => setCurrentPageIndex(getNextPageIndex('next', currentPage.pagesIndex, pages))}
|
onClick={() => setPageToScrollToIndex(getNextPageIndex('next', currentPage.pagesIndex, pages))}
|
||||||
>
|
>
|
||||||
{totalPages}
|
{totalPages}
|
||||||
</ReaderProgressBarPageNumber>
|
</ReaderProgressBarPageNumber>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { IReaderSettingsWithDefaultFlag } from '@/modules/reader/types/Reader.ty
|
|||||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||||
import { ReaderSettingExitMode } from '@/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx';
|
import { ReaderSettingExitMode } from '@/modules/reader/components/settings/behaviour/ReaderSettingExitMode.tsx';
|
||||||
import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.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 = ({
|
export const ReaderBehaviourSettings = ({
|
||||||
settings,
|
settings,
|
||||||
@@ -43,6 +45,25 @@ export const ReaderBehaviourSettings = ({
|
|||||||
onChange={(_, checked) => updateSetting('shouldOffsetDoubleSpreads', checked)}
|
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>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
|
||||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||||
|
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||||
|
|
||||||
export const ReaderSettingPageGap = ({
|
export const ReaderSettingPageGap = ({
|
||||||
pageGap,
|
pageGap,
|
||||||
@@ -20,9 +21,7 @@ export const ReaderSettingPageGap = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isChangeable = [ReadingMode.CONTINUOUS_HORIZONTAL, ReadingMode.CONTINUOUS_VERTICAL].includes(
|
const isChangeable = isContinuousReadingMode(readingMode.value);
|
||||||
readingMode.value,
|
|
||||||
);
|
|
||||||
if (!isChangeable) {
|
if (!isChangeable) {
|
||||||
return null;
|
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,
|
profiles: undefined,
|
||||||
readingModesDefaultProfile: undefined,
|
readingModesDefaultProfile: undefined,
|
||||||
hotkeys: undefined,
|
hotkeys: undefined,
|
||||||
|
imagePreLoadAmount: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
|
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.STRETCH_IMAGE]: ['f'],
|
||||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
||||||
},
|
},
|
||||||
|
imagePreLoadAmount: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const ReaderStatePagesContext = createContext<ReaderStatePages>({
|
|||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
currentPageIndex: 0,
|
currentPageIndex: 0,
|
||||||
setCurrentPageIndex: () => undefined,
|
setCurrentPageIndex: () => undefined,
|
||||||
|
pageToScrollToIndex: 0,
|
||||||
|
setPageToScrollToIndex: () => undefined,
|
||||||
setTotalPages: () => undefined,
|
setTotalPages: () => undefined,
|
||||||
pageUrls: [],
|
pageUrls: [],
|
||||||
setPageUrls: () => undefined,
|
setPageUrls: () => undefined,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type TContext = ContextType<typeof ReaderStatePagesContext>;
|
|||||||
export const ReaderStatePagesContextProvider = ({ children }: { children: ReactNode }) => {
|
export const ReaderStatePagesContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [totalPages, setTotalPages] = useState<TContext['totalPages']>(0);
|
const [totalPages, setTotalPages] = useState<TContext['totalPages']>(0);
|
||||||
const [currentPageIndex, setCurrentPageIndex] = useState<TContext['currentPageIndex']>(0);
|
const [currentPageIndex, setCurrentPageIndex] = useState<TContext['currentPageIndex']>(0);
|
||||||
|
const [pageToScrollToIndex, setPageToScrollToIndex] = useState<TContext['pageToScrollToIndex']>(0);
|
||||||
const [pageUrls, setPageUrls] = useState<TContext['pageUrls']>([]);
|
const [pageUrls, setPageUrls] = useState<TContext['pageUrls']>([]);
|
||||||
const [pageLoadStates, setPageLoadStates] = useState<TContext['pageLoadStates']>([]);
|
const [pageLoadStates, setPageLoadStates] = useState<TContext['pageLoadStates']>([]);
|
||||||
const [pages, setPages] = useState<TContext['pages']>([createPageData('', 0)]);
|
const [pages, setPages] = useState<TContext['pages']>([createPageData('', 0)]);
|
||||||
@@ -25,6 +26,8 @@ export const ReaderStatePagesContextProvider = ({ children }: { children: ReactN
|
|||||||
setTotalPages,
|
setTotalPages,
|
||||||
currentPageIndex,
|
currentPageIndex,
|
||||||
setCurrentPageIndex,
|
setCurrentPageIndex,
|
||||||
|
pageToScrollToIndex,
|
||||||
|
setPageToScrollToIndex,
|
||||||
pageUrls,
|
pageUrls,
|
||||||
setPageUrls,
|
setPageUrls,
|
||||||
pageLoadStates,
|
pageLoadStates,
|
||||||
@@ -32,7 +35,7 @@ export const ReaderStatePagesContextProvider = ({ children }: { children: ReactN
|
|||||||
pages,
|
pages,
|
||||||
setPages,
|
setPages,
|
||||||
}),
|
}),
|
||||||
[totalPages, pages, currentPageIndex, pageUrls, pageLoadStates],
|
[totalPages, pages, currentPageIndex, pageToScrollToIndex, pageUrls, pageLoadStates],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <ReaderStatePagesContext.Provider value={value}>{children}</ReaderStatePagesContext.Provider>;
|
return <ReaderStatePagesContext.Provider value={value}>{children}</ReaderStatePagesContext.Provider>;
|
||||||
|
|||||||
@@ -6,16 +6,22 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* 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 { Direction } from '@mui/material/styles';
|
||||||
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||||
import { getNextPageIndex, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
import { getNextPageIndex, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
|
||||||
import { getOptionForDirection } from '@/theme.tsx';
|
import { getOptionForDirection } from '@/theme.tsx';
|
||||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
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 { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts';
|
||||||
import { ReaderScrollAmount } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
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> = {
|
const READING_DIRECTION_TO_DIRECTION: Record<ReadingDirection, Direction> = {
|
||||||
[ReadingDirection.LTR]: 'ltr',
|
[ReadingDirection.LTR]: 'ltr',
|
||||||
@@ -95,7 +101,7 @@ export class ReaderControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static useOpenPage(): (offset: 'previous' | 'next') => void {
|
static useOpenPage(): (offset: 'previous' | 'next') => void {
|
||||||
const { currentPageIndex, setCurrentPageIndex, pages } = userReaderStatePagesContext();
|
const { currentPageIndex, setPageToScrollToIndex, pages } = userReaderStatePagesContext();
|
||||||
const { readingDirection } = ReaderService.useSettings();
|
const { readingDirection } = ReaderService.useSettings();
|
||||||
|
|
||||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||||
@@ -113,10 +119,10 @@ export class ReaderControls {
|
|||||||
(offset) => {
|
(offset) => {
|
||||||
switch (offset) {
|
switch (offset) {
|
||||||
case 'previous':
|
case 'previous':
|
||||||
setCurrentPageIndex(getOptionForDirection(previousPageIndex, nextPageIndex, direction));
|
setPageToScrollToIndex(getOptionForDirection(previousPageIndex, nextPageIndex, direction));
|
||||||
break;
|
break;
|
||||||
case 'next':
|
case 'next':
|
||||||
setCurrentPageIndex(getOptionForDirection(nextPageIndex, previousPageIndex, direction));
|
setPageToScrollToIndex(getOptionForDirection(nextPageIndex, previousPageIndex, direction));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unexpected "offset" (${offset})`);
|
throw new Error(`Unexpected "offset" (${offset})`);
|
||||||
@@ -125,4 +131,69 @@ export class ReaderControls {
|
|||||||
[direction, previousPageIndex, nextPageIndex],
|
[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/.
|
* 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 { TapZoneInvertMode, TapZoneLayouts } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||||
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
|
|
||||||
export enum ProgressBarType {
|
export enum ProgressBarType {
|
||||||
HIDDEN,
|
HIDDEN,
|
||||||
@@ -120,6 +122,7 @@ export interface IReaderSettingsGlobal {
|
|||||||
profiles: string[];
|
profiles: string[];
|
||||||
readingModesDefaultProfile: Record<ReadingMode, string>;
|
readingModesDefaultProfile: Record<ReadingMode, string>;
|
||||||
hotkeys: Record<ReaderHotkey, string[]>;
|
hotkeys: Record<ReaderHotkey, string[]>;
|
||||||
|
imagePreLoadAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReaderSettingsManga {
|
export interface IReaderSettingsManga {
|
||||||
@@ -193,3 +196,13 @@ export enum ReaderHotkey {
|
|||||||
STRETCH_IMAGE,
|
STRETCH_IMAGE,
|
||||||
OFFSET_SPREAD_PAGES,
|
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>>;
|
setTotalPages: React.Dispatch<React.SetStateAction<number>>;
|
||||||
currentPageIndex: number;
|
currentPageIndex: number;
|
||||||
setCurrentPageIndex: React.Dispatch<React.SetStateAction<number>>;
|
setCurrentPageIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
pageToScrollToIndex: number;
|
||||||
|
setPageToScrollToIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||||
pageUrls: string[];
|
pageUrls: string[];
|
||||||
setPageUrls: React.Dispatch<React.SetStateAction<string[]>>;
|
setPageUrls: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
pageLoadStates: boolean[];
|
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>,
|
progressBarRef: RefObject<HTMLDivElement | null>,
|
||||||
isDragging: boolean,
|
isDragging: boolean,
|
||||||
currentPage: TReaderProgressCurrentPage,
|
currentPage: TReaderProgressCurrentPage,
|
||||||
setCurrentPageIndex: (pageIndex: number) => void,
|
setPageToScrollToIndex: (pageIndex: number) => void,
|
||||||
pages: ReaderProgressBarProps['pages'],
|
pages: ReaderProgressBarProps['pages'],
|
||||||
progressBarPosition: ProgressBarPosition,
|
progressBarPosition: ProgressBarPosition,
|
||||||
getOptionForDirection: typeof getOptionForDirectionImpl,
|
getOptionForDirection: typeof getOptionForDirectionImpl,
|
||||||
@@ -115,7 +115,7 @@ export const useHandleProgressDragging = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPageIndex(newPageIndex);
|
setPageToScrollToIndex(newPageIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
|
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 { DEFAULT_READER_PROFILE } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||||
|
|
||||||
@@ -31,3 +31,9 @@ export const createProfileValueToDisplayData = (
|
|||||||
|
|
||||||
export const getValidReaderProfile = (profile: string, profiles: string[]): string =>
|
export const getValidReaderProfile = (profile: string, profiles: string[]): string =>
|
||||||
profiles.includes(profile) ? profile : DEFAULT_READER_PROFILE;
|
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)
|
baseCleanup(str)
|
||||||
.replace(/[^a-zA-Z0-9\\s]+/g, ' ')
|
.replace(/[^a-zA-Z0-9\\s]+/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
export const reverseString = (str: string, separator: string = ''): string =>
|
||||||
|
str.split(separator).reverse().join(separator);
|
||||||
|
|||||||
Reference in New Issue
Block a user