Add infinite scroll to reader
This commit is contained in:
@@ -124,7 +124,7 @@ export const ChapterCard = memo((props: IProps) => {
|
|||||||
style={{
|
style={{
|
||||||
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
||||||
}}
|
}}
|
||||||
state={Chapters.getReaderOpenChapterLocationState(chapter)}
|
state={Chapters.getReaderOpenChapterLocationState(chapter, true)}
|
||||||
replace={mode === 'reader'}
|
replace={mode === 'reader'}
|
||||||
onClick={(e) => handleClick(e)}
|
onClick={(e) => handleClick(e)}
|
||||||
{...longPressBind(popupState.open)}
|
{...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 { DirectionOffset, TranslationKey } from '@/Base.types.ts';
|
||||||
import { MangaIdInfo } from '@/modules/manga/Manga.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 { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
|
||||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||||
import { DOWNLOAD_TYPE_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts';
|
import { DOWNLOAD_TYPE_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts';
|
||||||
@@ -460,9 +460,13 @@ export class Chapters {
|
|||||||
return ReaderResumeMode.LAST_READ;
|
return ReaderResumeMode.LAST_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getReaderOpenChapterLocationState(chapter: ChapterReadInfo): { resumeMode: ReaderResumeMode } {
|
static getReaderOpenChapterLocationState(
|
||||||
|
chapter: ChapterReadInfo,
|
||||||
|
updateInitialChapter?: boolean,
|
||||||
|
): ReaderOpenChapterLocationState {
|
||||||
return {
|
return {
|
||||||
resumeMode: Chapters.getReaderResumeMode(chapter),
|
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,
|
ForwardedRef,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||||
import {
|
import {
|
||||||
IReaderSettings,
|
IReaderSettings,
|
||||||
PageInViewportType,
|
PageInViewportType,
|
||||||
ReaderPageSpreadState,
|
ReaderOpenChapterLocationState,
|
||||||
ReaderTransitionPageMode,
|
ReaderResumeMode,
|
||||||
|
ReaderStateChapters,
|
||||||
ReadingDirection,
|
ReadingDirection,
|
||||||
ReadingMode,
|
ReadingMode,
|
||||||
TReaderScrollbarContext,
|
TReaderScrollbarContext,
|
||||||
} from '@/modules/reader/types/Reader.types.ts';
|
} 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 { getDoublePageModePages, isPageOfOutdatedPageLoadStates } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
|
||||||
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
|
||||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
||||||
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
||||||
import {
|
import {
|
||||||
getPagerForReadingMode,
|
|
||||||
isContinuousReadingMode,
|
isContinuousReadingMode,
|
||||||
isContinuousVerticalReadingMode,
|
isContinuousVerticalReadingMode,
|
||||||
shouldApplyReaderWidth,
|
shouldApplyReaderWidth,
|
||||||
@@ -48,7 +46,6 @@ import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.type
|
|||||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.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 { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||||
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
||||||
import { useReaderAutoScroll } from '@/modules/reader/hooks/useReaderAutoScroll.ts';
|
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 { useReaderHideCursorOnInactivity } from '@/modules/reader/hooks/useReaderHideCursorOnInactivity.ts';
|
||||||
import { useReaderScrollToStartOnPageChange } from '@/modules/reader/hooks/useReaderScrollToStartOnPageChange.ts';
|
import { useReaderScrollToStartOnPageChange } from '@/modules/reader/hooks/useReaderScrollToStartOnPageChange.ts';
|
||||||
import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHandlePageSelection.ts';
|
import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHandlePageSelection.ts';
|
||||||
import { useReaderConvertPagesForReadingMode } from '@/modules/reader/hooks/useReaderConvertPagesForReadingMode.ts';
|
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||||
import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.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> = {
|
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
|
||||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||||
@@ -75,18 +77,19 @@ const BaseReaderViewer = forwardRef(
|
|||||||
pageToScrollToIndex,
|
pageToScrollToIndex,
|
||||||
setPageToScrollToIndex,
|
setPageToScrollToIndex,
|
||||||
pages,
|
pages,
|
||||||
setPages,
|
|
||||||
totalPages,
|
totalPages,
|
||||||
pageUrls,
|
setPages,
|
||||||
pageLoadStates,
|
|
||||||
setPageLoadStates,
|
setPageLoadStates,
|
||||||
|
setTotalPages,
|
||||||
|
setCurrentPageIndex,
|
||||||
transitionPageMode,
|
transitionPageMode,
|
||||||
retryFailedPagesKeyPrefix,
|
retryFailedPagesKeyPrefix,
|
||||||
|
setTransitionPageMode,
|
||||||
readingMode,
|
readingMode,
|
||||||
shouldOffsetDoubleSpreads,
|
|
||||||
readingDirection,
|
readingDirection,
|
||||||
readerWidth,
|
readerWidth,
|
||||||
pageScaleMode,
|
pageScaleMode,
|
||||||
|
shouldOffsetDoubleSpreads,
|
||||||
setScrollbarXSize,
|
setScrollbarXSize,
|
||||||
setScrollbarYSize,
|
setScrollbarYSize,
|
||||||
isVisible: isOverlayVisible,
|
isVisible: isOverlayVisible,
|
||||||
@@ -94,26 +97,42 @@ const BaseReaderViewer = forwardRef(
|
|||||||
updateCurrentPageIndex,
|
updateCurrentPageIndex,
|
||||||
showPreview,
|
showPreview,
|
||||||
setShowPreview,
|
setShowPreview,
|
||||||
|
initialChapter,
|
||||||
|
currentChapter,
|
||||||
|
chapters,
|
||||||
|
visibleChapters,
|
||||||
|
setReaderStateChapters,
|
||||||
|
isCurrentChapterReady,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
ReaderStatePages,
|
ReaderStatePages,
|
||||||
| 'currentPageIndex'
|
| 'currentPageIndex'
|
||||||
| 'pageToScrollToIndex'
|
| 'pageToScrollToIndex'
|
||||||
| 'setPageToScrollToIndex'
|
| 'setPageToScrollToIndex'
|
||||||
| 'pages'
|
| 'pages'
|
||||||
| 'setPages'
|
|
||||||
| 'totalPages'
|
| 'totalPages'
|
||||||
| 'pageUrls'
|
| 'setPages'
|
||||||
| 'pageLoadStates'
|
|
||||||
| 'setPageLoadStates'
|
| 'setPageLoadStates'
|
||||||
|
| 'setTotalPages'
|
||||||
|
| 'setCurrentPageIndex'
|
||||||
| 'transitionPageMode'
|
| 'transitionPageMode'
|
||||||
| 'retryFailedPagesKeyPrefix'
|
| 'retryFailedPagesKeyPrefix'
|
||||||
|
| 'setTransitionPageMode'
|
||||||
> &
|
> &
|
||||||
Pick<
|
Pick<
|
||||||
IReaderSettings,
|
IReaderSettings,
|
||||||
'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode'
|
'readingMode' | 'readingDirection' | 'readerWidth' | 'pageScaleMode' | 'shouldOffsetDoubleSpreads'
|
||||||
> &
|
> &
|
||||||
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
|
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
|
||||||
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
||||||
|
Pick<
|
||||||
|
ReaderStateChapters,
|
||||||
|
| 'initialChapter'
|
||||||
|
| 'currentChapter'
|
||||||
|
| 'chapters'
|
||||||
|
| 'visibleChapters'
|
||||||
|
| 'setReaderStateChapters'
|
||||||
|
| 'isCurrentChapterReady'
|
||||||
|
> &
|
||||||
TReaderTapZoneContext & {
|
TReaderTapZoneContext & {
|
||||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
||||||
},
|
},
|
||||||
@@ -121,10 +140,14 @@ const BaseReaderViewer = forwardRef(
|
|||||||
ref: ForwardedRef<HTMLDivElement | null>,
|
ref: ForwardedRef<HTMLDivElement | null>,
|
||||||
) => {
|
) => {
|
||||||
const { direction: themeDirection } = useTheme();
|
const { direction: themeDirection } = useTheme();
|
||||||
|
const { resumeMode } = useLocation<ReaderOpenChapterLocationState>().state ?? {
|
||||||
|
resumeMode: ReaderResumeMode.START,
|
||||||
|
};
|
||||||
|
|
||||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
useImperativeHandle(ref, () => scrollElementRef.current!);
|
useImperativeHandle(ref, () => scrollElementRef.current!);
|
||||||
|
|
||||||
|
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
|
||||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||||
const isDragging = useMouseDragScroll(scrollElementRef);
|
const isDragging = useMouseDragScroll(scrollElementRef);
|
||||||
|
|
||||||
@@ -140,81 +163,27 @@ const BaseReaderViewer = forwardRef(
|
|||||||
|
|
||||||
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
|
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 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 inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
|
||||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||||
|
const initialChapterIndex = useMemo(
|
||||||
const onLoad = useMemo(
|
() => chapters.findIndex((chapter) => chapter.id === initialChapter?.id),
|
||||||
|
[initialChapter?.id],
|
||||||
|
);
|
||||||
|
const chaptersToRender = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createUpdateReaderPageLoadState(
|
chapters.slice(
|
||||||
actualPages,
|
Math.max(0, initialChapterIndex - visibleChapters.trailing),
|
||||||
pagesToSpreadState,
|
Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1),
|
||||||
setPagesToSpreadState,
|
|
||||||
pageLoadStates,
|
|
||||||
setPageLoadStates,
|
|
||||||
readingMode,
|
|
||||||
),
|
),
|
||||||
// do not add "pagesToSpreadState" and "pageLoadStates" as a dependency, otherwise, every page gets re-rendered
|
[chapters, initialChapterIndex, visibleChapters.trailing, visibleChapters.leading, chapters.length],
|
||||||
// when they change which impacts the performance massively (depending on the total page count)
|
);
|
||||||
[actualPages, readingMode],
|
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(
|
useReaderHandlePageSelection(
|
||||||
pageToScrollToIndex,
|
pageToScrollToIndex,
|
||||||
currentPageIndex,
|
currentPageIndex,
|
||||||
@@ -245,6 +214,10 @@ const BaseReaderViewer = forwardRef(
|
|||||||
);
|
);
|
||||||
useReaderAutoScroll(isOverlayVisible, automaticScrolling);
|
useReaderAutoScroll(isOverlayVisible, automaticScrolling);
|
||||||
|
|
||||||
|
if (!initialChapter || !currentChapter) {
|
||||||
|
throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
ref={scrollElementRef}
|
ref={scrollElementRef}
|
||||||
@@ -254,11 +227,10 @@ const BaseReaderViewer = forwardRef(
|
|||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
...applyStyles(
|
...applyStyles(
|
||||||
isContinuousVerticalReadingMode(readingMode) &&
|
isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||||
shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
|
||||||
{ alignItems: 'center' },
|
{ alignItems: 'center' },
|
||||||
),
|
),
|
||||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
...applyStyles(!isContinuousVerticalReadingModeActive, {
|
||||||
...applyStyles(themeDirection === 'ltr', {
|
...applyStyles(themeDirection === 'ltr', {
|
||||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||||
}),
|
}),
|
||||||
@@ -278,19 +250,78 @@ const BaseReaderViewer = forwardRef(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ReaderTransitionPage type={ReaderTransitionPageMode.PREVIOUS} />
|
{chaptersToRender.map((_, index) => {
|
||||||
<Pager
|
// chapters are sorted by latest to oldest, thus, loop over it in reversed order
|
||||||
totalPages={totalPages}
|
const chapterIndex = Math.max(0, chaptersToRender.length - index - 1);
|
||||||
currentPageIndex={currentPageIndex}
|
const chapter = chaptersToRender[chapterIndex];
|
||||||
pages={actualPages}
|
|
||||||
transitionPageMode={transitionPageMode}
|
const previousChapter =
|
||||||
pageLoadStates={pageLoadStates}
|
chaptersToRender[chapterIndex + 1] ??
|
||||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
chapters[initialChapterIndex + visibleChapters.leading + 1];
|
||||||
imageRefs={imageRefs}
|
const nextChapter =
|
||||||
onLoad={onLoad}
|
chaptersToRender[chapterIndex - 1] ??
|
||||||
onError={onError}
|
chapters[initialChapterIndex - visibleChapters.trailing - 1];
|
||||||
/>
|
|
||||||
<ReaderTransitionPage type={ReaderTransitionPageMode.NEXT} />
|
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>
|
</Stack>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -305,24 +336,26 @@ export const ReaderViewer = withPropsFrom(
|
|||||||
useReaderOverlayContext,
|
useReaderOverlayContext,
|
||||||
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
|
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
|
||||||
useReaderTapZoneContext,
|
useReaderTapZoneContext,
|
||||||
|
useReaderStateChaptersContext,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'currentPageIndex',
|
'currentPageIndex',
|
||||||
'pageToScrollToIndex',
|
'pageToScrollToIndex',
|
||||||
'setPageToScrollToIndex',
|
'setPageToScrollToIndex',
|
||||||
'pages',
|
'pages',
|
||||||
'setPages',
|
|
||||||
'totalPages',
|
'totalPages',
|
||||||
'pageUrls',
|
'setPages',
|
||||||
'pageLoadStates',
|
|
||||||
'setPageLoadStates',
|
'setPageLoadStates',
|
||||||
'transitionPageMode',
|
'setTotalPages',
|
||||||
|
'setCurrentPageIndex',
|
||||||
'retryFailedPagesKeyPrefix',
|
'retryFailedPagesKeyPrefix',
|
||||||
|
'setTransitionPageMode',
|
||||||
'readingMode',
|
'readingMode',
|
||||||
'shouldOffsetDoubleSpreads',
|
|
||||||
'readingDirection',
|
'readingDirection',
|
||||||
'readerWidth',
|
'readerWidth',
|
||||||
'pageScaleMode',
|
'pageScaleMode',
|
||||||
|
'shouldOffsetDoubleSpreads',
|
||||||
|
'transitionPageMode',
|
||||||
'setScrollbarXSize',
|
'setScrollbarXSize',
|
||||||
'setScrollbarYSize',
|
'setScrollbarYSize',
|
||||||
'isVisible',
|
'isVisible',
|
||||||
@@ -330,5 +363,11 @@ export const ReaderViewer = withPropsFrom(
|
|||||||
'updateCurrentPageIndex',
|
'updateCurrentPageIndex',
|
||||||
'showPreview',
|
'showPreview',
|
||||||
'setShowPreview',
|
'setShowPreview',
|
||||||
|
'initialChapter',
|
||||||
|
'currentChapter',
|
||||||
|
'chapters',
|
||||||
|
'visibleChapters',
|
||||||
|
'setReaderStateChapters',
|
||||||
|
'isCurrentChapterReady',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ const BaseBasePager = ({
|
|||||||
slots,
|
slots,
|
||||||
readingMode,
|
readingMode,
|
||||||
imagePreLoadAmount,
|
imagePreLoadAmount,
|
||||||
|
isCurrentChapter,
|
||||||
|
isPreviousChapter,
|
||||||
|
isNextChapter,
|
||||||
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
|
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
|
||||||
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
|
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
|
||||||
createPage: (
|
createPage: (
|
||||||
@@ -45,8 +48,11 @@ const BaseBasePager = ({
|
|||||||
previousCurrentPageIndex.current,
|
previousCurrentPageIndex.current,
|
||||||
imagePreLoadAmount,
|
imagePreLoadAmount,
|
||||||
readingMode,
|
readingMode,
|
||||||
|
isCurrentChapter,
|
||||||
|
isPreviousChapter,
|
||||||
|
isNextChapter,
|
||||||
),
|
),
|
||||||
[currentPageIndex, pages, imagePreLoadAmount, readingMode],
|
[currentPageIndex, pages, imagePreLoadAmount, readingMode, isCurrentChapter, isPreviousChapter, isNextChapter],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
previousCurrentPageIndex.current = currentPageIndex;
|
previousCurrentPageIndex.current = currentPageIndex;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const BaseReaderDoublePagedPager = ({
|
|||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
shouldLoad,
|
shouldLoad,
|
||||||
shouldDisplay && isPrimaryPage,
|
shouldDisplay && isPrimaryPage && shouldLoad,
|
||||||
currentPage.primary.index,
|
currentPage.primary.index,
|
||||||
totalPages,
|
totalPages,
|
||||||
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||||
@@ -97,7 +97,7 @@ const BaseReaderDoublePagedPager = ({
|
|||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
shouldLoad,
|
shouldLoad,
|
||||||
shouldDisplay && isSecondaryPage,
|
shouldDisplay && isSecondaryPage && shouldLoad,
|
||||||
currentSecondaryPageIndex,
|
currentSecondaryPageIndex,
|
||||||
totalPages,
|
totalPages,
|
||||||
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const BaseReaderPagedPager = ({
|
|||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
shouldLoad,
|
shouldLoad,
|
||||||
shouldDisplay && currentPageIndex === page.primary.index,
|
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
|
||||||
currentPageIndex,
|
currentPageIndex,
|
||||||
totalPages,
|
totalPages,
|
||||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||||
|
|||||||
@@ -9,9 +9,22 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
|
||||||
export const ReaderStateChaptersContext = createContext<ReaderStateChapters>({
|
export const READER_STATE_CHAPTERS_DEFAULTS: Omit<ReaderStateChapters, 'setReaderStateChapters'> = {
|
||||||
mangaChapters: [],
|
mangaChapters: [],
|
||||||
chapters: [],
|
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: () => {},
|
setReaderStateChapters: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReactNode, useMemo, useState } from 'react';
|
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';
|
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
|
||||||
export const ReaderStateChaptersContextProvider = ({ children }: { children: ReactNode }) => {
|
export const ReaderStateChaptersContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [state, setState] = useState<Omit<ReaderStateChapters, 'setReaderStateChapters'>>({
|
const [state, setState] =
|
||||||
mangaChapters: [],
|
useState<Omit<ReaderStateChapters, 'setReaderStateChapters'>>(READER_STATE_CHAPTERS_DEFAULTS);
|
||||||
chapters: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const value = useMemo(
|
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 { DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.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 = (
|
export const useReaderResetStates = (
|
||||||
setManga: TReaderStateMangaContext['setManga'],
|
setManga: TReaderStateMangaContext['setManga'],
|
||||||
@@ -36,10 +37,7 @@ export const useReaderResetStates = (
|
|||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
setManga(undefined);
|
setManga(undefined);
|
||||||
setReaderStateChapters({
|
setReaderStateChapters(READER_STATE_CHAPTERS_DEFAULTS);
|
||||||
mangaChapters: [],
|
|
||||||
chapters: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
setCurrentPageIndex(0);
|
setCurrentPageIndex(0);
|
||||||
setPageToScrollToIndex(null);
|
setPageToScrollToIndex(null);
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import { DirectionOffset } from '@/Base.types.ts';
|
|||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { GetChaptersReaderQuery } from '@/lib/graphql/generated/graphql.ts';
|
import { GetChaptersReaderQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { IReaderSettings, ReaderStateChapters } from '@/modules/reader/types/Reader.types.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 = (
|
export const useReaderSetChaptersState = (
|
||||||
chaptersResponse: ReturnType<typeof requestManager.useGetMangaChapters<GetChaptersReaderQuery>>,
|
chaptersResponse: ReturnType<typeof requestManager.useGetMangaChapters<GetChaptersReaderQuery>>,
|
||||||
chapterSourceOrder: number,
|
chapterSourceOrder: number,
|
||||||
|
initialChapter: ReaderStateChapters['initialChapter'],
|
||||||
chapterForDuplicatesHandling: ReaderStateChapters['chapterForDuplicatesHandling'],
|
chapterForDuplicatesHandling: ReaderStateChapters['chapterForDuplicatesHandling'],
|
||||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||||
shouldSkipDupChapters: IReaderSettings['shouldSkipDupChapters'],
|
shouldSkipDupChapters: IReaderSettings['shouldSkipDupChapters'],
|
||||||
@@ -25,32 +27,55 @@ export const useReaderSetChaptersState = (
|
|||||||
const newCurrentChapter = newMangaChapters
|
const newCurrentChapter = newMangaChapters
|
||||||
? (newMangaChapters[newMangaChapters.length - chapterSourceOrder] ?? null)
|
? (newMangaChapters[newMangaChapters.length - chapterSourceOrder] ?? null)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const newInitialChapter = initialChapter ?? newCurrentChapter;
|
||||||
const newChapterForDuplicatesHandling = chapterForDuplicatesHandling ?? newCurrentChapter;
|
const newChapterForDuplicatesHandling = chapterForDuplicatesHandling ?? newCurrentChapter;
|
||||||
|
|
||||||
setReaderStateChapters({
|
const nextChapter =
|
||||||
mangaChapters: newMangaChapters ?? [],
|
newMangaChapters &&
|
||||||
chapters:
|
newCurrentChapter &&
|
||||||
newChapterForDuplicatesHandling && newMangaChapters
|
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||||
? Chapters.removeDuplicates(newChapterForDuplicatesHandling, newMangaChapters)
|
offset: DirectionOffset.NEXT,
|
||||||
: [],
|
skipDupe: shouldSkipDupChapters,
|
||||||
chapterForDuplicatesHandling: newChapterForDuplicatesHandling,
|
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||||
currentChapter: newCurrentChapter,
|
});
|
||||||
nextChapter:
|
const previousChapter =
|
||||||
newMangaChapters &&
|
newMangaChapters &&
|
||||||
newCurrentChapter &&
|
newCurrentChapter &&
|
||||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||||
offset: DirectionOffset.NEXT,
|
offset: DirectionOffset.PREVIOUS,
|
||||||
skipDupe: shouldSkipDupChapters,
|
skipDupe: shouldSkipDupChapters,
|
||||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||||
}),
|
});
|
||||||
previousChapter:
|
|
||||||
newMangaChapters &&
|
const hasInitialChapterChanged = newInitialChapter != null && newInitialChapter.id !== initialChapter?.id;
|
||||||
newCurrentChapter &&
|
|
||||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
setReaderStateChapters((prevState) => {
|
||||||
offset: DirectionOffset.PREVIOUS,
|
const hasCurrentChapterChanged = newCurrentChapter?.id !== prevState.currentChapter?.id;
|
||||||
skipDupe: shouldSkipDupChapters,
|
return {
|
||||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
...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/.
|
* 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 { getInitialReaderPageIndex } from '@/modules/reader/utils/Reader.utils.ts';
|
||||||
import { createPagesData } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
import { createPagesData } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
||||||
import {
|
import {
|
||||||
|
ReaderPageSpreadState,
|
||||||
ReaderResumeMode,
|
ReaderResumeMode,
|
||||||
ReaderStateChapters,
|
ReaderStateChapters,
|
||||||
ReaderTransitionPageMode,
|
ReaderTransitionPageMode,
|
||||||
} from '@/modules/reader/types/Reader.types.ts';
|
} from '@/modules/reader/types/Reader.types.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.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 = (
|
export const useReaderSetPagesState = (
|
||||||
|
isCurrentChapter: boolean,
|
||||||
pagesResponse: ReturnType<typeof requestManager.useGetChapterPagesFetch>[1],
|
pagesResponse: ReturnType<typeof requestManager.useGetChapterPagesFetch>[1],
|
||||||
resumeMode: ReaderResumeMode,
|
resumeMode: ReaderResumeMode,
|
||||||
currentChapter: ReaderStateChapters['currentChapter'],
|
lastPageRead: TChapterReader['lastPageRead'] | undefined,
|
||||||
|
pages: ReaderStatePages['pages'],
|
||||||
|
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
||||||
|
pagesToSpreadState: ReaderPageSpreadState[],
|
||||||
|
arePagesFetched: boolean,
|
||||||
setArePagesFetched: (fetched: boolean) => void,
|
setArePagesFetched: (fetched: boolean) => void,
|
||||||
|
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||||
setTotalPages: ReaderStatePages['setTotalPages'],
|
setTotalPages: ReaderStatePages['setTotalPages'],
|
||||||
setPages: ReaderStatePages['setPages'],
|
setPages: ReaderStatePages['setPages'],
|
||||||
setPageUrls: ReaderStatePages['setPageUrls'],
|
setPageUrls: ReaderStatePages['setPageUrls'],
|
||||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||||
|
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
||||||
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
||||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||||
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
const previousPageData = useRef<string[]>();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
const pagesPayload = pagesResponse.data?.fetchChapterPages;
|
const pagesPayload = pagesResponse.data?.fetchChapterPages;
|
||||||
if (pagesPayload) {
|
if (!pagesPayload) {
|
||||||
const { pages } = pagesPayload;
|
return;
|
||||||
const newPages = pages.length ? pages : [''];
|
}
|
||||||
|
|
||||||
const initialReaderPageIndex = getInitialReaderPageIndex(
|
const { pages: pagesFromResponse } = pagesPayload;
|
||||||
resumeMode,
|
const newPages = pages.length ? pagesFromResponse : [''];
|
||||||
currentChapter?.lastPageRead ?? 0,
|
const initialReaderPageIndex = getInitialReaderPageIndex(resumeMode, lastPageRead ?? 0, newPages.length - 1);
|
||||||
newPages.length - 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const didPagesChange = previousPageData.current !== pagesPayload?.pages;
|
||||||
|
if (didPagesChange) {
|
||||||
|
previousPageData.current = pagesPayload.pages;
|
||||||
const newPageData = createPagesData(newPages);
|
const newPageData = createPagesData(newPages);
|
||||||
|
|
||||||
setArePagesFetched(true);
|
setArePagesFetched(true);
|
||||||
setTotalPages(pagesPayload.chapter.pageCount);
|
|
||||||
setPages(newPageData);
|
setPages(newPageData);
|
||||||
setPageUrls(newPages);
|
setPageUrls(newPages);
|
||||||
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
||||||
|
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||||
setCurrentPageIndex(initialReaderPageIndex);
|
setCurrentPageIndex(initialReaderPageIndex);
|
||||||
setPageToScrollToIndex(initialReaderPageIndex);
|
setPageToScrollToIndex(initialReaderPageIndex);
|
||||||
} else {
|
} else {
|
||||||
setArePagesFetched(false);
|
setPages(pages);
|
||||||
setCurrentPageIndex(READER_STATE_PAGES_DEFAULTS.currentPageIndex);
|
setPageLoadStates(pageLoadStates);
|
||||||
setPageToScrollToIndex(READER_STATE_PAGES_DEFAULTS.pageToScrollToIndex);
|
setPagesToSpreadState(pagesToSpreadState);
|
||||||
setTotalPages(READER_STATE_PAGES_DEFAULTS.totalPages);
|
|
||||||
setPages(READER_STATE_PAGES_DEFAULTS.pages);
|
|
||||||
setPageUrls(READER_STATE_PAGES_DEFAULTS.pageUrls);
|
|
||||||
setPageLoadStates(READER_STATE_PAGES_DEFAULTS.pageLoadStates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTotalPages(pagesPayload.pages.length);
|
||||||
|
setPageUrls(newPages);
|
||||||
|
setCurrentPageIndex(initialReaderPageIndex);
|
||||||
|
setPageToScrollToIndex(initialReaderPageIndex);
|
||||||
|
setReaderStateChapters((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
isCurrentChapterReady: arePagesFetched || didPagesChange,
|
||||||
|
}));
|
||||||
|
|
||||||
setTransitionPageMode(ReaderTransitionPageMode.NONE);
|
setTransitionPageMode(ReaderTransitionPageMode.NONE);
|
||||||
}, [pagesResponse.data?.fetchChapterPages?.pages]);
|
}, [pagesResponse.data?.fetchChapterPages?.pages, isCurrentChapter]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
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 { useLocation, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||||
@@ -33,7 +33,7 @@ import { ReaderHotkeys } from '@/modules/reader/components/ReaderHotkeys.tsx';
|
|||||||
import {
|
import {
|
||||||
IReaderSettings,
|
IReaderSettings,
|
||||||
IReaderSettingsWithDefaultFlag,
|
IReaderSettingsWithDefaultFlag,
|
||||||
ReaderResumeMode,
|
ReaderOpenChapterLocationState,
|
||||||
ReaderStateChapters,
|
ReaderStateChapters,
|
||||||
TReaderAutoScrollContext,
|
TReaderAutoScrollContext,
|
||||||
TReaderStateMangaContext,
|
TReaderStateMangaContext,
|
||||||
@@ -49,7 +49,6 @@ import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAuto
|
|||||||
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
|
||||||
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
|
||||||
import { useReaderResetStates } from '@/modules/reader/hooks/useReaderResetStates.ts';
|
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 { useReaderSetSettingsState } from '@/modules/reader/hooks/useReaderSetSettingsState.ts';
|
||||||
import { useReaderShowSettingPreviewOnChange } from '@/modules/reader/hooks/useReaderShowSettingPreviewOnChange.ts';
|
import { useReaderShowSettingPreviewOnChange } from '@/modules/reader/hooks/useReaderShowSettingPreviewOnChange.ts';
|
||||||
import { useReaderSetChaptersState } from '@/modules/reader/hooks/useReaderSetChaptersState.ts';
|
import { useReaderSetChaptersState } from '@/modules/reader/hooks/useReaderSetChaptersState.ts';
|
||||||
@@ -70,9 +69,9 @@ const BaseReader = ({
|
|||||||
shouldShowReadingModePreview,
|
shouldShowReadingModePreview,
|
||||||
shouldShowTapZoneLayoutPreview,
|
shouldShowTapZoneLayoutPreview,
|
||||||
setSettings,
|
setSettings,
|
||||||
|
initialChapter,
|
||||||
chapterForDuplicatesHandling,
|
chapterForDuplicatesHandling,
|
||||||
currentChapter,
|
currentChapter,
|
||||||
mangaChapters,
|
|
||||||
setReaderStateChapters,
|
setReaderStateChapters,
|
||||||
firstPageUrl,
|
firstPageUrl,
|
||||||
totalPages,
|
totalPages,
|
||||||
@@ -94,7 +93,10 @@ const BaseReader = ({
|
|||||||
'shouldSkipDupChapters' | 'backgroundColor' | 'shouldShowReadingModePreview' | 'shouldShowTapZoneLayoutPreview'
|
'shouldSkipDupChapters' | 'backgroundColor' | 'shouldShowReadingModePreview' | 'shouldShowTapZoneLayoutPreview'
|
||||||
> &
|
> &
|
||||||
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'tapZoneLayout' | 'tapZoneInvertMode'> &
|
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'tapZoneLayout' | 'tapZoneInvertMode'> &
|
||||||
Pick<ReaderStateChapters, 'chapterForDuplicatesHandling' | 'currentChapter' | 'mangaChapters' | 'setReaderStateChapters'> &
|
Pick<
|
||||||
|
ReaderStateChapters,
|
||||||
|
'initialChapter' | 'chapterForDuplicatesHandling' | 'currentChapter' | 'setReaderStateChapters'
|
||||||
|
> &
|
||||||
Pick<
|
Pick<
|
||||||
ReaderStatePages,
|
ReaderStatePages,
|
||||||
| 'totalPages'
|
| 'totalPages'
|
||||||
@@ -111,9 +113,7 @@ const BaseReader = ({
|
|||||||
cancelAutoScroll: TReaderAutoScrollContext['cancel'];
|
cancelAutoScroll: TReaderAutoScrollContext['cancel'];
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { resumeMode } = useLocation<{
|
const { updateInitialChapter } = useLocation<ReaderOpenChapterLocationState>().state ?? {};
|
||||||
resumeMode: ReaderResumeMode;
|
|
||||||
}>().state ?? { resumeMode: ReaderResumeMode.START };
|
|
||||||
|
|
||||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -128,20 +128,6 @@ const BaseReader = ({
|
|||||||
|
|
||||||
const mangaResponse = requestManager.useGetManga<GetMangaReaderQuery>(GET_MANGA_READER, mangaId);
|
const mangaResponse = requestManager.useGetManga<GetMangaReaderQuery>(GET_MANGA_READER, mangaId);
|
||||||
const chaptersResponse = requestManager.useGetMangaChapters<GetChaptersReaderQuery>(GET_CHAPTERS_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 {
|
const {
|
||||||
metadata: defaultSettingsMetadata,
|
metadata: defaultSettingsMetadata,
|
||||||
@@ -149,21 +135,13 @@ const BaseReader = ({
|
|||||||
request: defaultSettingsResponse,
|
request: defaultSettingsResponse,
|
||||||
} = useDefaultReaderSettings();
|
} = useDefaultReaderSettings();
|
||||||
|
|
||||||
const doesChapterExist =
|
|
||||||
!chaptersResponse.loading &&
|
|
||||||
!chaptersResponse.error &&
|
|
||||||
chapterSourceOrder >= 0 &&
|
|
||||||
chapterSourceOrder <= mangaChapters.length;
|
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
currentChapter === undefined ||
|
currentChapter === undefined ||
|
||||||
!areSettingsSet ||
|
!areSettingsSet ||
|
||||||
mangaResponse.loading ||
|
mangaResponse.loading ||
|
||||||
chaptersResponse.loading ||
|
chaptersResponse.loading ||
|
||||||
defaultSettingsResponse.loading ||
|
defaultSettingsResponse.loading;
|
||||||
pagesResponse.loading ||
|
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error;
|
||||||
(doesChapterExist && !arePagesFetched && !chaptersResponse.error && !pagesResponse.error);
|
|
||||||
const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error ?? pagesResponse.error;
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!manga || !currentChapter) {
|
if (!manga || !currentChapter) {
|
||||||
@@ -174,11 +152,6 @@ const BaseReader = ({
|
|||||||
setTitle(`${manga.title}: ${currentChapter.name}`);
|
setTitle(`${manga.title}: ${currentChapter.name}`);
|
||||||
}, [t, mangaId, chapterSourceOrder, manga, currentChapter]);
|
}, [t, mangaId, chapterSourceOrder, manga, currentChapter]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
doFetchPages();
|
|
||||||
return () => setArePagesFetched(false);
|
|
||||||
}, [currentChapter?.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setManga(mangaResponse.data?.manga);
|
setManga(mangaResponse.data?.manga);
|
||||||
}, [mangaResponse.data?.manga]);
|
}, [mangaResponse.data?.manga]);
|
||||||
@@ -197,19 +170,6 @@ const BaseReader = ({
|
|||||||
setSettings,
|
setSettings,
|
||||||
cancelAutoScroll,
|
cancelAutoScroll,
|
||||||
);
|
);
|
||||||
useReaderSetPagesState(
|
|
||||||
pagesResponse,
|
|
||||||
resumeMode,
|
|
||||||
currentChapter,
|
|
||||||
setArePagesFetched,
|
|
||||||
setTotalPages,
|
|
||||||
setPages,
|
|
||||||
setPageUrls,
|
|
||||||
setPageLoadStates,
|
|
||||||
setCurrentPageIndex,
|
|
||||||
setPageToScrollToIndex,
|
|
||||||
setTransitionPageMode,
|
|
||||||
);
|
|
||||||
useReaderSetSettingsState(
|
useReaderSetSettingsState(
|
||||||
mangaResponse,
|
mangaResponse,
|
||||||
defaultSettingsResponse,
|
defaultSettingsResponse,
|
||||||
@@ -232,6 +192,7 @@ const BaseReader = ({
|
|||||||
useReaderSetChaptersState(
|
useReaderSetChaptersState(
|
||||||
chaptersResponse,
|
chaptersResponse,
|
||||||
chapterSourceOrder,
|
chapterSourceOrder,
|
||||||
|
updateInitialChapter ? undefined : initialChapter,
|
||||||
chapterForDuplicatesHandling,
|
chapterForDuplicatesHandling,
|
||||||
setReaderStateChapters,
|
setReaderStateChapters,
|
||||||
shouldSkipDupChapters,
|
shouldSkipDupChapters,
|
||||||
@@ -283,10 +244,6 @@ const BaseReader = ({
|
|||||||
if (chaptersResponse.error) {
|
if (chaptersResponse.error) {
|
||||||
chaptersResponse.refetch().catch(defaultPromiseErrorHandler('Reader::refetchChapters'));
|
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')} />;
|
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) {
|
if (!manga || !currentChapter) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -395,9 +348,9 @@ export const Reader = withPropsFrom(
|
|||||||
'shouldShowReadingModePreview',
|
'shouldShowReadingModePreview',
|
||||||
'shouldShowTapZoneLayoutPreview',
|
'shouldShowTapZoneLayoutPreview',
|
||||||
'setSettings',
|
'setSettings',
|
||||||
|
'initialChapter',
|
||||||
'chapterForDuplicatesHandling',
|
'chapterForDuplicatesHandling',
|
||||||
'currentChapter',
|
'currentChapter',
|
||||||
'mangaChapters',
|
|
||||||
'setReaderStateChapters',
|
'setReaderStateChapters',
|
||||||
'firstPageUrl',
|
'firstPageUrl',
|
||||||
'totalPages',
|
'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 { t } = useTranslation();
|
||||||
const { readingMode, shouldInformAboutMissingChapter, shouldInformAboutScanlatorChange } =
|
const { readingMode, shouldInformAboutMissingChapter, shouldInformAboutScanlatorChange } =
|
||||||
ReaderService.useSettings();
|
ReaderService.useSettings();
|
||||||
@@ -179,7 +183,7 @@ export class ReaderControls {
|
|||||||
const openChapter = ReaderService.useNavigateToChapter();
|
const openChapter = ReaderService.useNavigateToChapter();
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(offset, doTransitionCheck = true) => {
|
(offset, doTransitionCheck = true, scrollIntoView = true) => {
|
||||||
if (!currentChapter) {
|
if (!currentChapter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -227,7 +231,12 @@ export class ReaderControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setReaderStateChapters((prevState) =>
|
setReaderStateChapters((prevState) =>
|
||||||
updateReaderStateVisibleChapters(isPreviousChapter, prevState, chapterToOpen.sourceOrder),
|
updateReaderStateVisibleChapters(
|
||||||
|
isPreviousChapter,
|
||||||
|
prevState,
|
||||||
|
chapterToOpen.sourceOrder,
|
||||||
|
scrollIntoView,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
openChapter(
|
openChapter(
|
||||||
|
|||||||
@@ -189,9 +189,22 @@ export interface ReaderStateChapters {
|
|||||||
*/
|
*/
|
||||||
chapters: TChapterReader[];
|
chapters: TChapterReader[];
|
||||||
chapterForDuplicatesHandling?: TChapterReader | null;
|
chapterForDuplicatesHandling?: TChapterReader | null;
|
||||||
|
initialChapter?: TChapterReader | null;
|
||||||
currentChapter?: TChapterReader | null;
|
currentChapter?: TChapterReader | null;
|
||||||
nextChapter?: TChapterReader;
|
nextChapter?: TChapterReader;
|
||||||
previousChapter?: 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'>>>;
|
setReaderStateChapters: React.Dispatch<React.SetStateAction<Omit<ReaderStateChapters, 'setReaderStateChapters'>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,9 +259,12 @@ export interface ReaderPagerProps
|
|||||||
| 'pageLoadStates'
|
| 'pageLoadStates'
|
||||||
| 'retryFailedPagesKeyPrefix'
|
| 'retryFailedPagesKeyPrefix'
|
||||||
> {
|
> {
|
||||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
|
||||||
onLoad?: (pagesIndex: number, url: string, isPrimary?: boolean) => void;
|
onLoad?: (pagesIndex: number, url: string, isPrimary?: boolean) => void;
|
||||||
onError?: (pageIndex: number, url: string) => void;
|
onError?: (pageIndex: number, url: string) => void;
|
||||||
|
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
||||||
|
isCurrentChapter: boolean;
|
||||||
|
isPreviousChapter: boolean;
|
||||||
|
isNextChapter: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PageInViewportType {
|
export enum PageInViewportType {
|
||||||
@@ -269,6 +285,11 @@ export enum ReaderResumeMode {
|
|||||||
LAST_READ,
|
LAST_READ,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReaderOpenChapterLocationState {
|
||||||
|
resumeMode: ReaderResumeMode;
|
||||||
|
updateInitialChapter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type TReaderScrollbarContext = {
|
export type TReaderScrollbarContext = {
|
||||||
scrollbarXSize: number;
|
scrollbarXSize: number;
|
||||||
setScrollbarXSize: (size: number) => void;
|
setScrollbarXSize: (size: number) => void;
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
* 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 { 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 { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||||
import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
||||||
@@ -180,3 +185,112 @@ export const getReaderOpenChapterResumeMode = (
|
|||||||
|
|
||||||
return ReaderResumeMode.START;
|
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;
|
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;
|
const PREVIOUS_IMAGE_LOAD_AMOUNT = 2;
|
||||||
export const getPageIndexesToLoad = (
|
export const getPageIndexesToLoad = (
|
||||||
currentPageIndex: number,
|
currentPageIndex: number,
|
||||||
@@ -512,20 +535,33 @@ export const getPageIndexesToLoad = (
|
|||||||
previousCurrentPageIndex: number,
|
previousCurrentPageIndex: number,
|
||||||
imagePreLoadAmount: number,
|
imagePreLoadAmount: number,
|
||||||
readingMode: ReadingMode,
|
readingMode: ReadingMode,
|
||||||
|
isCurrentChapter: boolean,
|
||||||
|
isPreviousChapter: boolean,
|
||||||
|
isNextChapter: boolean,
|
||||||
): number[] => {
|
): 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
|
// 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
|
// when going back to the previous pages
|
||||||
const startPagesIndexTrailingIncluded = Math.max(
|
const startPagesIndexTrailingIncluded = Math.max(
|
||||||
0,
|
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
|
// 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 startPagesIndex = !isContinuousReadingMode(readingMode) ? startPagesIndexTrailingIncluded : currentPageIndex;
|
||||||
const endPagesIndex = currentPagesIndex + imagePreLoadAmount * directionInvert;
|
const endPagesIndex = currentPagesIndex + finalImagePreLoadAmount * directionInvert;
|
||||||
const pagesToRenderLength = Math.abs(endPagesIndex - startPagesIndex) + 1;
|
const pagesToRenderLength = Math.max(1, Math.abs(endPagesIndex - startPagesIndex));
|
||||||
|
|
||||||
return Array(pagesToRenderLength)
|
return Array(pagesToRenderLength)
|
||||||
.fill(1)
|
.fill(1)
|
||||||
|
|||||||
@@ -9,12 +9,13 @@
|
|||||||
import { ReaderProgressBarProps, TReaderProgressCurrentPage } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
import { ReaderProgressBarProps, TReaderProgressCurrentPage } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
import { getOptionForDirection as getOptionForDirectionImpl } from '@/modules/theme/services/ThemeCreator.ts';
|
import { getOptionForDirection as getOptionForDirectionImpl } from '@/modules/theme/services/ThemeCreator.ts';
|
||||||
import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.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 => {
|
export const getPage = (pageIndex: number, pages: ReaderProgressBarProps['pages']): TReaderProgressCurrentPage => {
|
||||||
const pagesIndex = pages.findIndex(({ primary, secondary }) =>
|
const pagesIndex = pages.findIndex(({ primary, secondary }) =>
|
||||||
[primary.index, secondary?.index].includes(pageIndex),
|
[primary.index, secondary?.index].includes(pageIndex),
|
||||||
);
|
);
|
||||||
const page = pages[pagesIndex];
|
const page = pages[coerceIn(pagesIndex, 0, pages.length - 1)];
|
||||||
return {
|
return {
|
||||||
...page,
|
...page,
|
||||||
pagesIndex,
|
pagesIndex,
|
||||||
|
|||||||
Reference in New Issue
Block a user