Add infinite scroll to reader
This commit is contained in:
@@ -124,7 +124,7 @@ export const ChapterCard = memo((props: IProps) => {
|
||||
style={{
|
||||
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
||||
}}
|
||||
state={Chapters.getReaderOpenChapterLocationState(chapter)}
|
||||
state={Chapters.getReaderOpenChapterLocationState(chapter, true)}
|
||||
replace={mode === 'reader'}
|
||||
onClick={(e) => handleClick(e)}
|
||||
{...longPressBind(popupState.open)}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts
|
||||
|
||||
import { DirectionOffset, TranslationKey } from '@/Base.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import { ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ReaderOpenChapterLocationState, ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
|
||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||
import { DOWNLOAD_TYPE_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts';
|
||||
@@ -460,9 +460,13 @@ export class Chapters {
|
||||
return ReaderResumeMode.LAST_READ;
|
||||
}
|
||||
|
||||
static getReaderOpenChapterLocationState(chapter: ChapterReadInfo): { resumeMode: ReaderResumeMode } {
|
||||
static getReaderOpenChapterLocationState(
|
||||
chapter: ChapterReadInfo,
|
||||
updateInitialChapter?: boolean,
|
||||
): ReaderOpenChapterLocationState {
|
||||
return {
|
||||
resumeMode: Chapters.getReaderResumeMode(chapter),
|
||||
updateInitialChapter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
403
src/modules/reader/components/viewer/ReaderChapterViewer.tsx
Normal file
403
src/modules/reader/components/viewer/ReaderChapterViewer.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* 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 { memo, MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { getDoublePageModePages } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
||||
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
||||
import {
|
||||
getPagerForReadingMode,
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import {
|
||||
createHandleReaderPageLoadError,
|
||||
createUpdateReaderPageLoadState,
|
||||
} from '@/modules/reader/utils/Reader.utils.ts';
|
||||
import { useReaderConvertPagesForReadingMode } from '@/modules/reader/hooks/useReaderConvertPagesForReadingMode.ts';
|
||||
import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.tsx';
|
||||
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
||||
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
||||
import { READER_STATE_PAGES_DEFAULTS } from '@/modules/reader/constants/ReaderContext.constants.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useReaderSetPagesState } from '@/modules/reader/hooks/useReaderSetPagesState.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { ReaderInfiniteScrollUpdateChapter } from '@/modules/reader/components/viewer/ReaderInfiniteScrollUpdateChapter.tsx';
|
||||
|
||||
const BaseReaderChapterViewer = ({
|
||||
currentPageIndex,
|
||||
setPages: setContextPages,
|
||||
setPageLoadStates: setContextPageLoadStates,
|
||||
setTotalPages: setContextTotalPages,
|
||||
setCurrentPageIndex: setContextCurrentPageIndex,
|
||||
updateCurrentPageIndex,
|
||||
setPageToScrollToIndex,
|
||||
transitionPageMode,
|
||||
retryFailedPagesKeyPrefix,
|
||||
setTransitionPageMode,
|
||||
readingMode,
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
readingDirection,
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
nextChapterId,
|
||||
isPreviousChapterVisible,
|
||||
isNextChapterVisible,
|
||||
lastPageRead,
|
||||
isInitialChapter,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
imageRefs: globalImageRefs,
|
||||
scrollIntoView,
|
||||
setReaderStateChapters,
|
||||
resumeMode,
|
||||
}: Pick<
|
||||
ReaderStatePages,
|
||||
| 'currentPageIndex'
|
||||
| 'setPages'
|
||||
| 'setPageLoadStates'
|
||||
| 'setTotalPages'
|
||||
| 'setCurrentPageIndex'
|
||||
| 'setPageToScrollToIndex'
|
||||
| 'transitionPageMode'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
| 'setTransitionPageMode'
|
||||
> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode'
|
||||
> &
|
||||
Pick<ReaderStateChapters, 'setReaderStateChapters'> & {
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
previousChapterId?: ChapterIdInfo['id'];
|
||||
nextChapterId?: ChapterIdInfo['id'];
|
||||
isPreviousChapterVisible: boolean;
|
||||
isNextChapterVisible: boolean;
|
||||
lastPageRead: number;
|
||||
isInitialChapter: boolean;
|
||||
isCurrentChapter: boolean;
|
||||
isPreviousChapter: boolean;
|
||||
isNextChapter: boolean;
|
||||
isLeadingChapter: boolean;
|
||||
isTrailingChapter: boolean;
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
||||
scrollIntoView: boolean;
|
||||
resumeMode: ReaderResumeMode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { direction: themeDirection } = useTheme();
|
||||
|
||||
const [fetchPages, pagesResponse] = requestManager.useGetChapterPagesFetch(chapterId ?? -1);
|
||||
|
||||
const [arePagesFetched, setArePagesFetched] = useState(false);
|
||||
const [totalPages, setTotalPages] = useState<ReaderStatePages['totalPages']>(
|
||||
READER_STATE_PAGES_DEFAULTS.totalPages,
|
||||
);
|
||||
const [pageUrls, setPageUrls] = useState<ReaderStatePages['pageUrls']>(READER_STATE_PAGES_DEFAULTS.pageUrls);
|
||||
const [pages, setPages] = useState<ReaderStatePages['pages']>(READER_STATE_PAGES_DEFAULTS.pages);
|
||||
const [pageLoadStates, setPageLoadStates] = useState<ReaderStatePages['pageLoadStates']>(
|
||||
READER_STATE_PAGES_DEFAULTS.pageLoadStates,
|
||||
);
|
||||
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
|
||||
pageLoadStates.map(({ url }) => ({ url, isSpread: false })),
|
||||
);
|
||||
|
||||
const isCurrentChapterRef = useRef(isCurrentChapter);
|
||||
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
|
||||
|
||||
const actualPages = useMemo(() => {
|
||||
const arePagesLoaded = !!totalPages;
|
||||
if (!arePagesLoaded) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
const isSpreadStateUpdated = totalPages === pagesToSpreadState.length;
|
||||
if (!isSpreadStateUpdated) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
||||
return getDoublePageModePages(pageUrls, pagesToSpreadState, shouldOffsetDoubleSpreads, readingDirection);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]);
|
||||
|
||||
const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]);
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const shouldHideChapter = !isContinuousReadingModeActive && !isCurrentChapter;
|
||||
|
||||
isCurrentChapterRef.current = isCurrentChapter;
|
||||
if (isCurrentChapter) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
globalImageRefs.current = imageRefs.current;
|
||||
}
|
||||
|
||||
const doFetchPages = useCallback(() => {
|
||||
if (!chapterId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setArePagesFetched(false);
|
||||
|
||||
fetchPages({ variables: { input: { chapterId } } }).catch(
|
||||
defaultPromiseErrorHandler(`ReaderChapterViewer(${chapterId})::fetchPages`),
|
||||
);
|
||||
}, [fetchPages, chapterId]);
|
||||
|
||||
const onLoad = useMemo(
|
||||
() =>
|
||||
createUpdateReaderPageLoadState(
|
||||
actualPages,
|
||||
pagesToSpreadState,
|
||||
setPagesToSpreadState,
|
||||
pageLoadStates,
|
||||
(value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
setContextPageLoadStates(value);
|
||||
}
|
||||
|
||||
setPageLoadStates(value);
|
||||
},
|
||||
readingMode,
|
||||
),
|
||||
// do not add "pagesToSpreadState" and "pageLoadStates" as a dependency, otherwise, every page gets re-rendered
|
||||
// when they change which impacts the performance massively (depending on the total page count)
|
||||
[actualPages, readingMode],
|
||||
);
|
||||
|
||||
const onError = useMemo(
|
||||
() =>
|
||||
createHandleReaderPageLoadError((value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
setContextPageLoadStates(value);
|
||||
}
|
||||
|
||||
setPageLoadStates(value);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
doFetchPages();
|
||||
}, [chapterId]);
|
||||
|
||||
const updatePageState = <T,>(
|
||||
value: T,
|
||||
setLocalState: (value: T) => void,
|
||||
setGlobalState: (value: T) => void,
|
||||
forceLocal: boolean = false,
|
||||
) => {
|
||||
if (forceLocal || !arePagesFetched) {
|
||||
setLocalState(value);
|
||||
}
|
||||
|
||||
if (isCurrentChapter) {
|
||||
setGlobalState(value);
|
||||
}
|
||||
};
|
||||
useReaderSetPagesState(
|
||||
isCurrentChapter,
|
||||
pagesResponse,
|
||||
resumeMode,
|
||||
lastPageRead,
|
||||
actualPages,
|
||||
pageLoadStates,
|
||||
pagesToSpreadState,
|
||||
arePagesFetched,
|
||||
setArePagesFetched,
|
||||
setReaderStateChapters,
|
||||
(value) => updatePageState(value, setTotalPages, setContextTotalPages),
|
||||
(value) => updatePageState(value, setPages, setContextPages),
|
||||
(value) => updatePageState(value, setPageUrls, noOp),
|
||||
(value) => updatePageState(value, setPageLoadStates, setContextPageLoadStates),
|
||||
(value) => updatePageState(value, setPagesToSpreadState, noOp),
|
||||
(value) => updatePageState(value, noOp, setContextCurrentPageIndex),
|
||||
(value) => {
|
||||
if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
|
||||
setPageToScrollToIndex(value);
|
||||
setReaderStateChapters((prevState) => ({
|
||||
...prevState,
|
||||
visibleChapters: { ...prevState.visibleChapters, scrollIntoView: false, resumeMode: undefined },
|
||||
}));
|
||||
}
|
||||
},
|
||||
(value) => updatePageState(value, noOp, setTransitionPageMode),
|
||||
);
|
||||
|
||||
useReaderConvertPagesForReadingMode(
|
||||
currentPageIndex,
|
||||
actualPages,
|
||||
pageUrls,
|
||||
(value) => updatePageState(value, setPages, setContextPages, true),
|
||||
(value) => updatePageState(value, setPagesToSpreadState, noOp, true),
|
||||
(value) => updatePageState(value, noOp, updateCurrentPageIndex),
|
||||
readingMode,
|
||||
);
|
||||
|
||||
// for non-continuous reading modes, only the current, previous and next chapter are relevant
|
||||
// every other chapter does not need to be rendered all the time since it's not affecting the
|
||||
// visible content anyway
|
||||
// the previous and next chapter are rendered so that going to the previous/next chapter feels smoother
|
||||
// since the relevant pages are already rendered
|
||||
const shouldRenderChapterViewer =
|
||||
isContinuousReadingModeActive || isCurrentChapter || isPreviousChapter || isNextChapter;
|
||||
if (!shouldRenderChapterViewer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pagesResponse.error) {
|
||||
if (shouldHideChapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
messageExtra={getErrorMessage(pagesResponse.error)}
|
||||
retry={() => {
|
||||
doFetchPages();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (pagesResponse.loading || !arePagesFetched) {
|
||||
if (shouldHideChapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<LoadingPlaceholder />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (chapterId != null && !totalPages) {
|
||||
if (shouldHideChapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minWidth: '100%', minHeight: '100%', position: 'relative' }}>
|
||||
<EmptyViewAbsoluteCentered message={t('reader.error.label.no_pages_found')} retry={doFetchPages} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
width: '100%',
|
||||
minWidth: 'fit-content',
|
||||
height: '100%',
|
||||
minHeight: 'fit-content',
|
||||
margin: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(shouldHideChapter, {
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
minWidth: 'unset',
|
||||
minHeight: 'unset',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
...applyStyles(
|
||||
isContinuousVerticalReadingMode(readingMode) && shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||
{ alignItems: 'center' },
|
||||
),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ReaderInfiniteScrollUpdateChapter
|
||||
readingMode={readingMode}
|
||||
readingDirection={readingDirection}
|
||||
chapterId={chapterId}
|
||||
previousChapterId={previousChapterId}
|
||||
nextChapterId={nextChapterId}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapterVisible={isPreviousChapterVisible}
|
||||
isNextChapterVisible={isNextChapterVisible}
|
||||
firstImage={imageRefs.current[0]}
|
||||
lastImage={imageRefs.current[imageRefs.current.length - 1]}
|
||||
/>
|
||||
{((!isContinuousReadingModeActive && isCurrentChapter) ||
|
||||
(isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter))) && (
|
||||
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.PREVIOUS} />
|
||||
)}
|
||||
<Pager
|
||||
totalPages={totalPages}
|
||||
currentPageIndex={currentPageIndex}
|
||||
pages={actualPages}
|
||||
transitionPageMode={transitionPageMode}
|
||||
pageLoadStates={pageLoadStates}
|
||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
||||
imageRefs={imageRefs}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapter={isPreviousChapter}
|
||||
isNextChapter={isNextChapter}
|
||||
/>
|
||||
{((!isContinuousReadingModeActive && isCurrentChapter) ||
|
||||
(isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter))) && (
|
||||
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.NEXT} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderChapterViewer = memo(BaseReaderChapterViewer);
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { memo } from 'react';
|
||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderInfiniteScrollUpdateChapter } from '@/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts';
|
||||
import { ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
||||
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
||||
|
||||
const BaseReaderInfiniteScrollUpdateChapter = ({
|
||||
readingMode,
|
||||
readingDirection,
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
nextChapterId,
|
||||
isPreviousChapterVisible,
|
||||
isCurrentChapter,
|
||||
isNextChapterVisible,
|
||||
firstImage,
|
||||
lastImage,
|
||||
openChapter,
|
||||
}: {
|
||||
readingMode: ReadingMode;
|
||||
readingDirection: ReadingDirection;
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
previousChapterId?: ChapterIdInfo['id'];
|
||||
nextChapterId?: ChapterIdInfo['id'];
|
||||
isPreviousChapterVisible: boolean;
|
||||
isCurrentChapter: boolean;
|
||||
isNextChapterVisible: boolean;
|
||||
firstImage: HTMLElement | null;
|
||||
lastImage: HTMLElement | null;
|
||||
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>;
|
||||
}) => {
|
||||
useReaderInfiniteScrollUpdateChapter(
|
||||
'first',
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
isCurrentChapter,
|
||||
isPreviousChapterVisible,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
openChapter,
|
||||
firstImage,
|
||||
);
|
||||
useReaderInfiniteScrollUpdateChapter(
|
||||
'last',
|
||||
chapterId,
|
||||
nextChapterId,
|
||||
isCurrentChapter,
|
||||
isNextChapterVisible,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
openChapter,
|
||||
lastImage,
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ReaderInfiniteScrollUpdateChapter = withPropsFrom(
|
||||
memo(BaseReaderInfiniteScrollUpdateChapter),
|
||||
[() => ({ openChapter: ReaderControls.useOpenChapter() })],
|
||||
['openChapter'],
|
||||
);
|
||||
@@ -10,33 +10,31 @@ import {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
PageInViewportType,
|
||||
ReaderPageSpreadState,
|
||||
ReaderTransitionPageMode,
|
||||
ReaderOpenChapterLocationState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { getDoublePageModePages, isPageOfOutdatedPageLoadStates } 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 {
|
||||
getPagerForReadingMode,
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
@@ -48,7 +46,6 @@ import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.type
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
import { createUpdateReaderPageLoadState } from '@/modules/reader/utils/Reader.utils.ts';
|
||||
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
import { useReaderAutoScroll } from '@/modules/reader/hooks/useReaderAutoScroll.ts';
|
||||
@@ -57,8 +54,13 @@ import { useReaderHorizontalModeInvertXYScrolling } from '@/modules/reader/hooks
|
||||
import { useReaderHideCursorOnInactivity } from '@/modules/reader/hooks/useReaderHideCursorOnInactivity.ts';
|
||||
import { useReaderScrollToStartOnPageChange } from '@/modules/reader/hooks/useReaderScrollToStartOnPageChange.ts';
|
||||
import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHandlePageSelection.ts';
|
||||
import { useReaderConvertPagesForReadingMode } from '@/modules/reader/hooks/useReaderConvertPagesForReadingMode.ts';
|
||||
import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderChapterViewer } from '@/modules/reader/components/viewer/ReaderChapterViewer.tsx';
|
||||
import {
|
||||
getReaderChapterViewerCurrentPageIndex,
|
||||
getReaderChapterViewResumeMode,
|
||||
} from '@/modules/reader/utils/Reader.utils.ts';
|
||||
import { noOp } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
|
||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||
@@ -75,18 +77,19 @@ const BaseReaderViewer = forwardRef(
|
||||
pageToScrollToIndex,
|
||||
setPageToScrollToIndex,
|
||||
pages,
|
||||
setPages,
|
||||
totalPages,
|
||||
pageUrls,
|
||||
pageLoadStates,
|
||||
setPages,
|
||||
setPageLoadStates,
|
||||
setTotalPages,
|
||||
setCurrentPageIndex,
|
||||
transitionPageMode,
|
||||
retryFailedPagesKeyPrefix,
|
||||
setTransitionPageMode,
|
||||
readingMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
readingDirection,
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
setScrollbarXSize,
|
||||
setScrollbarYSize,
|
||||
isVisible: isOverlayVisible,
|
||||
@@ -94,26 +97,42 @@ const BaseReaderViewer = forwardRef(
|
||||
updateCurrentPageIndex,
|
||||
showPreview,
|
||||
setShowPreview,
|
||||
initialChapter,
|
||||
currentChapter,
|
||||
chapters,
|
||||
visibleChapters,
|
||||
setReaderStateChapters,
|
||||
isCurrentChapterReady,
|
||||
}: Pick<
|
||||
ReaderStatePages,
|
||||
| 'currentPageIndex'
|
||||
| 'pageToScrollToIndex'
|
||||
| 'setPageToScrollToIndex'
|
||||
| 'pages'
|
||||
| 'setPages'
|
||||
| 'totalPages'
|
||||
| 'pageUrls'
|
||||
| 'pageLoadStates'
|
||||
| 'setPages'
|
||||
| 'setPageLoadStates'
|
||||
| 'setTotalPages'
|
||||
| 'setCurrentPageIndex'
|
||||
| 'transitionPageMode'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
| 'setTransitionPageMode'
|
||||
> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode'
|
||||
'readingMode' | 'readingDirection' | 'readerWidth' | 'pageScaleMode' | 'shouldOffsetDoubleSpreads'
|
||||
> &
|
||||
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
|
||||
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
||||
Pick<
|
||||
ReaderStateChapters,
|
||||
| 'initialChapter'
|
||||
| 'currentChapter'
|
||||
| 'chapters'
|
||||
| 'visibleChapters'
|
||||
| 'setReaderStateChapters'
|
||||
| 'isCurrentChapterReady'
|
||||
> &
|
||||
TReaderTapZoneContext & {
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
||||
},
|
||||
@@ -121,10 +140,14 @@ const BaseReaderViewer = forwardRef(
|
||||
ref: ForwardedRef<HTMLDivElement | null>,
|
||||
) => {
|
||||
const { direction: themeDirection } = useTheme();
|
||||
const { resumeMode } = useLocation<ReaderOpenChapterLocationState>().state ?? {
|
||||
resumeMode: ReaderResumeMode.START,
|
||||
};
|
||||
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
useImperativeHandle(ref, () => scrollElementRef.current!);
|
||||
|
||||
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const isDragging = useMouseDragScroll(scrollElementRef);
|
||||
|
||||
@@ -140,81 +163,27 @@ const BaseReaderViewer = forwardRef(
|
||||
|
||||
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
|
||||
|
||||
const previousTotalPages = useRef(totalPages);
|
||||
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
|
||||
pageLoadStates.map(({ url }) => ({ url, isSpread: false })),
|
||||
);
|
||||
|
||||
const resetPagesSpreadState = previousTotalPages.current !== totalPages;
|
||||
if (resetPagesSpreadState) {
|
||||
previousTotalPages.current = totalPages;
|
||||
setPagesToSpreadState(pageLoadStates.map(({ url }) => ({ url, isSpread: false })));
|
||||
}
|
||||
|
||||
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
|
||||
|
||||
const actualPages = useMemo(() => {
|
||||
const arePagesLoaded = !!totalPages;
|
||||
if (!arePagesLoaded) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
||||
return getDoublePageModePages(
|
||||
pageUrls,
|
||||
pagesToSpreadState,
|
||||
shouldOffsetDoubleSpreads,
|
||||
readingDirection,
|
||||
);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]);
|
||||
|
||||
const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]);
|
||||
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
const onLoad = useMemo(
|
||||
const initialChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === initialChapter?.id),
|
||||
[initialChapter?.id],
|
||||
);
|
||||
const chaptersToRender = useMemo(
|
||||
() =>
|
||||
createUpdateReaderPageLoadState(
|
||||
actualPages,
|
||||
pagesToSpreadState,
|
||||
setPagesToSpreadState,
|
||||
pageLoadStates,
|
||||
setPageLoadStates,
|
||||
readingMode,
|
||||
chapters.slice(
|
||||
Math.max(0, initialChapterIndex - visibleChapters.trailing),
|
||||
Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1),
|
||||
),
|
||||
// do not add "pagesToSpreadState" and "pageLoadStates" as a dependency, otherwise, every page gets re-rendered
|
||||
// when they change which impacts the performance massively (depending on the total page count)
|
||||
[actualPages, readingMode],
|
||||
[chapters, initialChapterIndex, visibleChapters.trailing, visibleChapters.leading, chapters.length],
|
||||
);
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chaptersToRender.findIndex((chapter) => chapter.id === currentChapter?.id),
|
||||
[currentChapter, chaptersToRender],
|
||||
);
|
||||
|
||||
const onError = useCallback((pageIndex: number, url: string) => {
|
||||
setPageLoadStates((statePageLoadStates) => {
|
||||
const pageLoadState = statePageLoadStates[pageIndex];
|
||||
|
||||
if (isPageOfOutdatedPageLoadStates(url, pageLoadState)) {
|
||||
return statePageLoadStates;
|
||||
}
|
||||
|
||||
return statePageLoadStates.toSpliced(pageIndex, 1, {
|
||||
...pageLoadState,
|
||||
loaded: false,
|
||||
error: true,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useReaderConvertPagesForReadingMode(
|
||||
currentPageIndex,
|
||||
actualPages,
|
||||
pageUrls,
|
||||
setPages,
|
||||
setPagesToSpreadState,
|
||||
updateCurrentPageIndex,
|
||||
readingMode,
|
||||
);
|
||||
useReaderHandlePageSelection(
|
||||
pageToScrollToIndex,
|
||||
currentPageIndex,
|
||||
@@ -245,6 +214,10 @@ const BaseReaderViewer = forwardRef(
|
||||
);
|
||||
useReaderAutoScroll(isOverlayVisible, automaticScrolling);
|
||||
|
||||
if (!initialChapter || !currentChapter) {
|
||||
throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined');
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={scrollElementRef}
|
||||
@@ -254,11 +227,10 @@ const BaseReaderViewer = forwardRef(
|
||||
overflow: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(
|
||||
isContinuousVerticalReadingMode(readingMode) &&
|
||||
shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||
isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||
{ alignItems: 'center' },
|
||||
),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
...applyStyles(!isContinuousVerticalReadingModeActive, {
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
@@ -278,19 +250,78 @@ const BaseReaderViewer = forwardRef(
|
||||
)
|
||||
}
|
||||
>
|
||||
<ReaderTransitionPage type={ReaderTransitionPageMode.PREVIOUS} />
|
||||
<Pager
|
||||
totalPages={totalPages}
|
||||
currentPageIndex={currentPageIndex}
|
||||
pages={actualPages}
|
||||
transitionPageMode={transitionPageMode}
|
||||
pageLoadStates={pageLoadStates}
|
||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
||||
imageRefs={imageRefs}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
/>
|
||||
<ReaderTransitionPage type={ReaderTransitionPageMode.NEXT} />
|
||||
{chaptersToRender.map((_, index) => {
|
||||
// chapters are sorted by latest to oldest, thus, loop over it in reversed order
|
||||
const chapterIndex = Math.max(0, chaptersToRender.length - index - 1);
|
||||
const chapter = chaptersToRender[chapterIndex];
|
||||
|
||||
const previousChapter =
|
||||
chaptersToRender[chapterIndex + 1] ??
|
||||
chapters[initialChapterIndex + visibleChapters.leading + 1];
|
||||
const nextChapter =
|
||||
chaptersToRender[chapterIndex - 1] ??
|
||||
chapters[initialChapterIndex - visibleChapters.trailing - 1];
|
||||
|
||||
const isInitialChapter = chapter.id === initialChapter.id;
|
||||
const isCurrentChapter = chapter.id === currentChapter.id;
|
||||
const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id;
|
||||
const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id;
|
||||
const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder;
|
||||
const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder;
|
||||
|
||||
return (
|
||||
<ReaderChapterViewer
|
||||
key={chapter.id}
|
||||
chapterId={chapter.id}
|
||||
previousChapterId={previousChapter?.id}
|
||||
nextChapterId={nextChapter?.id}
|
||||
isPreviousChapterVisible={!!chaptersToRender[chapterIndex + 1]}
|
||||
isNextChapterVisible={!!chaptersToRender[chapterIndex - 1]}
|
||||
lastPageRead={chapter.lastPageRead}
|
||||
currentPageIndex={getReaderChapterViewerCurrentPageIndex(
|
||||
currentPageIndex,
|
||||
chapter,
|
||||
currentChapter,
|
||||
isCurrentChapter,
|
||||
isCurrentChapterReady,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
visibleChapters,
|
||||
)}
|
||||
isInitialChapter={isInitialChapter}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapter={isPreviousChapter}
|
||||
isNextChapter={isNextChapter}
|
||||
isLeadingChapter={isLeadingChapter}
|
||||
isTrailingChapter={isTrailingChapter}
|
||||
imageRefs={imageRefs}
|
||||
setPages={setPages}
|
||||
setPageLoadStates={setPageLoadStates}
|
||||
setTotalPages={setTotalPages}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
setPageToScrollToIndex={setPageToScrollToIndex}
|
||||
transitionPageMode={transitionPageMode}
|
||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
||||
readingMode={readingMode}
|
||||
readerWidth={readerWidth}
|
||||
pageScaleMode={pageScaleMode}
|
||||
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads}
|
||||
readingDirection={readingDirection}
|
||||
updateCurrentPageIndex={isCurrentChapter ? updateCurrentPageIndex : noOp}
|
||||
scrollIntoView={isCurrentChapter && visibleChapters.scrollIntoView}
|
||||
resumeMode={getReaderChapterViewResumeMode(
|
||||
isCurrentChapter,
|
||||
isInitialChapter,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
visibleChapters.resumeMode,
|
||||
resumeMode,
|
||||
)}
|
||||
setReaderStateChapters={setReaderStateChapters}
|
||||
setTransitionPageMode={setTransitionPageMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
@@ -305,24 +336,26 @@ export const ReaderViewer = withPropsFrom(
|
||||
useReaderOverlayContext,
|
||||
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
|
||||
useReaderTapZoneContext,
|
||||
useReaderStateChaptersContext,
|
||||
],
|
||||
[
|
||||
'currentPageIndex',
|
||||
'pageToScrollToIndex',
|
||||
'setPageToScrollToIndex',
|
||||
'pages',
|
||||
'setPages',
|
||||
'totalPages',
|
||||
'pageUrls',
|
||||
'pageLoadStates',
|
||||
'setPages',
|
||||
'setPageLoadStates',
|
||||
'transitionPageMode',
|
||||
'setTotalPages',
|
||||
'setCurrentPageIndex',
|
||||
'retryFailedPagesKeyPrefix',
|
||||
'setTransitionPageMode',
|
||||
'readingMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'readingDirection',
|
||||
'readerWidth',
|
||||
'pageScaleMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'transitionPageMode',
|
||||
'setScrollbarXSize',
|
||||
'setScrollbarYSize',
|
||||
'isVisible',
|
||||
@@ -330,5 +363,11 @@ export const ReaderViewer = withPropsFrom(
|
||||
'updateCurrentPageIndex',
|
||||
'showPreview',
|
||||
'setShowPreview',
|
||||
'initialChapter',
|
||||
'currentChapter',
|
||||
'chapters',
|
||||
'visibleChapters',
|
||||
'setReaderStateChapters',
|
||||
'isCurrentChapterReady',
|
||||
],
|
||||
);
|
||||
|
||||
@@ -25,6 +25,9 @@ const BaseBasePager = ({
|
||||
slots,
|
||||
readingMode,
|
||||
imagePreLoadAmount,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
|
||||
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
|
||||
createPage: (
|
||||
@@ -45,8 +48,11 @@ const BaseBasePager = ({
|
||||
previousCurrentPageIndex.current,
|
||||
imagePreLoadAmount,
|
||||
readingMode,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
),
|
||||
[currentPageIndex, pages, imagePreLoadAmount, readingMode],
|
||||
[currentPageIndex, pages, imagePreLoadAmount, readingMode, isCurrentChapter, isPreviousChapter, isNextChapter],
|
||||
);
|
||||
useEffect(() => {
|
||||
previousCurrentPageIndex.current = currentPageIndex;
|
||||
|
||||
@@ -81,7 +81,7 @@ const BaseReaderDoublePagedPager = ({
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && isPrimaryPage,
|
||||
shouldDisplay && isPrimaryPage && shouldLoad,
|
||||
currentPage.primary.index,
|
||||
totalPages,
|
||||
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
@@ -97,7 +97,7 @@ const BaseReaderDoublePagedPager = ({
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && isSecondaryPage,
|
||||
shouldDisplay && isSecondaryPage && shouldLoad,
|
||||
currentSecondaryPageIndex,
|
||||
totalPages,
|
||||
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
|
||||
@@ -33,7 +33,7 @@ const BaseReaderPagedPager = ({
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && currentPageIndex === page.primary.index,
|
||||
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
|
||||
@@ -9,9 +9,22 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderStateChaptersContext = createContext<ReaderStateChapters>({
|
||||
export const READER_STATE_CHAPTERS_DEFAULTS: Omit<ReaderStateChapters, 'setReaderStateChapters'> = {
|
||||
mangaChapters: [],
|
||||
chapters: [],
|
||||
isCurrentChapterReady: false,
|
||||
visibleChapters: {
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
lastLeadingChapterSourceOrder: 99999,
|
||||
lastTrailingChapterSourceOrder: -1,
|
||||
scrollIntoView: false,
|
||||
resumeMode: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReaderStateChaptersContext = createContext<ReaderStateChapters>({
|
||||
...READER_STATE_CHAPTERS_DEFAULTS,
|
||||
setReaderStateChapters: () => {},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
*/
|
||||
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { ReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import {
|
||||
READER_STATE_CHAPTERS_DEFAULTS,
|
||||
ReaderStateChaptersContext,
|
||||
} from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderStateChaptersContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [state, setState] = useState<Omit<ReaderStateChapters, 'setReaderStateChapters'>>({
|
||||
mangaChapters: [],
|
||||
chapters: [],
|
||||
});
|
||||
const [state, setState] =
|
||||
useState<Omit<ReaderStateChapters, 'setReaderStateChapters'>>(READER_STATE_CHAPTERS_DEFAULTS);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
120
src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts
Normal file
120
src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 { useLayoutEffect } from 'react';
|
||||
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
||||
import { ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
|
||||
|
||||
const OPEN_CHAPTER_INTERSECTION_RATIO = 0.1;
|
||||
|
||||
const getScrollingDirection = (
|
||||
readingMode: ReadingMode,
|
||||
readingDirection: ReadingDirection,
|
||||
{ bottom, left, right }: DOMRect,
|
||||
): 'backward' | 'forward' => {
|
||||
if (isContinuousVerticalReadingMode(readingMode)) {
|
||||
const isScrollingBackward = bottom >= window.innerHeight;
|
||||
if (isScrollingBackward) {
|
||||
return 'backward';
|
||||
}
|
||||
|
||||
return 'forward';
|
||||
}
|
||||
|
||||
const themeDirectionOfReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
||||
const endOfElement = getOptionForDirection(right, left, themeDirectionOfReadingDirection);
|
||||
|
||||
const isScrollingBackward =
|
||||
endOfElement >= getOptionForDirection(window.innerWidth, 0, themeDirectionOfReadingDirection);
|
||||
if (isScrollingBackward) {
|
||||
return 'backward';
|
||||
}
|
||||
|
||||
return 'forward';
|
||||
};
|
||||
|
||||
export const useReaderInfiniteScrollUpdateChapter = (
|
||||
pageType: 'first' | 'last',
|
||||
chapterId: number,
|
||||
chapterToOpenId: number | undefined,
|
||||
isCurrentChapter: boolean,
|
||||
isChapterToOpenVisible: boolean,
|
||||
readingMode: ReadingMode,
|
||||
readingDirection: ReadingDirection,
|
||||
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>,
|
||||
image: HTMLElement | null,
|
||||
) => {
|
||||
useLayoutEffect(() => {
|
||||
if (!image || !isContinuousReadingMode(readingMode) || chapterToOpenId === undefined) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// gets immediately observed once on initial render
|
||||
let isInitialObserve = true;
|
||||
let wasNextChapterOpened = false;
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (isInitialObserve) {
|
||||
isInitialObserve = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wasNextChapterOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = entries[entries.length - 1];
|
||||
|
||||
const wasPageScrolledOutOfView = entry.intersectionRatio < OPEN_CHAPTER_INTERSECTION_RATIO;
|
||||
const isScrollingBackward =
|
||||
getScrollingDirection(readingMode, readingDirection, entry.target.getBoundingClientRect()) ===
|
||||
'backward';
|
||||
const isScrollingForward = !isScrollingBackward;
|
||||
|
||||
// the first page only opens the previous chapter in case it hasn't been loaded yet, otherwise, the last
|
||||
// page handles setting the correct chapter
|
||||
const initialOpenPreviousChapter =
|
||||
pageType === 'first' && isScrollingBackward && !isChapterToOpenVisible;
|
||||
const openPreviousChapter = pageType === 'last' && !isCurrentChapter && isScrollingForward;
|
||||
const openNextChapter = pageType === 'last' && isScrollingForward && wasPageScrolledOutOfView;
|
||||
|
||||
const openChapterToOpen = initialOpenPreviousChapter || openNextChapter;
|
||||
if (openChapterToOpen) {
|
||||
openChapter(chapterToOpenId, !isChapterToOpenVisible, false);
|
||||
wasNextChapterOpened = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (openPreviousChapter) {
|
||||
openChapter(chapterId, false, false);
|
||||
wasNextChapterOpened = true;
|
||||
}
|
||||
},
|
||||
{ threshold: [OPEN_CHAPTER_INTERSECTION_RATIO] },
|
||||
);
|
||||
intersectionObserver.observe(image);
|
||||
|
||||
return () => intersectionObserver.unobserve(image);
|
||||
}, [
|
||||
pageType,
|
||||
chapterId,
|
||||
chapterToOpenId,
|
||||
isCurrentChapter,
|
||||
isChapterToOpenVisible,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
openChapter,
|
||||
image,
|
||||
]);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
|
||||
export const useReaderResetStates = (
|
||||
setManga: TReaderStateMangaContext['setManga'],
|
||||
@@ -36,10 +37,7 @@ export const useReaderResetStates = (
|
||||
useEffect(
|
||||
() => () => {
|
||||
setManga(undefined);
|
||||
setReaderStateChapters({
|
||||
mangaChapters: [],
|
||||
chapters: [],
|
||||
});
|
||||
setReaderStateChapters(READER_STATE_CHAPTERS_DEFAULTS);
|
||||
|
||||
setCurrentPageIndex(0);
|
||||
setPageToScrollToIndex(null);
|
||||
|
||||
@@ -12,10 +12,12 @@ import { DirectionOffset } from '@/Base.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { GetChaptersReaderQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { IReaderSettings, ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
|
||||
export const useReaderSetChaptersState = (
|
||||
chaptersResponse: ReturnType<typeof requestManager.useGetMangaChapters<GetChaptersReaderQuery>>,
|
||||
chapterSourceOrder: number,
|
||||
initialChapter: ReaderStateChapters['initialChapter'],
|
||||
chapterForDuplicatesHandling: ReaderStateChapters['chapterForDuplicatesHandling'],
|
||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||
shouldSkipDupChapters: IReaderSettings['shouldSkipDupChapters'],
|
||||
@@ -25,32 +27,55 @@ export const useReaderSetChaptersState = (
|
||||
const newCurrentChapter = newMangaChapters
|
||||
? (newMangaChapters[newMangaChapters.length - chapterSourceOrder] ?? null)
|
||||
: undefined;
|
||||
const newInitialChapter = initialChapter ?? newCurrentChapter;
|
||||
const newChapterForDuplicatesHandling = chapterForDuplicatesHandling ?? newCurrentChapter;
|
||||
|
||||
setReaderStateChapters({
|
||||
mangaChapters: newMangaChapters ?? [],
|
||||
chapters:
|
||||
newChapterForDuplicatesHandling && newMangaChapters
|
||||
? Chapters.removeDuplicates(newChapterForDuplicatesHandling, newMangaChapters)
|
||||
: [],
|
||||
chapterForDuplicatesHandling: newChapterForDuplicatesHandling,
|
||||
currentChapter: newCurrentChapter,
|
||||
nextChapter:
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
}),
|
||||
previousChapter:
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
}),
|
||||
const nextChapter =
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
});
|
||||
const previousChapter =
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
});
|
||||
|
||||
const hasInitialChapterChanged = newInitialChapter != null && newInitialChapter.id !== initialChapter?.id;
|
||||
|
||||
setReaderStateChapters((prevState) => {
|
||||
const hasCurrentChapterChanged = newCurrentChapter?.id !== prevState.currentChapter?.id;
|
||||
return {
|
||||
...prevState,
|
||||
mangaChapters: newMangaChapters ?? [],
|
||||
chapters:
|
||||
newChapterForDuplicatesHandling && newMangaChapters
|
||||
? Chapters.removeDuplicates(newChapterForDuplicatesHandling, newMangaChapters)
|
||||
: [],
|
||||
initialChapter: newInitialChapter,
|
||||
chapterForDuplicatesHandling: newChapterForDuplicatesHandling,
|
||||
currentChapter: newCurrentChapter,
|
||||
nextChapter,
|
||||
previousChapter,
|
||||
isCurrentChapterReady: hasCurrentChapterChanged ? false : prevState.isCurrentChapterReady,
|
||||
visibleChapters: hasInitialChapterChanged
|
||||
? {
|
||||
...READER_STATE_CHAPTERS_DEFAULTS.visibleChapters,
|
||||
lastLeadingChapterSourceOrder:
|
||||
newInitialChapter?.sourceOrder ??
|
||||
READER_STATE_CHAPTERS_DEFAULTS.visibleChapters.lastLeadingChapterSourceOrder,
|
||||
lastTrailingChapterSourceOrder:
|
||||
newInitialChapter?.sourceOrder ??
|
||||
READER_STATE_CHAPTERS_DEFAULTS.visibleChapters.lastTrailingChapterSourceOrder,
|
||||
}
|
||||
: prevState.visibleChapters,
|
||||
};
|
||||
});
|
||||
}, [chaptersResponse.data?.chapters.nodes, chapterSourceOrder, shouldSkipDupChapters]);
|
||||
}, [chaptersResponse.data?.chapters.nodes, chapterSourceOrder, shouldSkipDupChapters, initialChapter]);
|
||||
};
|
||||
|
||||
@@ -6,62 +6,78 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { getInitialReaderPageIndex } from '@/modules/reader/utils/Reader.utils.ts';
|
||||
import { createPagesData } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
||||
import {
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { READER_STATE_PAGES_DEFAULTS } from '@/modules/reader/constants/ReaderContext.constants.ts';
|
||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
|
||||
export const useReaderSetPagesState = (
|
||||
isCurrentChapter: boolean,
|
||||
pagesResponse: ReturnType<typeof requestManager.useGetChapterPagesFetch>[1],
|
||||
resumeMode: ReaderResumeMode,
|
||||
currentChapter: ReaderStateChapters['currentChapter'],
|
||||
lastPageRead: TChapterReader['lastPageRead'] | undefined,
|
||||
pages: ReaderStatePages['pages'],
|
||||
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
||||
pagesToSpreadState: ReaderPageSpreadState[],
|
||||
arePagesFetched: boolean,
|
||||
setArePagesFetched: (fetched: boolean) => void,
|
||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||
setTotalPages: ReaderStatePages['setTotalPages'],
|
||||
setPages: ReaderStatePages['setPages'],
|
||||
setPageUrls: ReaderStatePages['setPageUrls'],
|
||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
||||
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const previousPageData = useRef<string[]>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const pagesPayload = pagesResponse.data?.fetchChapterPages;
|
||||
if (pagesPayload) {
|
||||
const { pages } = pagesPayload;
|
||||
const newPages = pages.length ? pages : [''];
|
||||
if (!pagesPayload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialReaderPageIndex = getInitialReaderPageIndex(
|
||||
resumeMode,
|
||||
currentChapter?.lastPageRead ?? 0,
|
||||
newPages.length - 1,
|
||||
);
|
||||
const { pages: pagesFromResponse } = pagesPayload;
|
||||
const newPages = pages.length ? pagesFromResponse : [''];
|
||||
const initialReaderPageIndex = getInitialReaderPageIndex(resumeMode, lastPageRead ?? 0, newPages.length - 1);
|
||||
|
||||
const didPagesChange = previousPageData.current !== pagesPayload?.pages;
|
||||
if (didPagesChange) {
|
||||
previousPageData.current = pagesPayload.pages;
|
||||
const newPageData = createPagesData(newPages);
|
||||
|
||||
setArePagesFetched(true);
|
||||
setTotalPages(pagesPayload.chapter.pageCount);
|
||||
setPages(newPageData);
|
||||
setPageUrls(newPages);
|
||||
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
||||
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||
setCurrentPageIndex(initialReaderPageIndex);
|
||||
setPageToScrollToIndex(initialReaderPageIndex);
|
||||
} else {
|
||||
setArePagesFetched(false);
|
||||
setCurrentPageIndex(READER_STATE_PAGES_DEFAULTS.currentPageIndex);
|
||||
setPageToScrollToIndex(READER_STATE_PAGES_DEFAULTS.pageToScrollToIndex);
|
||||
setTotalPages(READER_STATE_PAGES_DEFAULTS.totalPages);
|
||||
setPages(READER_STATE_PAGES_DEFAULTS.pages);
|
||||
setPageUrls(READER_STATE_PAGES_DEFAULTS.pageUrls);
|
||||
setPageLoadStates(READER_STATE_PAGES_DEFAULTS.pageLoadStates);
|
||||
setPages(pages);
|
||||
setPageLoadStates(pageLoadStates);
|
||||
setPagesToSpreadState(pagesToSpreadState);
|
||||
}
|
||||
|
||||
setTotalPages(pagesPayload.pages.length);
|
||||
setPageUrls(newPages);
|
||||
setCurrentPageIndex(initialReaderPageIndex);
|
||||
setPageToScrollToIndex(initialReaderPageIndex);
|
||||
setReaderStateChapters((prevState) => ({
|
||||
...prevState,
|
||||
isCurrentChapterReady: arePagesFetched || didPagesChange,
|
||||
}));
|
||||
|
||||
setTransitionPageMode(ReaderTransitionPageMode.NONE);
|
||||
}, [pagesResponse.data?.fetchChapterPages?.pages]);
|
||||
}, [pagesResponse.data?.fetchChapterPages?.pages, isCurrentChapter]);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
@@ -33,7 +33,7 @@ import { ReaderHotkeys } from '@/modules/reader/components/ReaderHotkeys.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReaderResumeMode,
|
||||
ReaderOpenChapterLocationState,
|
||||
ReaderStateChapters,
|
||||
TReaderAutoScrollContext,
|
||||
TReaderStateMangaContext,
|
||||
@@ -49,7 +49,6 @@ import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAuto
|
||||
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
import { useReaderResetStates } from '@/modules/reader/hooks/useReaderResetStates.ts';
|
||||
import { useReaderSetPagesState } from '@/modules/reader/hooks/useReaderSetPagesState.ts';
|
||||
import { useReaderSetSettingsState } from '@/modules/reader/hooks/useReaderSetSettingsState.ts';
|
||||
import { useReaderShowSettingPreviewOnChange } from '@/modules/reader/hooks/useReaderShowSettingPreviewOnChange.ts';
|
||||
import { useReaderSetChaptersState } from '@/modules/reader/hooks/useReaderSetChaptersState.ts';
|
||||
@@ -70,9 +69,9 @@ const BaseReader = ({
|
||||
shouldShowReadingModePreview,
|
||||
shouldShowTapZoneLayoutPreview,
|
||||
setSettings,
|
||||
initialChapter,
|
||||
chapterForDuplicatesHandling,
|
||||
currentChapter,
|
||||
mangaChapters,
|
||||
setReaderStateChapters,
|
||||
firstPageUrl,
|
||||
totalPages,
|
||||
@@ -94,7 +93,10 @@ const BaseReader = ({
|
||||
'shouldSkipDupChapters' | 'backgroundColor' | 'shouldShowReadingModePreview' | 'shouldShowTapZoneLayoutPreview'
|
||||
> &
|
||||
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'tapZoneLayout' | 'tapZoneInvertMode'> &
|
||||
Pick<ReaderStateChapters, 'chapterForDuplicatesHandling' | 'currentChapter' | 'mangaChapters' | 'setReaderStateChapters'> &
|
||||
Pick<
|
||||
ReaderStateChapters,
|
||||
'initialChapter' | 'chapterForDuplicatesHandling' | 'currentChapter' | 'setReaderStateChapters'
|
||||
> &
|
||||
Pick<
|
||||
ReaderStatePages,
|
||||
| 'totalPages'
|
||||
@@ -111,9 +113,7 @@ const BaseReader = ({
|
||||
cancelAutoScroll: TReaderAutoScrollContext['cancel'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { resumeMode } = useLocation<{
|
||||
resumeMode: ReaderResumeMode;
|
||||
}>().state ?? { resumeMode: ReaderResumeMode.START };
|
||||
const { updateInitialChapter } = useLocation<ReaderOpenChapterLocationState>().state ?? {};
|
||||
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -128,20 +128,6 @@ const BaseReader = ({
|
||||
|
||||
const mangaResponse = requestManager.useGetManga<GetMangaReaderQuery>(GET_MANGA_READER, mangaId);
|
||||
const chaptersResponse = requestManager.useGetMangaChapters<GetChaptersReaderQuery>(GET_CHAPTERS_READER, mangaId);
|
||||
const [arePagesFetched, setArePagesFetched] = useState(false);
|
||||
const [fetchPages, pagesResponse] = requestManager.useGetChapterPagesFetch(-1);
|
||||
|
||||
const doFetchPages = useCallback(() => {
|
||||
if (!currentChapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
setArePagesFetched(false);
|
||||
|
||||
fetchPages({ variables: { input: { chapterId: currentChapter.id } } }).catch(
|
||||
defaultPromiseErrorHandler('Reader::fetchPages'),
|
||||
);
|
||||
}, [fetchPages, currentChapter?.id]);
|
||||
|
||||
const {
|
||||
metadata: defaultSettingsMetadata,
|
||||
@@ -149,21 +135,13 @@ const BaseReader = ({
|
||||
request: defaultSettingsResponse,
|
||||
} = useDefaultReaderSettings();
|
||||
|
||||
const doesChapterExist =
|
||||
!chaptersResponse.loading &&
|
||||
!chaptersResponse.error &&
|
||||
chapterSourceOrder >= 0 &&
|
||||
chapterSourceOrder <= mangaChapters.length;
|
||||
|
||||
const isLoading =
|
||||
currentChapter === undefined ||
|
||||
!areSettingsSet ||
|
||||
mangaResponse.loading ||
|
||||
chaptersResponse.loading ||
|
||||
defaultSettingsResponse.loading ||
|
||||
pagesResponse.loading ||
|
||||
(doesChapterExist && !arePagesFetched && !chaptersResponse.error && !pagesResponse.error);
|
||||
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error ?? pagesResponse.error;
|
||||
defaultSettingsResponse.loading;
|
||||
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!manga || !currentChapter) {
|
||||
@@ -174,11 +152,6 @@ const BaseReader = ({
|
||||
setTitle(`${manga.title}: ${currentChapter.name}`);
|
||||
}, [t, mangaId, chapterSourceOrder, manga, currentChapter]);
|
||||
|
||||
useEffect(() => {
|
||||
doFetchPages();
|
||||
return () => setArePagesFetched(false);
|
||||
}, [currentChapter?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setManga(mangaResponse.data?.manga);
|
||||
}, [mangaResponse.data?.manga]);
|
||||
@@ -197,19 +170,6 @@ const BaseReader = ({
|
||||
setSettings,
|
||||
cancelAutoScroll,
|
||||
);
|
||||
useReaderSetPagesState(
|
||||
pagesResponse,
|
||||
resumeMode,
|
||||
currentChapter,
|
||||
setArePagesFetched,
|
||||
setTotalPages,
|
||||
setPages,
|
||||
setPageUrls,
|
||||
setPageLoadStates,
|
||||
setCurrentPageIndex,
|
||||
setPageToScrollToIndex,
|
||||
setTransitionPageMode,
|
||||
);
|
||||
useReaderSetSettingsState(
|
||||
mangaResponse,
|
||||
defaultSettingsResponse,
|
||||
@@ -232,6 +192,7 @@ const BaseReader = ({
|
||||
useReaderSetChaptersState(
|
||||
chaptersResponse,
|
||||
chapterSourceOrder,
|
||||
updateInitialChapter ? undefined : initialChapter,
|
||||
chapterForDuplicatesHandling,
|
||||
setReaderStateChapters,
|
||||
shouldSkipDupChapters,
|
||||
@@ -283,10 +244,6 @@ const BaseReader = ({
|
||||
if (chaptersResponse.error) {
|
||||
chaptersResponse.refetch().catch(defaultPromiseErrorHandler('Reader::refetchChapters'));
|
||||
}
|
||||
|
||||
if (pagesResponse.error) {
|
||||
doFetchPages();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -311,10 +268,6 @@ const BaseReader = ({
|
||||
return <EmptyViewAbsoluteCentered message={t('reader.error.label.chapter_not_found')} />;
|
||||
}
|
||||
|
||||
if (currentChapter && !currentChapter?.pageCount) {
|
||||
return <EmptyViewAbsoluteCentered message={t('reader.error.label.no_pages_found')} retry={doFetchPages} />;
|
||||
}
|
||||
|
||||
if (!manga || !currentChapter) {
|
||||
return null;
|
||||
}
|
||||
@@ -395,9 +348,9 @@ export const Reader = withPropsFrom(
|
||||
'shouldShowReadingModePreview',
|
||||
'shouldShowTapZoneLayoutPreview',
|
||||
'setSettings',
|
||||
'initialChapter',
|
||||
'chapterForDuplicatesHandling',
|
||||
'currentChapter',
|
||||
'mangaChapters',
|
||||
'setReaderStateChapters',
|
||||
'firstPageUrl',
|
||||
'totalPages',
|
||||
|
||||
@@ -169,7 +169,11 @@ export class ReaderControls {
|
||||
}
|
||||
}
|
||||
|
||||
static useOpenChapter(): (offset: 'previous' | 'next' | ChapterIdInfo['id'], doTransitionCheck?: boolean) => void {
|
||||
static useOpenChapter(): (
|
||||
offset: 'previous' | 'next' | ChapterIdInfo['id'],
|
||||
doTransitionCheck?: boolean,
|
||||
scrollIntoView?: boolean,
|
||||
) => void {
|
||||
const { t } = useTranslation();
|
||||
const { readingMode, shouldInformAboutMissingChapter, shouldInformAboutScanlatorChange } =
|
||||
ReaderService.useSettings();
|
||||
@@ -179,7 +183,7 @@ export class ReaderControls {
|
||||
const openChapter = ReaderService.useNavigateToChapter();
|
||||
|
||||
return useCallback(
|
||||
(offset, doTransitionCheck = true) => {
|
||||
(offset, doTransitionCheck = true, scrollIntoView = true) => {
|
||||
if (!currentChapter) {
|
||||
return;
|
||||
}
|
||||
@@ -227,7 +231,12 @@ export class ReaderControls {
|
||||
}
|
||||
|
||||
setReaderStateChapters((prevState) =>
|
||||
updateReaderStateVisibleChapters(isPreviousChapter, prevState, chapterToOpen.sourceOrder),
|
||||
updateReaderStateVisibleChapters(
|
||||
isPreviousChapter,
|
||||
prevState,
|
||||
chapterToOpen.sourceOrder,
|
||||
scrollIntoView,
|
||||
),
|
||||
);
|
||||
|
||||
openChapter(
|
||||
|
||||
@@ -189,9 +189,22 @@ export interface ReaderStateChapters {
|
||||
*/
|
||||
chapters: TChapterReader[];
|
||||
chapterForDuplicatesHandling?: TChapterReader | null;
|
||||
initialChapter?: TChapterReader | null;
|
||||
currentChapter?: TChapterReader | null;
|
||||
nextChapter?: TChapterReader;
|
||||
previousChapter?: TChapterReader;
|
||||
isCurrentChapterReady: boolean;
|
||||
/**
|
||||
* Based from the initial chapter index
|
||||
*/
|
||||
visibleChapters: {
|
||||
leading: number;
|
||||
trailing: number;
|
||||
lastLeadingChapterSourceOrder: number;
|
||||
lastTrailingChapterSourceOrder: number;
|
||||
scrollIntoView: boolean;
|
||||
resumeMode?: ReaderResumeMode;
|
||||
};
|
||||
setReaderStateChapters: React.Dispatch<React.SetStateAction<Omit<ReaderStateChapters, 'setReaderStateChapters'>>>;
|
||||
}
|
||||
|
||||
@@ -246,9 +259,12 @@ export interface ReaderPagerProps
|
||||
| 'pageLoadStates'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
> {
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
||||
onLoad?: (pagesIndex: number, url: string, isPrimary?: boolean) => void;
|
||||
onError?: (pageIndex: number, url: string) => void;
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
||||
isCurrentChapter: boolean;
|
||||
isPreviousChapter: boolean;
|
||||
isNextChapter: boolean;
|
||||
}
|
||||
|
||||
export enum PageInViewportType {
|
||||
@@ -269,6 +285,11 @@ export enum ReaderResumeMode {
|
||||
LAST_READ,
|
||||
}
|
||||
|
||||
export interface ReaderOpenChapterLocationState {
|
||||
resumeMode: ReaderResumeMode;
|
||||
updateInitialChapter?: boolean;
|
||||
}
|
||||
|
||||
export type TReaderScrollbarContext = {
|
||||
scrollbarXSize: number;
|
||||
setScrollbarXSize: (size: number) => void;
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { ReaderPageSpreadState, ReaderResumeMode, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReadingMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
||||
@@ -180,3 +185,112 @@ export const getReaderOpenChapterResumeMode = (
|
||||
|
||||
return ReaderResumeMode.START;
|
||||
};
|
||||
|
||||
export const createHandleReaderPageLoadError =
|
||||
(setPageLoadStates: ReaderStatePages['setPageLoadStates']) => (pageIndex: number, url: string) => {
|
||||
setPageLoadStates((statePageLoadStates) => {
|
||||
const pageLoadState = statePageLoadStates[pageIndex];
|
||||
|
||||
if (isPageOfOutdatedPageLoadStates(url, pageLoadState)) {
|
||||
return statePageLoadStates;
|
||||
}
|
||||
|
||||
return statePageLoadStates.toSpliced(pageIndex, 1, {
|
||||
...pageLoadState,
|
||||
loaded: false,
|
||||
error: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateReaderStateVisibleChapters = (
|
||||
isPreviousChapter: boolean,
|
||||
state: Omit<ReaderStateChapters, 'setReaderStateChapters'>,
|
||||
chapterToOpenSourceOrder: TChapterReader['sourceOrder'],
|
||||
scrollIntoView: boolean,
|
||||
): Omit<ReaderStateChapters, 'setReaderStateChapters'> => {
|
||||
const { leading, trailing, lastLeadingChapterSourceOrder, lastTrailingChapterSourceOrder } = state.visibleChapters;
|
||||
|
||||
const isNewLeadingChapter = isPreviousChapter && chapterToOpenSourceOrder < lastLeadingChapterSourceOrder;
|
||||
const isNewTrailingChapter = !isPreviousChapter && chapterToOpenSourceOrder > lastTrailingChapterSourceOrder;
|
||||
|
||||
return {
|
||||
...state,
|
||||
visibleChapters: {
|
||||
...state.visibleChapters,
|
||||
leading: leading + Number(isNewLeadingChapter),
|
||||
trailing: trailing + Number(isNewTrailingChapter),
|
||||
lastLeadingChapterSourceOrder: isNewLeadingChapter
|
||||
? chapterToOpenSourceOrder
|
||||
: lastLeadingChapterSourceOrder,
|
||||
lastTrailingChapterSourceOrder: isNewTrailingChapter
|
||||
? chapterToOpenSourceOrder
|
||||
: lastTrailingChapterSourceOrder,
|
||||
scrollIntoView,
|
||||
resumeMode: isPreviousChapter ? ReaderResumeMode.END : ReaderResumeMode.START,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getReaderChapterViewerCurrentPageIndex = (
|
||||
currentPageIndex: number,
|
||||
chapter: TChapterReader,
|
||||
currentChapter: TChapterReader,
|
||||
isCurrentChapter: boolean,
|
||||
isCurrentChapterReady: boolean,
|
||||
isLeadingChapter: boolean,
|
||||
isTrailingChapter: boolean,
|
||||
visibleChapters: ReaderStateChapters['visibleChapters'],
|
||||
): number => {
|
||||
if (isCurrentChapter) {
|
||||
if (isCurrentChapterReady) {
|
||||
return coerceIn(currentPageIndex, 0, chapter.pageCount - 1);
|
||||
}
|
||||
|
||||
if (visibleChapters.scrollIntoView && visibleChapters.resumeMode !== undefined) {
|
||||
return getInitialReaderPageIndex(visibleChapters.resumeMode, 0, chapter.pageCount - 1);
|
||||
}
|
||||
|
||||
if (isTrailingChapter) {
|
||||
return Math.max(0, chapter.pageCount - 1);
|
||||
}
|
||||
|
||||
if (isLeadingChapter) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const isPreviousChapter = chapter.sourceOrder < currentChapter.sourceOrder;
|
||||
if (isPreviousChapter) {
|
||||
return Math.max(chapter.pageCount - 1, 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const getReaderChapterViewResumeMode = (
|
||||
isCurrentChapter: boolean,
|
||||
isInitialChapter: boolean,
|
||||
isLeadingChapter: boolean,
|
||||
isTrailingChapter: boolean,
|
||||
forcedResumeMode: ReaderResumeMode | undefined,
|
||||
resumeMode: ReaderResumeMode = ReaderResumeMode.START,
|
||||
): ReaderResumeMode => {
|
||||
if (isCurrentChapter && forcedResumeMode !== undefined) {
|
||||
return forcedResumeMode;
|
||||
}
|
||||
|
||||
if (isInitialChapter) {
|
||||
return resumeMode;
|
||||
}
|
||||
|
||||
if (isLeadingChapter) {
|
||||
return ReaderResumeMode.END;
|
||||
}
|
||||
|
||||
if (isTrailingChapter) {
|
||||
return ReaderResumeMode.START;
|
||||
}
|
||||
|
||||
return ReaderResumeMode.START;
|
||||
};
|
||||
|
||||
@@ -505,6 +505,29 @@ export const isSpreadPage = (image: HTMLImageElement): boolean => {
|
||||
return aspectRatio < 1;
|
||||
};
|
||||
|
||||
const MIN_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT = 0;
|
||||
const MAX_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT = 1;
|
||||
const getImagePreLoadAmount = (
|
||||
isCurrentChapter: boolean,
|
||||
isPreviousChapter: boolean,
|
||||
isNextChapter: boolean,
|
||||
imagePreLoadAmount: number,
|
||||
): number => {
|
||||
if (isCurrentChapter) {
|
||||
return imagePreLoadAmount;
|
||||
}
|
||||
|
||||
if (isPreviousChapter || isNextChapter) {
|
||||
return coerceIn(
|
||||
MAX_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT,
|
||||
MIN_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT,
|
||||
imagePreLoadAmount,
|
||||
);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const PREVIOUS_IMAGE_LOAD_AMOUNT = 2;
|
||||
export const getPageIndexesToLoad = (
|
||||
currentPageIndex: number,
|
||||
@@ -512,20 +535,33 @@ export const getPageIndexesToLoad = (
|
||||
previousCurrentPageIndex: number,
|
||||
imagePreLoadAmount: number,
|
||||
readingMode: ReadingMode,
|
||||
isCurrentChapter: boolean,
|
||||
isPreviousChapter: boolean,
|
||||
isNextChapter: boolean,
|
||||
): number[] => {
|
||||
const currentPagesIndex = getPage(currentPageIndex, pages).pagesIndex;
|
||||
if (!isCurrentChapter && !isPreviousChapter && !isNextChapter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const directionInvert = previousCurrentPageIndex <= currentPageIndex ? 1 : -1;
|
||||
const currentPagesIndex = getPage(currentPageIndex, pages).pagesIndex;
|
||||
const finalImagePreLoadAmount = getImagePreLoadAmount(
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
imagePreLoadAmount,
|
||||
);
|
||||
|
||||
const directionInvert = previousCurrentPageIndex <= currentPageIndex && !isPreviousChapter ? 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 startPagesIndexTrailingIncluded = Math.max(
|
||||
0,
|
||||
currentPagesIndex - Math.min(PREVIOUS_IMAGE_LOAD_AMOUNT, imagePreLoadAmount) * directionInvert,
|
||||
currentPagesIndex - Math.min(PREVIOUS_IMAGE_LOAD_AMOUNT, finalImagePreLoadAmount) * directionInvert,
|
||||
);
|
||||
// do not load previous pages for continuous pagers to prevent layout shifts due to leading pages getting loaded
|
||||
const startPagesIndex = !isContinuousReadingMode(readingMode) ? startPagesIndexTrailingIncluded : currentPageIndex;
|
||||
const endPagesIndex = currentPagesIndex + imagePreLoadAmount * directionInvert;
|
||||
const pagesToRenderLength = Math.abs(endPagesIndex - startPagesIndex) + 1;
|
||||
const endPagesIndex = currentPagesIndex + finalImagePreLoadAmount * directionInvert;
|
||||
const pagesToRenderLength = Math.max(1, Math.abs(endPagesIndex - startPagesIndex));
|
||||
|
||||
return Array(pagesToRenderLength)
|
||||
.fill(1)
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
import { ReaderProgressBarProps, TReaderProgressCurrentPage } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { getOptionForDirection as getOptionForDirectionImpl } from '@/modules/theme/services/ThemeCreator.ts';
|
||||
import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
export const getPage = (pageIndex: number, pages: ReaderProgressBarProps['pages']): TReaderProgressCurrentPage => {
|
||||
const pagesIndex = pages.findIndex(({ primary, secondary }) =>
|
||||
[primary.index, secondary?.index].includes(pageIndex),
|
||||
);
|
||||
const page = pages[pagesIndex];
|
||||
const page = pages[coerceIn(pagesIndex, 0, pages.length - 1)];
|
||||
return {
|
||||
...page,
|
||||
pagesIndex,
|
||||
|
||||
Reference in New Issue
Block a user