Structure "reader" in sub-features
This commit is contained in:
466
src/features/reader/viewer/ReaderChapterViewer.tsx
Normal file
466
src/features/reader/viewer/ReaderChapterViewer.tsx
Normal file
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* 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,
|
||||
ReaderPagerProps,
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { getDoublePageModePages } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import {
|
||||
getPagerForReadingMode,
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { createHandleReaderPageLoadError, createUpdateReaderPageLoadState } from '@/features/reader/Reader.utils.ts';
|
||||
import { useReaderConvertPagesForReadingMode } from '@/features/reader/viewer/hooks/useReaderConvertPagesForReadingMode.ts';
|
||||
import { ReaderTransitionPage } from '@/features/reader/viewer/components/ReaderTransitionPage.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { READER_STATE_PAGES_DEFAULTS } from '@/features/reader/ReaderContext.constants.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useReaderSetPagesState } from '@/features/reader/viewer/hooks/useReaderSetPagesState.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/features/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { ReaderInfiniteScrollUpdateChapter } from '@/features/reader/infinite-scroll/ReaderInfiniteScrollUpdateChapter.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const BaseReaderChapterViewer = ({
|
||||
currentPageIndex,
|
||||
setPages: setContextPages,
|
||||
setPageLoadStates: setContextPageLoadStates,
|
||||
setTotalPages: setContextTotalPages,
|
||||
setCurrentPageIndex: setContextCurrentPageIndex,
|
||||
updateCurrentPageIndex,
|
||||
setPageToScrollToIndex,
|
||||
transitionPageMode,
|
||||
retryFailedPagesKeyPrefix,
|
||||
setTransitionPageMode,
|
||||
readingMode,
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
imagePreLoadAmount,
|
||||
pageGap,
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
nextChapterId,
|
||||
isPreviousChapterVisible,
|
||||
isNextChapterVisible,
|
||||
lastPageRead,
|
||||
isInitialChapter,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
isPreloadMode,
|
||||
imageRefs: globalImageRefs,
|
||||
scrollIntoView,
|
||||
setReaderStateChapters,
|
||||
resumeMode,
|
||||
customFilter,
|
||||
shouldStretchPage,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
onSizeChange,
|
||||
minWidth,
|
||||
minHeight,
|
||||
scrollElement,
|
||||
}: Pick<
|
||||
ReaderStatePages,
|
||||
| 'currentPageIndex'
|
||||
| 'setPages'
|
||||
| 'setPageLoadStates'
|
||||
| 'setTotalPages'
|
||||
| 'setCurrentPageIndex'
|
||||
| 'setPageToScrollToIndex'
|
||||
| 'transitionPageMode'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
| 'setTransitionPageMode'
|
||||
> &
|
||||
Omit<ReaderPagerProps, 'pages' | 'totalPages' | 'pageLoadStates' | 'handleAsInitialRender' | 'resumeMode'> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
| 'readingMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'readingDirection'
|
||||
| 'readerWidth'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldUseInfiniteScroll'
|
||||
> &
|
||||
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;
|
||||
onSizeChange: (width: number, height: number, chapterId: ChapterIdInfo['id']) => void;
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
scrollElement: HTMLElement | null;
|
||||
}) => {
|
||||
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 ref = useRef<HTMLDivElement>(null);
|
||||
const isCurrentChapterRef = useRef(isCurrentChapter);
|
||||
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
|
||||
const pagerRef = useRef<HTMLDivElement>(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) || isPreloadMode;
|
||||
|
||||
const isCurrentChapterInSinglePager = !isContinuousReadingModeActive && isCurrentChapter;
|
||||
const showPreviousTransitionPage =
|
||||
!shouldHideChapter &&
|
||||
(isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter)));
|
||||
const showNextTransitionPage =
|
||||
!shouldHideChapter &&
|
||||
(isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter)));
|
||||
|
||||
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,
|
||||
setPagesToSpreadState,
|
||||
(value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
setContextPageLoadStates(value);
|
||||
}
|
||||
|
||||
setPageLoadStates(value);
|
||||
},
|
||||
readingMode,
|
||||
),
|
||||
[actualPages, readingMode],
|
||||
);
|
||||
|
||||
const onError = useMemo(
|
||||
() =>
|
||||
createHandleReaderPageLoadError((value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
setContextPageLoadStates(value);
|
||||
}
|
||||
|
||||
setPageLoadStates(value);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
doFetchPages();
|
||||
}, [chapterId]);
|
||||
|
||||
useResizeObserver(
|
||||
ref,
|
||||
useCallback(
|
||||
(entries) => {
|
||||
const { clientWidth, clientHeight } = entries[0].target;
|
||||
onSizeChange(clientWidth, clientHeight, chapterId);
|
||||
},
|
||||
[onSizeChange, ref.current, chapterId],
|
||||
),
|
||||
);
|
||||
|
||||
const updateState = <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,
|
||||
(value) => updateState(value, noOp, setReaderStateChapters),
|
||||
(value) => updateState(value, setTotalPages, setContextTotalPages),
|
||||
(value) => updateState(value, setPages, setContextPages),
|
||||
(value) => updateState(value, setPageUrls, noOp),
|
||||
(value) => updateState(value, setPageLoadStates, setContextPageLoadStates),
|
||||
(value) => updateState(value, setPagesToSpreadState, noOp),
|
||||
(value) => updateState(value, noOp, setContextCurrentPageIndex),
|
||||
(value) => {
|
||||
if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
|
||||
setPageToScrollToIndex(value);
|
||||
setReaderStateChapters((prevState) => ({
|
||||
...prevState,
|
||||
visibleChapters: { ...prevState.visibleChapters, scrollIntoView: false, resumeMode: undefined },
|
||||
}));
|
||||
}
|
||||
},
|
||||
(value) => updateState(value, noOp, setTransitionPageMode),
|
||||
);
|
||||
|
||||
useReaderConvertPagesForReadingMode(
|
||||
currentPageIndex,
|
||||
actualPages,
|
||||
pageUrls,
|
||||
(value) => updateState(value, setPages, setContextPages, true),
|
||||
(value) => updateState(value, setPagesToSpreadState, noOp, true),
|
||||
(value) => updateState(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
|
||||
ref={ref}
|
||||
sx={{
|
||||
width: 'fit-content',
|
||||
height: 'fit-content',
|
||||
margin: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
minHeight,
|
||||
}),
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
minWidth,
|
||||
}),
|
||||
...applyStyles(shouldHideChapter, {
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
minWidth: 'unset',
|
||||
minHeight: 'unset',
|
||||
overflow: 'hidden',
|
||||
margin: 'unset',
|
||||
}),
|
||||
...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',
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{!isPreloadMode && (
|
||||
<ReaderInfiniteScrollUpdateChapter
|
||||
readingMode={readingMode}
|
||||
readingDirection={readingDirection}
|
||||
shouldUseInfiniteScroll={shouldUseInfiniteScroll}
|
||||
chapterId={chapterId}
|
||||
previousChapterId={previousChapterId}
|
||||
nextChapterId={nextChapterId}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapterVisible={isPreviousChapterVisible}
|
||||
isNextChapterVisible={isNextChapterVisible}
|
||||
imageWrapper={pagerRef.current}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
scrollElement={scrollElement}
|
||||
/>
|
||||
)}
|
||||
{showPreviousTransitionPage && (
|
||||
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.PREVIOUS} />
|
||||
)}
|
||||
<Pager
|
||||
ref={pagerRef}
|
||||
totalPages={totalPages}
|
||||
currentPageIndex={currentPageIndex}
|
||||
pages={actualPages}
|
||||
transitionPageMode={transitionPageMode}
|
||||
pageLoadStates={pageLoadStates}
|
||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
||||
imageRefs={imageRefs}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapter={isPreviousChapter}
|
||||
isNextChapter={isNextChapter}
|
||||
readingMode={readingMode}
|
||||
imagePreLoadAmount={imagePreLoadAmount}
|
||||
readingDirection={readingDirection}
|
||||
pageScaleMode={pageScaleMode}
|
||||
pageGap={pageGap}
|
||||
customFilter={customFilter}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
readerWidth={readerWidth}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
readerNavBarWidth={readerNavBarWidth}
|
||||
isPreloadMode={isPreloadMode}
|
||||
resumeMode={resumeMode}
|
||||
handleAsInitialRender={scrollIntoView}
|
||||
/>
|
||||
{showNextTransitionPage && (
|
||||
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.NEXT} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderChapterViewer = memo(BaseReaderChapterViewer);
|
||||
501
src/features/reader/viewer/ReaderViewer.tsx
Normal file
501
src/features/reader/viewer/ReaderViewer.tsx
Normal file
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
PageInViewportType,
|
||||
ReaderOpenChapterLocationState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { useMouseDragScroll } from '@/features/core/hooks/useMouseDragScroll.tsx';
|
||||
import { useReaderOverlayContext } from '@/features/reader/overlay/contexts/ReaderOverlayContext.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { TReaderOverlayContext } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderAutoScrollContext } from '@/features/reader/auto-scroll/contexts/ReaderAutoScrollContext.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
import { useReaderTapZoneContext } from '@/features/reader/tap-zones/contexts/ReaderTapZoneContext.tsx';
|
||||
import { useReaderAutoScroll } from '@/features/reader/auto-scroll/hooks/useReaderAutoScroll.ts';
|
||||
import { useReaderHideOverlayOnUserScroll } from '@/features/reader/overlay/hooks/useReaderHideOverlayOnUserScroll.ts';
|
||||
import { useReaderHorizontalModeInvertXYScrolling } from '@/features/reader/viewer/hooks/useReaderHorizontalModeInvertXYScrolling.ts';
|
||||
import { useReaderHideCursorOnInactivity } from '@/features/reader/viewer/hooks/useReaderHideCursorOnInactivity.ts';
|
||||
import { useReaderScrollToStartOnPageChange } from '@/features/reader/viewer/hooks/useReaderScrollToStartOnPageChange.ts';
|
||||
import { useReaderHandlePageSelection } from '@/features/reader/viewer/hooks/useReaderHandlePageSelection.ts';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderChapterViewer } from '@/features/reader/viewer/ReaderChapterViewer.tsx';
|
||||
import {
|
||||
getPreviousNextChapterVisibility,
|
||||
getReaderChapterViewerCurrentPageIndex,
|
||||
getReaderChapterViewResumeMode,
|
||||
} from '@/features/reader/Reader.utils.ts';
|
||||
import { coerceIn, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { useReaderPreserveScrollPosition } from '@/features/reader/viewer/hooks/useReaderPreserveScrollPosition.ts';
|
||||
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
|
||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||
[ReadingMode.DOUBLE_PAGE]: PageInViewportType.X,
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: PageInViewportType.Y,
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: PageInViewportType.X,
|
||||
[ReadingMode.WEBTOON]: PageInViewportType.Y,
|
||||
};
|
||||
|
||||
const BaseReaderViewer = forwardRef(
|
||||
(
|
||||
{
|
||||
currentPageIndex,
|
||||
pageToScrollToIndex,
|
||||
setPageToScrollToIndex,
|
||||
pages,
|
||||
totalPages,
|
||||
setPages,
|
||||
setPageLoadStates,
|
||||
setTotalPages,
|
||||
setCurrentPageIndex,
|
||||
transitionPageMode,
|
||||
retryFailedPagesKeyPrefix,
|
||||
setTransitionPageMode,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
imagePreLoadAmount,
|
||||
pageGap,
|
||||
customFilter,
|
||||
shouldStretchPage,
|
||||
isStaticNav,
|
||||
readerNavBarWidth,
|
||||
setScrollbarXSize,
|
||||
setScrollbarYSize,
|
||||
isVisible: isOverlayVisible,
|
||||
setIsVisible: setIsOverlayVisible,
|
||||
updateCurrentPageIndex,
|
||||
showPreview,
|
||||
setShowPreview,
|
||||
initialChapter,
|
||||
currentChapter,
|
||||
chapters,
|
||||
visibleChapters,
|
||||
setReaderStateChapters,
|
||||
isCurrentChapterReady,
|
||||
}: Pick<
|
||||
ReaderStatePages,
|
||||
| 'currentPageIndex'
|
||||
| 'pageToScrollToIndex'
|
||||
| 'setPageToScrollToIndex'
|
||||
| 'pages'
|
||||
| 'totalPages'
|
||||
| 'setPages'
|
||||
| 'setPageLoadStates'
|
||||
| 'setTotalPages'
|
||||
| 'setCurrentPageIndex'
|
||||
| 'transitionPageMode'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
| 'setTransitionPageMode'
|
||||
> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
| 'readingMode'
|
||||
| 'readingDirection'
|
||||
| 'shouldUseInfiniteScroll'
|
||||
| 'readerWidth'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'imagePreLoadAmount'
|
||||
| 'pageGap'
|
||||
| 'customFilter'
|
||||
| 'shouldStretchPage'
|
||||
| 'isStaticNav'
|
||||
> &
|
||||
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> &
|
||||
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
||||
Pick<
|
||||
ReaderStateChapters,
|
||||
| 'initialChapter'
|
||||
| 'currentChapter'
|
||||
| 'chapters'
|
||||
| 'visibleChapters'
|
||||
| 'setReaderStateChapters'
|
||||
| 'isCurrentChapterReady'
|
||||
> &
|
||||
TReaderTapZoneContext & {
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
||||
},
|
||||
|
||||
ref: ForwardedRef<HTMLDivElement | null>,
|
||||
) => {
|
||||
const { direction: themeDirection } = useTheme();
|
||||
const { resumeMode = ReaderResumeMode.START } = useLocation<ReaderOpenChapterLocationState>().state ?? {
|
||||
resumeMode: ReaderResumeMode.START,
|
||||
};
|
||||
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMergedRef(ref, scrollElementRef);
|
||||
|
||||
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const isDragging = useMouseDragScroll(scrollElementRef);
|
||||
|
||||
const automaticScrolling = useReaderAutoScrollContext();
|
||||
useEffect(() => automaticScrolling.setScrollRef(scrollElementRef), []);
|
||||
|
||||
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
|
||||
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
|
||||
useLayoutEffect(() => {
|
||||
setScrollbarXSize(scrollbarXSize);
|
||||
setScrollbarYSize(scrollbarYSize);
|
||||
}, [scrollbarXSize, scrollbarYSize]);
|
||||
|
||||
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
|
||||
|
||||
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
|
||||
const [{ minChapterViewWidth, minChapterViewHeight, minChapterSizeSourceChapterId }, setChapterViewerSize] =
|
||||
useState({
|
||||
minChapterViewWidth: 0,
|
||||
minChapterViewHeight: 0,
|
||||
minChapterSizeSourceChapterId: -1,
|
||||
});
|
||||
|
||||
const [, setTriggerReRender] = useState({});
|
||||
|
||||
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
const initialChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === initialChapter?.id),
|
||||
[chapters, initialChapter?.id],
|
||||
);
|
||||
const chaptersToRender = useMemo(
|
||||
() =>
|
||||
chapters.slice(
|
||||
Math.max(0, initialChapterIndex - visibleChapters.trailing),
|
||||
Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1),
|
||||
),
|
||||
[chapters, initialChapterIndex, visibleChapters.trailing, visibleChapters.leading],
|
||||
);
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chaptersToRender.findIndex((chapter) => chapter.id === currentChapter?.id),
|
||||
[currentChapter, chaptersToRender],
|
||||
);
|
||||
|
||||
const onChapterViewSizeChange = useCallback(
|
||||
(width: number, height: number, chapterId: ChapterIdInfo['id']) => {
|
||||
if (!isContinuousReadingModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSameChapterId = chapterId === minChapterSizeSourceChapterId;
|
||||
|
||||
if (isContinuousVerticalReadingModeActive) {
|
||||
if (!isSameChapterId && minChapterViewWidth >= width) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChapterViewerSize({
|
||||
minChapterViewWidth: width,
|
||||
minChapterViewHeight: 0,
|
||||
minChapterSizeSourceChapterId: chapterId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSameChapterId || minChapterViewHeight < height) {
|
||||
setChapterViewerSize({
|
||||
minChapterViewWidth: 0,
|
||||
minChapterViewHeight: height,
|
||||
minChapterSizeSourceChapterId: chapterId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
isContinuousReadingModeActive,
|
||||
isContinuousVerticalReadingModeActive,
|
||||
minChapterViewWidth,
|
||||
minChapterViewHeight,
|
||||
minChapterSizeSourceChapterId,
|
||||
],
|
||||
);
|
||||
|
||||
useReaderHandlePageSelection(
|
||||
pageToScrollToIndex,
|
||||
currentPageIndex,
|
||||
pages,
|
||||
totalPages,
|
||||
setPageToScrollToIndex,
|
||||
updateCurrentPageIndex,
|
||||
isContinuousReadingModeActive,
|
||||
imageRefs,
|
||||
themeDirection,
|
||||
readingDirection,
|
||||
);
|
||||
useReaderScrollToStartOnPageChange(
|
||||
currentPageIndex,
|
||||
isContinuousReadingModeActive,
|
||||
themeDirection,
|
||||
readingDirection,
|
||||
scrollElementRef,
|
||||
);
|
||||
useReaderHideCursorOnInactivity(scrollElementRef);
|
||||
useReaderHorizontalModeInvertXYScrolling(readingMode, readingDirection, scrollElementRef);
|
||||
useReaderHideOverlayOnUserScroll(
|
||||
isOverlayVisible,
|
||||
setIsOverlayVisible,
|
||||
showPreview,
|
||||
setShowPreview,
|
||||
scrollElementRef,
|
||||
);
|
||||
useReaderAutoScroll(isOverlayVisible, automaticScrolling, isStaticNav);
|
||||
useReaderPreserveScrollPosition(
|
||||
scrollElementRef,
|
||||
currentChapter?.id,
|
||||
currentChapterIndex,
|
||||
currentPageIndex,
|
||||
chaptersToRender,
|
||||
visibleChapters,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
setPageToScrollToIndex,
|
||||
pageScaleMode,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setChapterViewerSize({
|
||||
minChapterViewWidth: 0,
|
||||
minChapterViewHeight: 0,
|
||||
minChapterSizeSourceChapterId: -1,
|
||||
});
|
||||
setTriggerReRender({});
|
||||
}, [readingMode]);
|
||||
|
||||
if (!initialChapter || !currentChapter) {
|
||||
throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined');
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={mergedRef}
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(
|
||||
isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||
{ alignItems: 'center' },
|
||||
),
|
||||
...applyStyles(!isContinuousVerticalReadingModeActive, {
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
onClick={(e) => !isDragging && handleClick(e)}
|
||||
onScroll={() =>
|
||||
ReaderControls.updateCurrentPageOnScroll(
|
||||
imageRefs,
|
||||
totalPages - 1,
|
||||
updateCurrentPageIndex,
|
||||
inViewportType,
|
||||
readingDirection,
|
||||
)
|
||||
}
|
||||
>
|
||||
{chaptersToRender.map((_, index) => {
|
||||
// chapters are sorted by latest to oldest, thus, loop over it in reversed order
|
||||
const chapterIndex = Math.max(0, chaptersToRender.length - index - 1);
|
||||
const chapter = chaptersToRender[chapterIndex];
|
||||
|
||||
const previousChapter =
|
||||
chaptersToRender[chapterIndex + 1] ??
|
||||
chapters[initialChapterIndex + visibleChapters.leading + 1];
|
||||
const nextChapter =
|
||||
chaptersToRender[chapterIndex - 1] ??
|
||||
chapters[initialChapterIndex - visibleChapters.trailing - 1];
|
||||
|
||||
const isInitialChapter = chapter.id === initialChapter.id;
|
||||
const isCurrentChapter = chapter.id === currentChapter.id;
|
||||
const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id;
|
||||
const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id;
|
||||
const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder;
|
||||
const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder;
|
||||
const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder;
|
||||
const isLastTrailingChapter =
|
||||
visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder;
|
||||
const isPreloadMode =
|
||||
(isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) ||
|
||||
(isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode);
|
||||
|
||||
const previousNextChapterVisibility = getPreviousNextChapterVisibility(
|
||||
chapterIndex,
|
||||
chaptersToRender,
|
||||
visibleChapters,
|
||||
);
|
||||
|
||||
const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId;
|
||||
|
||||
return (
|
||||
<ReaderChapterViewer
|
||||
key={chapter.id}
|
||||
chapterId={chapter.id}
|
||||
previousChapterId={previousChapter?.id}
|
||||
nextChapterId={nextChapter?.id}
|
||||
isPreviousChapterVisible={previousNextChapterVisibility.previous}
|
||||
isNextChapterVisible={previousNextChapterVisibility.next}
|
||||
lastPageRead={coerceIn(chapter.lastPageRead, 0, chapter.pageCount - 1)}
|
||||
currentPageIndex={getReaderChapterViewerCurrentPageIndex(
|
||||
currentPageIndex,
|
||||
chapter,
|
||||
currentChapter,
|
||||
isCurrentChapter,
|
||||
isCurrentChapterReady,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
visibleChapters,
|
||||
)}
|
||||
isInitialChapter={isInitialChapter}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapter={isPreviousChapter}
|
||||
isNextChapter={isNextChapter}
|
||||
isLeadingChapter={isLeadingChapter}
|
||||
isTrailingChapter={isTrailingChapter}
|
||||
isPreloadMode={isPreloadMode}
|
||||
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}
|
||||
shouldUseInfiniteScroll={shouldUseInfiniteScroll}
|
||||
updateCurrentPageIndex={isCurrentChapter ? updateCurrentPageIndex : noOp}
|
||||
scrollIntoView={isCurrentChapter && visibleChapters.scrollIntoView}
|
||||
resumeMode={getReaderChapterViewResumeMode(
|
||||
isCurrentChapter,
|
||||
isInitialChapter,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
visibleChapters.resumeMode,
|
||||
resumeMode,
|
||||
)}
|
||||
setReaderStateChapters={setReaderStateChapters}
|
||||
setTransitionPageMode={setTransitionPageMode}
|
||||
pageGap={pageGap}
|
||||
imagePreLoadAmount={imagePreLoadAmount}
|
||||
customFilter={customFilter}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
readerNavBarWidth={readerNavBarWidth}
|
||||
onSizeChange={onChapterViewSizeChange}
|
||||
minWidth={isChapterSizeSourceChapter ? 0 : minChapterViewWidth}
|
||||
minHeight={isChapterSizeSourceChapter ? 0 : minChapterViewHeight}
|
||||
scrollElement={scrollElementRef.current}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderViewer = withPropsFrom(
|
||||
memo(BaseReaderViewer),
|
||||
[
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
useReaderScrollbarContext,
|
||||
useReaderOverlayContext,
|
||||
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
|
||||
useReaderTapZoneContext,
|
||||
useReaderStateChaptersContext,
|
||||
useNavBarContext,
|
||||
],
|
||||
[
|
||||
'currentPageIndex',
|
||||
'pageToScrollToIndex',
|
||||
'setPageToScrollToIndex',
|
||||
'pages',
|
||||
'totalPages',
|
||||
'setPages',
|
||||
'setPageLoadStates',
|
||||
'setTotalPages',
|
||||
'setCurrentPageIndex',
|
||||
'retryFailedPagesKeyPrefix',
|
||||
'setTransitionPageMode',
|
||||
'readingMode',
|
||||
'readingDirection',
|
||||
'shouldUseInfiniteScroll',
|
||||
'readerWidth',
|
||||
'pageScaleMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'imagePreLoadAmount',
|
||||
'pageGap',
|
||||
'customFilter',
|
||||
'shouldStretchPage',
|
||||
'isStaticNav',
|
||||
'readerNavBarWidth',
|
||||
'transitionPageMode',
|
||||
'setScrollbarXSize',
|
||||
'setScrollbarYSize',
|
||||
'isVisible',
|
||||
'setIsVisible',
|
||||
'updateCurrentPageIndex',
|
||||
'showPreview',
|
||||
'setShowPreview',
|
||||
'initialChapter',
|
||||
'currentChapter',
|
||||
'chapters',
|
||||
'visibleChapters',
|
||||
'setReaderStateChapters',
|
||||
'isCurrentChapterReady',
|
||||
],
|
||||
);
|
||||
170
src/features/reader/viewer/components/ReaderPage.tsx
Normal file
170
src/features/reader/viewer/components/ReaderPage.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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, useCallback } from 'react';
|
||||
import { SpinnerImage, SpinnerImageProps } from '@/features/core/components/SpinnerImage.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderCustomFilter,
|
||||
ReaderPagerProps,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
getImageMarginStyling,
|
||||
getImagePlaceholderStyling,
|
||||
getReaderImageStyling,
|
||||
} from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
|
||||
const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
|
||||
Object.keys(customFilter)
|
||||
.map((key) => {
|
||||
const filter = key as keyof ReaderCustomFilter;
|
||||
const value = customFilter[filter];
|
||||
|
||||
switch (filter) {
|
||||
case 'brightness':
|
||||
case 'contrast':
|
||||
case 'saturate':
|
||||
return (value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).enabled
|
||||
? `${filter}(${(value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).value / 100})`
|
||||
: '';
|
||||
case 'hue':
|
||||
return (value as ReaderCustomFilter['hue']).enabled
|
||||
? `hue-rotate(${(value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).value}deg)`
|
||||
: '';
|
||||
case 'rgba':
|
||||
return '';
|
||||
case 'sepia':
|
||||
case 'grayscale':
|
||||
case 'invert':
|
||||
return value ? `${filter}(${Number(value)})` : '';
|
||||
default:
|
||||
throw new Error(`Unexpected "CustomFilter" (${filter})`);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const BaseReaderPage = ({
|
||||
pageIndex,
|
||||
pagesIndex,
|
||||
isPrimaryPage,
|
||||
display,
|
||||
doublePage = false,
|
||||
position,
|
||||
marginTop,
|
||||
shouldLoad,
|
||||
readingMode,
|
||||
customFilter,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readerWidth,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
onLoad,
|
||||
onError,
|
||||
setRef,
|
||||
readerNavBarWidth,
|
||||
isLoaded,
|
||||
...props
|
||||
}: Omit<SpinnerImageProps, 'spinnerStyle' | 'imgStyle' | 'onLoad' | 'onError'> &
|
||||
Pick<IReaderSettings, 'readingMode' | 'customFilter' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> & {
|
||||
pageIndex: number;
|
||||
pagesIndex: number;
|
||||
isPrimaryPage: boolean;
|
||||
display: boolean;
|
||||
doublePage?: boolean;
|
||||
position?: 'left' | 'right';
|
||||
marginTop?: number;
|
||||
onLoad: ReaderPagerProps['onLoad'];
|
||||
onError: ReaderPagerProps['onError'];
|
||||
setRef?: (pagesIndex: number, ref: HTMLElement | null) => void;
|
||||
isLoaded?: boolean;
|
||||
}) => {
|
||||
const { src } = props;
|
||||
|
||||
const isTabletWidth = MediaQuery.useIsTabletWidth();
|
||||
|
||||
const handleLoad = useCallback(
|
||||
() => onLoad?.(pagesIndex, src, isPrimaryPage),
|
||||
[onLoad, pagesIndex, src, isPrimaryPage],
|
||||
);
|
||||
const handleError = useCallback(() => onError?.(pageIndex, src), [onError, pageIndex, src]);
|
||||
const updateRef = useCallback((element: HTMLElement | null) => setRef?.(pagesIndex, element), [pagesIndex, setRef]);
|
||||
|
||||
if (!display && !shouldLoad) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SpinnerImage
|
||||
{...props}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
shouldLoad={shouldLoad}
|
||||
shouldDecode
|
||||
ref={updateRef}
|
||||
spinnerStyle={{
|
||||
backgroundColor: 'background.paper',
|
||||
...getImagePlaceholderStyling(
|
||||
readingMode,
|
||||
shouldStretchPage,
|
||||
pageScaleMode,
|
||||
readerWidth,
|
||||
readerNavBarWidth + scrollbarYSize,
|
||||
scrollbarXSize,
|
||||
doublePage,
|
||||
isTabletWidth,
|
||||
),
|
||||
...applyStyles(!display, {
|
||||
display: 'none',
|
||||
}),
|
||||
...getImageMarginStyling(doublePage, position),
|
||||
}}
|
||||
imgStyle={{
|
||||
...getReaderImageStyling(
|
||||
readingMode,
|
||||
shouldStretchPage,
|
||||
pageScaleMode,
|
||||
doublePage,
|
||||
readerWidth,
|
||||
readerNavBarWidth + scrollbarYSize,
|
||||
scrollbarXSize,
|
||||
),
|
||||
filter: getCustomFilterString(customFilter),
|
||||
objectFit: 'contain',
|
||||
objectPosition: position,
|
||||
userSelect: 'none',
|
||||
...getImageMarginStyling(doublePage, position),
|
||||
...applyStyles(marginTop !== undefined, {
|
||||
mt: `${marginTop}px`,
|
||||
}),
|
||||
display: 'block',
|
||||
...applyStyles(!display, {
|
||||
display: 'none',
|
||||
}),
|
||||
...applyStyles(!isLoaded, {
|
||||
margin: 'unset',
|
||||
}),
|
||||
}}
|
||||
hideImgStyle={{
|
||||
visibility: 'hidden',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderPage = memo(BaseReaderPage);
|
||||
305
src/features/reader/viewer/components/ReaderTransitionPage.tsx
Normal file
305
src/features/reader/viewer/components/ReaderTransitionPage.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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 Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ComponentProps, memo, useMemo } from 'react';
|
||||
import { alpha, useTheme } from '@mui/material/styles';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingMode,
|
||||
TReaderScrollbarContext,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { isTransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { useBackButton } from '@/features/core/hooks/useBackButton.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { getValueFromObject, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const ChapterInfo = ({
|
||||
title,
|
||||
name,
|
||||
scanlator,
|
||||
backgroundColor,
|
||||
}: {
|
||||
title: string;
|
||||
name?: ChapterType['name'];
|
||||
scanlator?: ChapterType['scanlator'];
|
||||
backgroundColor: IReaderSettings['backgroundColor'];
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const contrastText = theme.palette.getContrastText(
|
||||
getValueFromObject(theme.palette, READER_BACKGROUND_TO_COLOR[backgroundColor]),
|
||||
);
|
||||
const disabledText = alpha(contrastText, 0.5);
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Typography color={contrastText}>{title}</Typography>
|
||||
<Typography color={contrastText} variant="h6" component="h1">
|
||||
{name}
|
||||
</Typography>
|
||||
{scanlator && (
|
||||
<Typography variant="body2" color={disabledText}>
|
||||
{scanlator}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseReaderTransitionPage = ({
|
||||
type,
|
||||
transitionPageMode,
|
||||
readingMode,
|
||||
backgroundColor,
|
||||
shouldShowTransitionPage,
|
||||
manga,
|
||||
currentChapterName,
|
||||
currentChapterScanlator,
|
||||
previousChapterName,
|
||||
previousChapterScanlator,
|
||||
nextChapterName,
|
||||
nextChapterScanlator,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
handleBack,
|
||||
}: Pick<IReaderSettings, 'readingMode' | 'backgroundColor' | 'shouldShowTransitionPage'> &
|
||||
Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
|
||||
Pick<ReaderStatePages, 'transitionPageMode'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> & {
|
||||
// gets used in the "source props creators" of the "withPropsFrom" call
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
currentChapterName?: ChapterType['name'];
|
||||
currentChapterScanlator?: ChapterType['scanlator'];
|
||||
previousChapterName?: ChapterType['name'];
|
||||
previousChapterScanlator?: ChapterType['scanlator'];
|
||||
nextChapterName?: ChapterType['name'];
|
||||
nextChapterScanlator?: ChapterType['scanlator'];
|
||||
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
|
||||
handleBack: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS;
|
||||
const isNextType = type === ReaderTransitionPageMode.NEXT;
|
||||
|
||||
const isFirstChapter = !!currentChapterName && !previousChapterName;
|
||||
const isLastChapter = !!currentChapterName && !nextChapterName;
|
||||
|
||||
const forceShowFirstChapterPreviousTransitionPage = isFirstChapter && type === ReaderTransitionPageMode.PREVIOUS;
|
||||
const forceShowLastChapterNextTransitionPage = isLastChapter && type === ReaderTransitionPageMode.NEXT;
|
||||
const forceShowTransitionPage =
|
||||
forceShowFirstChapterPreviousTransitionPage || forceShowLastChapterNextTransitionPage;
|
||||
|
||||
if (!shouldShowTransitionPage && !forceShowTransitionPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isTransitionPageVisible(type, transitionPageMode, readingMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...applyStyles(!isContinuousReadingMode(readingMode), {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}),
|
||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||
position: 'sticky',
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
left: 0,
|
||||
maxWidth: `calc(100vw - ${scrollbarYSize}px - ${readerNavBarWidth}px)`,
|
||||
minHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
}),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
top: 0,
|
||||
minWidth: `calc(100vw - ${scrollbarYSize}px - ${readerNavBarWidth}px)`,
|
||||
maxHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
gap: 2,
|
||||
maxWidth: (theme) =>
|
||||
// spacing = added padding left + right
|
||||
`calc(100vw - ${scrollbarYSize}px - ${readerNavBarWidth}px - ${theme.spacing(2)})`,
|
||||
maxHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
width: 'max-content',
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{isPreviousType && isFirstChapter && (
|
||||
<Typography variant="h6">{t('reader.transition_page.first_chapter')}</Typography>
|
||||
)}
|
||||
<Stack sx={{ gap: 5 }}>
|
||||
{isPreviousType && !isFirstChapter && (
|
||||
<ChapterInfo
|
||||
title={t('reader.transition_page.previous')}
|
||||
name={previousChapterName}
|
||||
scanlator={previousChapterScanlator}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
{!!currentChapterName && (
|
||||
<ChapterInfo
|
||||
title={t(
|
||||
isPreviousType ? 'reader.transition_page.current' : 'reader.transition_page.finished',
|
||||
)}
|
||||
name={currentChapterName}
|
||||
scanlator={currentChapterScanlator}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
{isNextType && !isLastChapter && (
|
||||
<ChapterInfo
|
||||
title={t('reader.transition_page.next')}
|
||||
name={nextChapterName}
|
||||
scanlator={nextChapterScanlator}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{isNextType && isLastChapter && (
|
||||
<Typography variant="h6">{t('reader.transition_page.last_chapter')}</Typography>
|
||||
)}
|
||||
{((isPreviousType && isFirstChapter) || (isNextType && isLastChapter)) && (
|
||||
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
|
||||
<Button
|
||||
sx={{ flexGrow: 1 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleBack();
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('reader.transition_page.exit.previous_page')}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ flexGrow: 1 }}
|
||||
component={Link}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
variant="contained"
|
||||
to={AppRoutes.manga.path(manga?.id ?? -1)}
|
||||
>
|
||||
{t('reader.transition_page.exit.manga_page')}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderTransitionPage = withPropsFrom(
|
||||
memo(BaseReaderTransitionPage) as typeof BaseReaderTransitionPage,
|
||||
[
|
||||
useReaderStateMangaContext,
|
||||
({ chapterId }: Pick<ComponentProps<typeof BaseReaderTransitionPage>, 'chapterId'>) => {
|
||||
const { chapters } = useReaderStateChaptersContext();
|
||||
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === chapterId),
|
||||
[chapterId, chapters],
|
||||
);
|
||||
const currentChapter = chapters[currentChapterIndex];
|
||||
// chapters are sorted from latest to oldest
|
||||
const previousChapter = useMemo(() => chapters[currentChapterIndex + 1], [currentChapterIndex, chapters]);
|
||||
const nextChapter = useMemo(() => chapters[currentChapterIndex - 1], [currentChapterIndex, chapters]);
|
||||
|
||||
return {
|
||||
currentChapterName: currentChapter?.name,
|
||||
currentChapterScanlator: currentChapter?.scanlator,
|
||||
previousChapterName: previousChapter?.name,
|
||||
previousChapterScanlator: previousChapter?.name,
|
||||
nextChapterName: nextChapter?.name,
|
||||
nextChapterScanlator: nextChapter?.scanlator,
|
||||
};
|
||||
},
|
||||
useReaderScrollbarContext,
|
||||
useNavBarContext,
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
({ chapterId, type }: Pick<ComponentProps<typeof BaseReaderTransitionPage>, 'chapterId' | 'type'>) => {
|
||||
const handleBack = useBackButton();
|
||||
const { chapters } = useReaderStateChaptersContext();
|
||||
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === chapterId),
|
||||
[chapterId, chapters],
|
||||
);
|
||||
|
||||
// chapters are sorted from latest to oldest
|
||||
const isLastChapter = currentChapterIndex === 0;
|
||||
const isFirstChapter = currentChapterIndex === chapters.length - 1;
|
||||
|
||||
const handleBackFirstChapter = type === ReaderTransitionPageMode.PREVIOUS && isFirstChapter;
|
||||
const handleBackLastChapter = type === ReaderTransitionPageMode.NEXT && isLastChapter;
|
||||
|
||||
const needsToHandleBack = handleBackFirstChapter || handleBackLastChapter;
|
||||
|
||||
return {
|
||||
handleBack: needsToHandleBack ? handleBack : noOp,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'manga',
|
||||
'currentChapterName',
|
||||
'currentChapterScanlator',
|
||||
'previousChapterName',
|
||||
'previousChapterScanlator',
|
||||
'nextChapterName',
|
||||
'nextChapterScanlator',
|
||||
'scrollbarXSize',
|
||||
'scrollbarYSize',
|
||||
'readerNavBarWidth',
|
||||
'backgroundColor',
|
||||
'transitionPageMode',
|
||||
'readingMode',
|
||||
'handleBack',
|
||||
'shouldShowTransitionPage',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { ReaderPageSpreadState, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { createPagesData } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
|
||||
export const useReaderConvertPagesForReadingMode = (
|
||||
currentPageIndex: number,
|
||||
actualPages: ReaderStatePages['pages'],
|
||||
pageUrls: ReaderStatePages['pageUrls'],
|
||||
setPages: ReaderStatePages['setPages'],
|
||||
setPagesToSpreadState: (states: ReaderPageSpreadState[]) => void,
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>,
|
||||
readingMode: ReadingMode,
|
||||
) => {
|
||||
const [wasDoublePageMode, setWasDoublePageMode] = useState(readingMode === ReadingMode.DOUBLE_PAGE);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const convertPagesToNormalPageMode = wasDoublePageMode && readingMode !== ReadingMode.DOUBLE_PAGE;
|
||||
if (convertPagesToNormalPageMode) {
|
||||
setWasDoublePageMode(false);
|
||||
|
||||
const newPageData = createPagesData(pageUrls);
|
||||
setPages(newPageData);
|
||||
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||
return;
|
||||
}
|
||||
|
||||
const convertPagesToDoublePageMode = readingMode === ReadingMode.DOUBLE_PAGE;
|
||||
if (convertPagesToDoublePageMode) {
|
||||
if (!wasDoublePageMode) {
|
||||
updateCurrentPageIndex(getNextIndexFromPage(getPage(currentPageIndex, actualPages)));
|
||||
}
|
||||
setPages(actualPages);
|
||||
setWasDoublePageMode(readingMode === ReadingMode.DOUBLE_PAGE);
|
||||
}
|
||||
}, [actualPages, readingMode]);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { MutableRefObject, useLayoutEffect } from 'react';
|
||||
import { Direction } from '@mui/material/styles';
|
||||
import { ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { DirectionOffset } from '@/Base.types.ts';
|
||||
import { getScrollIntoViewInlineOption } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
|
||||
export const useReaderHandlePageSelection = (
|
||||
pageToScrollToIndex: ReaderStatePages['pageToScrollToIndex'],
|
||||
currentPageIndex: number,
|
||||
pages: ReaderStatePages['pages'],
|
||||
totalPages: number,
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>,
|
||||
isContinuousReadingModeActive: boolean,
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>,
|
||||
themeDirection: Direction,
|
||||
readingDirection: ReadingDirection,
|
||||
) => {
|
||||
useLayoutEffect(() => {
|
||||
if (pageToScrollToIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageToScrollTo = getPage(pageToScrollToIndex, pages);
|
||||
|
||||
if (isContinuousReadingModeActive) {
|
||||
const directionOffset =
|
||||
pageToScrollToIndex > currentPageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT;
|
||||
const imageRef = imageRefs.current[pageToScrollTo.pagesIndex];
|
||||
|
||||
imageRef?.scrollIntoView({
|
||||
block: 'start',
|
||||
inline: getScrollIntoViewInlineOption(directionOffset, themeDirection, readingDirection),
|
||||
});
|
||||
}
|
||||
|
||||
const newPageIndex = getNextIndexFromPage(pageToScrollTo);
|
||||
const isLastPage = newPageIndex === totalPages - 1;
|
||||
|
||||
setPageToScrollToIndex(null);
|
||||
updateCurrentPageIndex(newPageIndex, !isLastPage, isLastPage);
|
||||
}, [pageToScrollToIndex]);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { MutableRefObject, useEffect, useRef } from 'react';
|
||||
|
||||
export const useReaderHideCursorOnInactivity = (scrollElementRef: MutableRefObject<HTMLDivElement | null>) => {
|
||||
const mouseInactiveTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const setCursorVisibility = (visible: boolean) => {
|
||||
const scrollElement = scrollElementRef.current;
|
||||
if (!scrollElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollElement.style.cursor = visible ? 'default' : 'none';
|
||||
};
|
||||
|
||||
const handleMouseMove = () => {
|
||||
setCursorVisibility(true);
|
||||
clearTimeout(mouseInactiveTimeout.current);
|
||||
mouseInactiveTimeout.current = setTimeout(() => {
|
||||
setCursorVisibility(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
handleMouseMove();
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
return () => {
|
||||
setCursorVisibility(true);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
clearTimeout(mouseInactiveTimeout.current);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { MutableRefObject, useEffect } from 'react';
|
||||
import { ReadingDirection, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
|
||||
export const useReaderHorizontalModeInvertXYScrolling = (
|
||||
readingMode: ReadingMode,
|
||||
readingDirection: ReadingDirection,
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>,
|
||||
) => {
|
||||
// invert x and y scrolling for the continuous horizontal reading mode
|
||||
useEffect(() => {
|
||||
if (readingMode !== ReadingMode.CONTINUOUS_HORIZONTAL) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (!scrollElementRef.current) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let previousDeltaX: number | undefined;
|
||||
let previousDeltaY: number | undefined;
|
||||
|
||||
// Trackpads can be identified by checking the delta values.
|
||||
// For a trackpad these values are not consistent since it basically behaves like scrolling via touch.
|
||||
// While for a mouse the values should always have the same value because each wheel turn scrolls the same exact amount.
|
||||
let isTrackpad: boolean | undefined;
|
||||
|
||||
const handleScroll = (e: WheelEvent) => {
|
||||
// Trackpads can scroll horizontally without the need of having "shift" pressed.
|
||||
// This is a problem because the hook will consider this to be a vertical scroll event and will invert it, which
|
||||
// breaks scrolling horizontally on trackpads.
|
||||
if (isTrackpad === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const isConsistentDeltaX = !Math.abs(Math.abs(previousDeltaX ?? e.deltaX) - Math.abs(e.deltaX));
|
||||
const isConsistentDeltaY = !Math.abs(Math.abs(previousDeltaY ?? e.deltaY) - Math.abs(e.deltaY));
|
||||
|
||||
previousDeltaX = e.deltaX;
|
||||
previousDeltaY = e.deltaY;
|
||||
|
||||
if (!isTrackpad) {
|
||||
isTrackpad = !isConsistentDeltaX || !isConsistentDeltaY;
|
||||
}
|
||||
|
||||
const preventInversion = isTrackpad === undefined;
|
||||
if (preventInversion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
scrollElementRef.current?.scrollBy({
|
||||
top: e.deltaY,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
scrollElementRef.current?.scrollBy({
|
||||
left: readingDirection === ReadingDirection.LTR ? e.deltaY : e.deltaY * -1,
|
||||
});
|
||||
};
|
||||
|
||||
scrollElementRef.current.addEventListener('wheel', handleScroll);
|
||||
return () => scrollElementRef.current?.removeEventListener('wheel', handleScroll);
|
||||
}, [readingMode, readingDirection]);
|
||||
};
|
||||
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* 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 { RefObject, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { useWindowEvent } from '@mantine/hooks';
|
||||
import {
|
||||
ReaderPageScaleMode,
|
||||
ReaderStateChapters,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
isHeightPageScaleMode,
|
||||
isReaderWidthEditable,
|
||||
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { getPreviousNextChapterVisibility } from '@/features/reader/Reader.utils.ts';
|
||||
import { ChapterIdInfo, TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const shouldPreserveOnResizeChange = (
|
||||
readingMode: ReadingMode,
|
||||
pageScaleMode: ReaderPageScaleMode,
|
||||
previousWidth: number,
|
||||
previousHeight: number,
|
||||
): boolean => {
|
||||
const isEditableReaderWidth = isReaderWidthEditable(pageScaleMode);
|
||||
const isHeightPageScaleModeActive = isHeightPageScaleMode(pageScaleMode);
|
||||
|
||||
const didWidthChange = previousWidth !== window.innerWidth;
|
||||
const didHeightChange = previousHeight !== window.innerHeight;
|
||||
|
||||
const handleWidthChange = isEditableReaderWidth && didWidthChange;
|
||||
const handleHeightChange = isHeightPageScaleModeActive && didHeightChange;
|
||||
|
||||
if (!isContinuousVerticalReadingMode(readingMode) && didWidthChange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return handleWidthChange || handleHeightChange;
|
||||
};
|
||||
|
||||
const usePreserveOnValueChange = (
|
||||
value: unknown,
|
||||
currentPageIndex: number,
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
) => {
|
||||
useLayoutEffect(() => {
|
||||
setPageToScrollToIndex(currentPageIndex);
|
||||
}, [value]);
|
||||
};
|
||||
|
||||
const usePreserveOnWindowResize = (
|
||||
readingMode: ReadingMode,
|
||||
pageScaleMode: ReaderPageScaleMode,
|
||||
setPageToScrollToIndex: React.Dispatch<React.SetStateAction<number | null>>,
|
||||
pageIndex: number,
|
||||
) => {
|
||||
const previousDimensionsRef = useRef({ width: window.innerWidth, height: window.innerHeight });
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
const { width, height } = previousDimensionsRef.current;
|
||||
previousDimensionsRef.current = { width: window.innerWidth, height: window.innerHeight };
|
||||
|
||||
if (!shouldPreserveOnResizeChange(readingMode, pageScaleMode, width, height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPageToScrollToIndex(pageIndex);
|
||||
}, [readingMode, pageScaleMode, pageIndex]);
|
||||
|
||||
useWindowEvent('resize', handleResize);
|
||||
};
|
||||
|
||||
interface ScrollPreservationInfo {
|
||||
left: number;
|
||||
top: number;
|
||||
visibleElement: HTMLElement | undefined;
|
||||
visibleElementLeft: number;
|
||||
visibleElementTop: number;
|
||||
}
|
||||
|
||||
const useScrollPreservationData = (
|
||||
scrollElementRef: RefObject<HTMLElement | null>,
|
||||
): RefObject<ScrollPreservationInfo> => {
|
||||
const dataRef = useRef<ScrollPreservationInfo>({
|
||||
left: 0,
|
||||
top: 0,
|
||||
visibleElement: undefined,
|
||||
visibleElementLeft: 0,
|
||||
visibleElementTop: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollElementRef.current;
|
||||
|
||||
if (!scrollElement) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
const { visibleElement } = dataRef.current;
|
||||
dataRef.current = {
|
||||
...dataRef.current,
|
||||
left: scrollElement.scrollLeft,
|
||||
top: scrollElement.scrollTop,
|
||||
visibleElementLeft: visibleElement?.offsetLeft ?? 0,
|
||||
visibleElementTop: visibleElement?.offsetTop ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
scrollElement.addEventListener('scroll', onScroll);
|
||||
|
||||
return () => scrollElement.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollElementRef.current;
|
||||
if (!scrollElement) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const updateObservation = (nodes: NodeList, intersectionAction: (element: Element) => void) =>
|
||||
Array.from(nodes)
|
||||
.filter((node) => node instanceof HTMLElement)
|
||||
.flatMap((element) =>
|
||||
element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
|
||||
)
|
||||
.forEach(intersectionAction);
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const firstVisibleElement = entries.find((entry) => entry.isIntersecting);
|
||||
|
||||
if (!(firstVisibleElement?.target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataRef.current = {
|
||||
...dataRef.current,
|
||||
visibleElement: firstVisibleElement.target,
|
||||
visibleElementLeft: firstVisibleElement.target.offsetLeft,
|
||||
visibleElementTop: firstVisibleElement.target.offsetTop,
|
||||
};
|
||||
});
|
||||
const mutationObserver = new MutationObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
updateObservation(entry.addedNodes, (element) => intersectionObserver.observe(element));
|
||||
updateObservation(entry.removedNodes, (element) => intersectionObserver.unobserve(element));
|
||||
}
|
||||
});
|
||||
mutationObserver.observe(scrollElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
intersectionObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return dataRef;
|
||||
};
|
||||
|
||||
const usePreserveOnLeadingPageRender = (scrollElementRef: RefObject<HTMLElement | null>, readingMode: ReadingMode) => {
|
||||
const preservationData = useScrollPreservationData(scrollElementRef);
|
||||
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollElementRef.current;
|
||||
|
||||
if (!scrollElement || !isContinuousReadingModeActive) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const preserveScrollPosition: ResizeObserverCallback = (entries) => {
|
||||
const { left, top, visibleElement, visibleElementLeft, visibleElementTop } = preservationData.current;
|
||||
|
||||
if (!visibleElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entriesBeforeScrollPosition = entries.filter((entry) => {
|
||||
if (!(entry.target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPreloadPage = !entry.target.clientWidth && !entry.target.clientHeight;
|
||||
if (isPreloadPage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isContinuousVerticalReadingModeActive) {
|
||||
return entry.target.offsetTop < top;
|
||||
}
|
||||
|
||||
return Math.abs(entry.target.offsetLeft) < Math.abs(left);
|
||||
});
|
||||
|
||||
const includesElementsBeforeScrollPosition = !!entriesBeforeScrollPosition.length;
|
||||
if (!includesElementsBeforeScrollPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newLeft = left - visibleElementLeft + visibleElement.offsetLeft;
|
||||
const newTop = top - visibleElementTop + visibleElement.offsetTop;
|
||||
|
||||
scrollElement.scrollTo(newLeft, newTop);
|
||||
};
|
||||
|
||||
const updateObservation = (nodes: NodeList, resizeAction: (element: Element) => void) =>
|
||||
Array.from(nodes)
|
||||
.filter((node) => node instanceof HTMLElement)
|
||||
.flatMap((element) =>
|
||||
element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
|
||||
)
|
||||
.forEach(resizeAction);
|
||||
|
||||
const resizeObserver = new ResizeObserver(preserveScrollPosition);
|
||||
const mutationObserver = new MutationObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
updateObservation(entry.addedNodes, (element) => resizeObserver.observe(element));
|
||||
updateObservation(entry.removedNodes, (element) => resizeObserver.unobserve(element));
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(scrollElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [isContinuousReadingModeActive, isContinuousVerticalReadingModeActive]);
|
||||
};
|
||||
|
||||
const usePreserveOnInfiniteScrollPreviousChapterInitialRender = (
|
||||
scrollElementRef: RefObject<HTMLElement | null>,
|
||||
currentChapterId: ChapterIdInfo['id'] | undefined,
|
||||
currentChapterIndex: number,
|
||||
currentPageIndex: number,
|
||||
chaptersToRender: TChapterReader[],
|
||||
visibleChapters: ReaderStateChapters['visibleChapters'],
|
||||
isContinuousReadingModeActive: boolean,
|
||||
) => {
|
||||
const preservationDataRef = useScrollPreservationData(scrollElementRef);
|
||||
|
||||
const preserveScrollPosition = (): boolean => {
|
||||
const scrollElement = scrollElementRef.current;
|
||||
const { left, top, visibleElementLeft, visibleElementTop, visibleElement } = preservationDataRef.current;
|
||||
|
||||
if (!scrollElement || !isContinuousReadingModeActive || !visibleElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousNextChapterVisibility = getPreviousNextChapterVisibility(
|
||||
currentChapterIndex,
|
||||
chaptersToRender,
|
||||
visibleChapters,
|
||||
);
|
||||
|
||||
const isRenderOfPreviousChapter = currentPageIndex === 0;
|
||||
|
||||
// only relevant when prepending content to the dom due to the resulting layout shift
|
||||
const isFirstRenderOfPreviousChapter = !previousNextChapterVisibility.previous && isRenderOfPreviousChapter;
|
||||
if (!isFirstRenderOfPreviousChapter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newLeft = left - visibleElementLeft + visibleElement.offsetLeft;
|
||||
const newTop = top - visibleElementTop + visibleElement.offsetTop;
|
||||
|
||||
scrollElement.scrollTo(newLeft, newTop);
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollElementRef.current;
|
||||
if (!scrollElement || !isContinuousReadingModeActive) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let preservedScrollPosition = false;
|
||||
|
||||
const updateObservation = (nodes: NodeList, resizeAction: (element: Element) => void) => {
|
||||
Array.from(nodes)
|
||||
.filter((node) => node instanceof HTMLElement)
|
||||
.flatMap((element) =>
|
||||
element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
|
||||
)
|
||||
.forEach(resizeAction);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (preservedScrollPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
preservedScrollPosition = preserveScrollPosition();
|
||||
if (preservedScrollPosition) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
});
|
||||
const mutationObserver = new MutationObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
updateObservation(entry.addedNodes, (element) => resizeObserver.observe(element));
|
||||
updateObservation(entry.removedNodes, (element) => resizeObserver.unobserve(element));
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(scrollElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [currentChapterId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
preserveScrollPosition();
|
||||
}, [currentChapterId]);
|
||||
};
|
||||
|
||||
export const useReaderPreserveScrollPosition = (
|
||||
scrollElementRef: RefObject<HTMLElement | null>,
|
||||
currentChapterId: ChapterIdInfo['id'] | undefined,
|
||||
currentChapterIndex: number,
|
||||
currentPageIndex: number,
|
||||
chaptersToRender: TChapterReader[],
|
||||
visibleChapters: ReaderStateChapters['visibleChapters'],
|
||||
readingMode: ReadingMode,
|
||||
readingDirection: ReadingDirection,
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
pageScaleMode: ReaderPageScaleMode,
|
||||
) => {
|
||||
usePreserveOnInfiniteScrollPreviousChapterInitialRender(
|
||||
scrollElementRef,
|
||||
currentChapterId,
|
||||
currentChapterIndex,
|
||||
currentPageIndex,
|
||||
chaptersToRender,
|
||||
visibleChapters,
|
||||
isContinuousReadingMode(readingMode),
|
||||
);
|
||||
usePreserveOnLeadingPageRender(scrollElementRef, readingMode);
|
||||
usePreserveOnWindowResize(readingMode, pageScaleMode, setPageToScrollToIndex, currentPageIndex);
|
||||
usePreserveOnValueChange(readingDirection, currentPageIndex, setPageToScrollToIndex);
|
||||
usePreserveOnValueChange(readingMode, currentPageIndex, setPageToScrollToIndex);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { MutableRefObject, useLayoutEffect } from 'react';
|
||||
import { Direction } from '@mui/material/styles';
|
||||
import { ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import { getScrollToXForReadingDirection } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
|
||||
export const useReaderScrollToStartOnPageChange = (
|
||||
currentPageIndex: ReaderStatePages['currentPageIndex'],
|
||||
isContinuousReadingModeActive: boolean,
|
||||
themeDirection: Direction,
|
||||
readingDirection: ReadingDirection,
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>,
|
||||
): void => {
|
||||
useLayoutEffect(() => {
|
||||
if (!isContinuousReadingModeActive) {
|
||||
scrollElementRef.current?.scrollTo(
|
||||
getScrollToXForReadingDirection(scrollElementRef.current, themeDirection, readingDirection),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}, [currentPageIndex]);
|
||||
};
|
||||
81
src/features/reader/viewer/hooks/useReaderSetPagesState.ts
Normal file
81
src/features/reader/viewer/hooks/useReaderSetPagesState.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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, useRef } from 'react';
|
||||
import { getInitialReaderPageIndex } from '@/features/reader/Reader.utils.ts';
|
||||
import { createPagesData } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import {
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
export const useReaderSetPagesState = (
|
||||
isCurrentChapter: boolean,
|
||||
pagesResponse: ReturnType<typeof requestManager.useGetChapterPagesFetch>[1],
|
||||
resumeMode: ReaderResumeMode,
|
||||
lastPageRead: TChapterReader['lastPageRead'] | undefined,
|
||||
pages: ReaderStatePages['pages'],
|
||||
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
||||
pagesToSpreadState: ReaderPageSpreadState[],
|
||||
arePagesFetched: boolean,
|
||||
setArePagesFetched: (fetched: boolean) => void,
|
||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||
setTotalPages: ReaderStatePages['setTotalPages'],
|
||||
setPages: ReaderStatePages['setPages'],
|
||||
setPageUrls: ReaderStatePages['setPageUrls'],
|
||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
||||
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
||||
) => {
|
||||
const previousPageData = useRef<string[]>(undefined);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const pagesPayload = pagesResponse.data?.fetchChapterPages;
|
||||
if (!pagesPayload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { pages: pagesFromResponse } = pagesPayload;
|
||||
const newPages = pagesFromResponse.length ? pagesFromResponse : [''];
|
||||
const initialReaderPageIndex = getInitialReaderPageIndex(resumeMode, lastPageRead ?? 0, newPages.length - 1);
|
||||
|
||||
const didPagesChange = previousPageData.current !== pagesPayload?.pages;
|
||||
if (didPagesChange) {
|
||||
previousPageData.current = pagesPayload.pages;
|
||||
const newPageData = createPagesData(newPages);
|
||||
|
||||
setArePagesFetched(true);
|
||||
setPages(newPageData);
|
||||
setPageUrls(newPages);
|
||||
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
||||
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||
} else {
|
||||
setPages(pages);
|
||||
setPageLoadStates(pageLoadStates);
|
||||
setPagesToSpreadState(pagesToSpreadState);
|
||||
}
|
||||
|
||||
setTotalPages(pagesPayload.pages.length);
|
||||
setPageUrls(newPages);
|
||||
setCurrentPageIndex(initialReaderPageIndex);
|
||||
setPageToScrollToIndex(initialReaderPageIndex);
|
||||
setReaderStateChapters((prevState) => ({
|
||||
...prevState,
|
||||
isCurrentChapterReady: arePagesFetched || didPagesChange,
|
||||
}));
|
||||
|
||||
setTransitionPageMode(ReaderTransitionPageMode.NONE);
|
||||
}, [pagesResponse.data?.fetchChapterPages?.pages, isCurrentChapter]);
|
||||
};
|
||||
634
src/features/reader/viewer/pager/ReaderPager.utils.tsx
Normal file
634
src/features/reader/viewer/pager/ReaderPager.utils.tsx
Normal file
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Direction, Theme } from '@mui/material/styles';
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import {
|
||||
IReaderSettings,
|
||||
PageInViewportType,
|
||||
ReaderPageScaleMode,
|
||||
ReaderPageSpreadState,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import {
|
||||
getSetReaderWidth,
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ReaderPage } from '@/features/reader/viewer/components/ReaderPage.tsx';
|
||||
import { reverseString } from '@/util/Strings.ts';
|
||||
import { getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { DirectionOffset } from '@/Base.types.ts';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
|
||||
type CSSObject = ReturnType<Theme['applyStyles']>;
|
||||
|
||||
const getPageWidthPercentage = (
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
isDoublePage: boolean,
|
||||
readerWidth: IReaderSettings['readerWidth'],
|
||||
isImage: boolean,
|
||||
): number => {
|
||||
if (shouldApplyReaderWidth(readerWidth, pageScaleMode)) {
|
||||
const width = readerWidth.value;
|
||||
if (isImage && isDoublePage) {
|
||||
return width / 100 / 2;
|
||||
}
|
||||
|
||||
return width / 100;
|
||||
}
|
||||
|
||||
if (isImage && isDoublePage) {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
export const getImagePlaceholderStyling = (
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
readerWidth: IReaderSettings['readerWidth'],
|
||||
widthOffset: number,
|
||||
heightOffset: number,
|
||||
isDoublePage?: boolean,
|
||||
isTabletWidth?: boolean,
|
||||
): CSSObject => {
|
||||
const OVER_9000 = 9000;
|
||||
|
||||
const getMaxWidth = (width: string) => `calc(${width} - ${widthOffset}px)`;
|
||||
const getDesktopWidth = (width: number, readerWidthValue?: number) =>
|
||||
getMaxWidth(`${coerceIn(readerWidthValue ?? width, Math.min(width, readerWidthValue ?? OVER_9000), width)}vw`);
|
||||
|
||||
const setReaderWidth = getSetReaderWidth(readerWidth, pageScaleMode);
|
||||
const fullWidth = getMaxWidth(`${Math.min(100, setReaderWidth ?? OVER_9000)}vw`);
|
||||
const fullHeight = `calc(100vh - ${heightOffset}px)`;
|
||||
|
||||
const DEFAULT_SINGLE_PAGE_WIDTH = isTabletWidth ? fullWidth : getDesktopWidth(40, setReaderWidth);
|
||||
const DEFAULT_SINGLE_PAGE_HEIGHT = isTabletWidth ? fullHeight : '85vh';
|
||||
|
||||
const READING_MODE_TO_PLACEHOLDER_SIZE: Record<ReadingMode, { minWidth: string; minHeight: string }> = {
|
||||
[ReadingMode.SINGLE_PAGE]: { minWidth: DEFAULT_SINGLE_PAGE_WIDTH, minHeight: DEFAULT_SINGLE_PAGE_HEIGHT },
|
||||
[ReadingMode.DOUBLE_PAGE]: {
|
||||
minWidth: isTabletWidth ? '50vw' : getDesktopWidth(35, setReaderWidth ? setReaderWidth / 2 : undefined),
|
||||
minHeight: DEFAULT_SINGLE_PAGE_HEIGHT,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
||||
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
||||
minHeight: '100vh',
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
||||
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
||||
minHeight: DEFAULT_SINGLE_PAGE_HEIGHT,
|
||||
},
|
||||
[ReadingMode.WEBTOON]: {
|
||||
minWidth: DEFAULT_SINGLE_PAGE_WIDTH,
|
||||
minHeight: '100vh',
|
||||
},
|
||||
};
|
||||
const defaultStyling = {
|
||||
...READING_MODE_TO_PLACEHOLDER_SIZE[readingMode],
|
||||
// the SpinnerImage placeholder has a default height of 100%, this causes the placeholder to take always take up 100% of the viewport
|
||||
height: 'unset',
|
||||
};
|
||||
const minWidthForStretch = isDoublePage ? `calc(${getMaxWidth(`${setReaderWidth ?? 100}vw`)} / 2)` : fullWidth;
|
||||
|
||||
switch (pageScaleMode) {
|
||||
case ReaderPageScaleMode.WIDTH:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: minWidthForStretch,
|
||||
}),
|
||||
};
|
||||
case ReaderPageScaleMode.HEIGHT:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minHeight: fullHeight,
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
minHeight: '100vh',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
case ReaderPageScaleMode.SCREEN:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: minWidthForStretch,
|
||||
minHeight: fullHeight,
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
minHeight: '100vh',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
case ReaderPageScaleMode.ORIGINAL:
|
||||
return {
|
||||
...defaultStyling,
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
height: undefined,
|
||||
}),
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unexpected "PageScaleMode" (${pageScaleMode})`);
|
||||
}
|
||||
};
|
||||
|
||||
const getReaderDimensionStyling = (
|
||||
widthPercentage: number,
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
widthOffset: number,
|
||||
heightOffset: number,
|
||||
): CSSObject => {
|
||||
const fullWidth = `calc((100vw - ${widthOffset}px) * ${widthPercentage})`;
|
||||
const fullHeight = `calc(100vh - ${heightOffset}px)`;
|
||||
|
||||
switch (pageScaleMode) {
|
||||
case ReaderPageScaleMode.WIDTH:
|
||||
return {
|
||||
minWidth: 0,
|
||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||
minWidth: 'unset',
|
||||
}),
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: fullWidth,
|
||||
}),
|
||||
maxWidth: fullWidth,
|
||||
};
|
||||
case ReaderPageScaleMode.HEIGHT:
|
||||
return {
|
||||
minHeight: 0,
|
||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||
minHeight: 'unset',
|
||||
}),
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minHeight: fullHeight,
|
||||
}),
|
||||
maxHeight: fullHeight,
|
||||
};
|
||||
case ReaderPageScaleMode.SCREEN:
|
||||
return {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||
minWidth: 'unset',
|
||||
minHeight: 'unset',
|
||||
}),
|
||||
...applyStyles(shouldStretchPage, {
|
||||
minWidth: fullWidth,
|
||||
minHeight: fullHeight,
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
minWidth: 'unset',
|
||||
minHeight: fullHeight,
|
||||
}),
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
minWidth: fullWidth,
|
||||
minHeight: 'unset',
|
||||
}),
|
||||
}),
|
||||
maxWidth: fullWidth,
|
||||
maxHeight: fullHeight,
|
||||
};
|
||||
case ReaderPageScaleMode.ORIGINAL:
|
||||
return {};
|
||||
default:
|
||||
throw new Error(`Unexpected "PageScaleMode" (${pageScaleMode})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getReaderImageStyling = (
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
isDoublePage: boolean,
|
||||
readerWidth: IReaderSettings['readerWidth'],
|
||||
widthOffset: number,
|
||||
heightOffset: number,
|
||||
): CSSObject => {
|
||||
const widthPercentage = getPageWidthPercentage(pageScaleMode, isDoublePage, readerWidth, true);
|
||||
return getReaderDimensionStyling(
|
||||
widthPercentage,
|
||||
readingMode,
|
||||
shouldStretchPage,
|
||||
pageScaleMode,
|
||||
widthOffset,
|
||||
heightOffset,
|
||||
);
|
||||
};
|
||||
|
||||
export const getImageMarginStyling = (doublePage: boolean, objectFitPosition?: 'left' | 'right'): CSSObject => ({
|
||||
m: 'auto',
|
||||
...applyStyles(doublePage, {
|
||||
// the margin on the object fit position needs to be removed so that there is no space between both images
|
||||
...applyStyles(objectFitPosition === 'right', { mr: 'unset ' }),
|
||||
...applyStyles(objectFitPosition === 'left', { ml: 'unset ' }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const createSinglePageData = (url: string, index: number): ReaderStatePages['pages'][number]['primary'] => ({
|
||||
index,
|
||||
alt: `Page #${index + 1}`,
|
||||
url: `${requestManager.getBaseUrl()}${url}`,
|
||||
});
|
||||
|
||||
export const createPageData = (url: string, index: number): ReaderStatePages['pages'][number] => ({
|
||||
name: `${index + 1}`,
|
||||
primary: createSinglePageData(url, index),
|
||||
});
|
||||
|
||||
export const createPagesData = (pageUrls: string[]): ReaderStatePages['pages'] => pageUrls.map(createPageData);
|
||||
|
||||
const getPageDownloadPriority = (
|
||||
currentPageIndex: number,
|
||||
pageIndex: number,
|
||||
totalPages: number,
|
||||
shouldLoad: boolean,
|
||||
isPreloadMode: boolean,
|
||||
): number => {
|
||||
if (!shouldLoad || isPreloadMode) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
const distanceToCurrentPage = Math.abs(pageIndex - currentPageIndex);
|
||||
const priorityBasedOnDistance = totalPages - distanceToCurrentPage;
|
||||
|
||||
const isPreviousPage = pageIndex < currentPageIndex;
|
||||
if (isPreviousPage) {
|
||||
return priorityBasedOnDistance - 1;
|
||||
}
|
||||
|
||||
return priorityBasedOnDistance;
|
||||
};
|
||||
|
||||
export const createReaderPage = (
|
||||
{ primary: { index, alt, url } }: ReaderStatePages['pages'][number],
|
||||
pagesIndex: number,
|
||||
isPrimaryPage: boolean,
|
||||
isLoaded: boolean,
|
||||
isPreloadMode: boolean,
|
||||
onLoad: ComponentProps<typeof ReaderPage>['onLoad'],
|
||||
onError: ComponentProps<typeof ReaderPage>['onError'],
|
||||
shouldLoad: boolean,
|
||||
display: boolean,
|
||||
currentPageIndex: number,
|
||||
totalPages: number,
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
customFilter: IReaderSettings['customFilter'],
|
||||
pageScaleMode: IReaderSettings['pageScaleMode'],
|
||||
shouldStretchPage: IReaderSettings['shouldStretchPage'],
|
||||
readerWidth: IReaderSettings['readerWidth'],
|
||||
scrollbarXSize: TReaderScrollbarContext['scrollbarXSize'],
|
||||
scrollbarYSize: TReaderScrollbarContext['scrollbarYSize'],
|
||||
readerNavBarWidth: NavbarContextType['readerNavBarWidth'],
|
||||
retryKeyPrefix?: string,
|
||||
position?: 'left' | 'right',
|
||||
isDoublePage?: boolean,
|
||||
marginTop?: number,
|
||||
setRef?: (pagesIndex: number, ref: HTMLElement | null) => void,
|
||||
): ReactNode => (
|
||||
<ReaderPage
|
||||
setRef={setRef}
|
||||
pageIndex={index}
|
||||
pagesIndex={pagesIndex}
|
||||
isPrimaryPage={isPrimaryPage}
|
||||
key={url}
|
||||
src={url}
|
||||
alt={alt}
|
||||
display={display}
|
||||
priority={getPageDownloadPriority(currentPageIndex, index, totalPages, shouldLoad, isPreloadMode)}
|
||||
position={position}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
doublePage={isDoublePage}
|
||||
shouldLoad={shouldLoad}
|
||||
retryKeyPrefix={retryKeyPrefix}
|
||||
marginTop={marginTop}
|
||||
isLoaded={isLoaded}
|
||||
readingMode={readingMode}
|
||||
customFilter={customFilter}
|
||||
pageScaleMode={pageScaleMode}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
readerWidth={readerWidth}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
readerNavBarWidth={readerNavBarWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
type InViewportThresholds = {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
};
|
||||
const getIsPageInViewportInfo = (
|
||||
element: HTMLElement,
|
||||
/**
|
||||
* Thresholds are not considered for the detection of an image filling the whole viewport.
|
||||
* They are only used for detecting if a specific side of an image is inside the viewport
|
||||
*/
|
||||
argThresholds?: InViewportThresholds,
|
||||
): {
|
||||
isLeftInViewport: boolean;
|
||||
isRightInViewport: boolean;
|
||||
isFillingWidthViewportCompletely: boolean;
|
||||
isTopInViewport: boolean;
|
||||
isBottomInViewport: boolean;
|
||||
isFillingHeightViewportCompletely: boolean;
|
||||
} => {
|
||||
const { top, bottom, left, right } = element.getBoundingClientRect();
|
||||
|
||||
const thresholds = {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
...argThresholds,
|
||||
};
|
||||
|
||||
const isLeftInViewport = left >= thresholds.left && left <= window.innerWidth;
|
||||
const isRightInViewport = right >= thresholds.right && right <= window.innerWidth;
|
||||
const isFillingWidthViewportCompletely = left <= 0 && right >= window.innerWidth;
|
||||
|
||||
const isTopInViewport = top >= thresholds.top && top <= window.innerHeight;
|
||||
const isBottomInViewport = bottom >= thresholds.bottom && bottom <= window.innerHeight;
|
||||
const isFillingHeightViewportCompletely = top <= 0 && bottom >= window.innerHeight;
|
||||
|
||||
return {
|
||||
isLeftInViewport,
|
||||
isRightInViewport,
|
||||
isFillingWidthViewportCompletely,
|
||||
isTopInViewport,
|
||||
isBottomInViewport,
|
||||
isFillingHeightViewportCompletely,
|
||||
};
|
||||
};
|
||||
|
||||
export const isPageInViewport = (
|
||||
element: HTMLElement,
|
||||
type: PageInViewportType,
|
||||
thresholds?: InViewportThresholds,
|
||||
): boolean => {
|
||||
const {
|
||||
isLeftInViewport,
|
||||
isRightInViewport,
|
||||
isFillingWidthViewportCompletely,
|
||||
isTopInViewport,
|
||||
isBottomInViewport,
|
||||
isFillingHeightViewportCompletely,
|
||||
} = getIsPageInViewportInfo(element, thresholds);
|
||||
|
||||
const isInViewportX = isLeftInViewport || isRightInViewport || isFillingWidthViewportCompletely;
|
||||
const isInViewportY = isTopInViewport || isBottomInViewport || isFillingHeightViewportCompletely;
|
||||
|
||||
switch (type) {
|
||||
case PageInViewportType.X:
|
||||
return isInViewportX;
|
||||
case PageInViewportType.Y:
|
||||
return isInViewportY;
|
||||
default:
|
||||
throw new Error(`unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const isEndOfPageInViewport = (
|
||||
element: HTMLElement,
|
||||
type: PageInViewportType,
|
||||
direction: ReadingDirection,
|
||||
): boolean => {
|
||||
const { isLeftInViewport, isRightInViewport, isBottomInViewport } = getIsPageInViewportInfo(element);
|
||||
|
||||
switch (type) {
|
||||
case PageInViewportType.X:
|
||||
return direction === ReadingDirection.LTR ? isRightInViewport : isLeftInViewport;
|
||||
case PageInViewportType.Y:
|
||||
return isBottomInViewport;
|
||||
default:
|
||||
throw new Error(`unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDoublePageModePages = (
|
||||
pageUrls: ReaderStatePages['pageUrls'],
|
||||
pagesToSpreadState: ReaderPageSpreadState[],
|
||||
shouldOffsetDoubleSpreads: IReaderSettings['shouldOffsetDoubleSpreads'],
|
||||
direction: ReadingDirection,
|
||||
): ReaderStatePages['pages'] => {
|
||||
const doublePageModePages: ReaderStatePages['pages'] = [];
|
||||
|
||||
// each spread page has to be counted as 2 pages and all trailing page numbers have to be increased by the count
|
||||
// of leading spread pages
|
||||
const pageToActualPageIndex = pagesToSpreadState.map(
|
||||
(_, page) => page + pagesToSpreadState.slice(0, page).filter(({ isSpread }) => isSpread).length,
|
||||
);
|
||||
|
||||
pageUrls.forEach((url, index) => {
|
||||
/*
|
||||
|
||||
| = page separator
|
||||
+ = double page
|
||||
_ = double spread
|
||||
|
||||
without double spreads:
|
||||
without double spread offset: | 0 + 1 | 2 + 3 | 4 + 5 | 6 + 7 | 8 |
|
||||
with double spread offset : | 0 | 1 + 2 | 3 + 4 | 5 + 6 | 7 + 8 |
|
||||
|
||||
with double spreads
|
||||
to handle double spreads:
|
||||
each double spread has to count as 2 pages, thus, each page number after a double spread has to increase
|
||||
by the number of leading double spreads
|
||||
|
||||
without double spread offset: | 0 + 1 | 2 | _3/4_ | 5 | 6 + 7 | 8 | _9/10_ | 11 | 12 + 13 | 14 |
|
||||
with double spread offset : | 0 | 1 + 2 | _3/4_ | 5 + 6 | 7 + 8 | _9/10_ | 11 + 12 | 13 + 14 |
|
||||
|
||||
thus, to get the second page:
|
||||
without offset: second page = current page number even ? +1 : -1
|
||||
with offset : second page = current page number even ? -1 : +1
|
||||
|
||||
the second page has to be ignored in case:
|
||||
- double spreads are offset, and the current page is the first page
|
||||
- either the current or second page is a double spread
|
||||
*/
|
||||
|
||||
const normalizedIndex = pageToActualPageIndex[index];
|
||||
const isCurrentPageEven = !(normalizedIndex % 2);
|
||||
|
||||
const secondPageOffset = (() => {
|
||||
const invert = shouldOffsetDoubleSpreads ? -1 : 1;
|
||||
const offset = isCurrentPageEven ? 1 : -1;
|
||||
|
||||
return offset * invert;
|
||||
})();
|
||||
|
||||
const secondPageIndex = index + secondPageOffset;
|
||||
|
||||
const isPrimaryPage = index < secondPageIndex;
|
||||
const isFirstPage = index === 0;
|
||||
const isLastPage = index === pageUrls.length - 1;
|
||||
const hasSecondPage = isPrimaryPage && !isLastPage;
|
||||
|
||||
const isPrimaryPageSpreadPage = pagesToSpreadState[index].isSpread;
|
||||
const isSecondPageSpreadPage = !!pagesToSpreadState[secondPageIndex]?.isSpread;
|
||||
const hasSpreadPage = isPrimaryPageSpreadPage || isSecondPageSpreadPage;
|
||||
|
||||
const ignoreSecondPageDueToOffset = isFirstPage && shouldOffsetDoubleSpreads;
|
||||
const displaySecondPage = !hasSpreadPage && !ignoreSecondPageDueToOffset;
|
||||
|
||||
if (!isPrimaryPage && displaySecondPage) {
|
||||
const doublePageModePage = doublePageModePages[doublePageModePages.length - 1];
|
||||
|
||||
doublePageModePages[doublePageModePages.length - 1] = {
|
||||
...doublePageModePage,
|
||||
secondary: createSinglePageData(url, index),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const SEPARATOR = '-';
|
||||
const pageName = `${index + 1}${hasSecondPage && displaySecondPage ? `${SEPARATOR}${secondPageIndex + 1}` : ''}`;
|
||||
|
||||
doublePageModePages.push({
|
||||
name: direction === ReadingDirection.LTR ? pageName : reverseString(pageName, SEPARATOR),
|
||||
primary: createSinglePageData(url, index),
|
||||
});
|
||||
});
|
||||
|
||||
return doublePageModePages;
|
||||
};
|
||||
|
||||
export const isSpreadPage = (image: HTMLImageElement): boolean => {
|
||||
const aspectRatio = image.height / image.width;
|
||||
return aspectRatio < 1;
|
||||
};
|
||||
|
||||
const MIN_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT = 0;
|
||||
const MAX_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT = 1;
|
||||
const getImagePreLoadAmount = (
|
||||
isCurrentChapter: boolean,
|
||||
isPreviousChapter: boolean,
|
||||
isNextChapter: boolean,
|
||||
imagePreLoadAmount: number,
|
||||
): number => {
|
||||
if (isCurrentChapter) {
|
||||
return imagePreLoadAmount;
|
||||
}
|
||||
|
||||
if (isPreviousChapter || isNextChapter) {
|
||||
return coerceIn(
|
||||
MAX_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT,
|
||||
MIN_PREVIOUS_NEXT_CHAPTER_IMAGE_LOAD_AMOUNT,
|
||||
imagePreLoadAmount,
|
||||
);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const PREVIOUS_IMAGE_LOAD_AMOUNT = 2;
|
||||
export const getPageIndexesToLoad = (
|
||||
currentPageIndex: number,
|
||||
pages: ReaderStatePages['pages'],
|
||||
previousCurrentPageIndex: number,
|
||||
imagePreLoadAmount: number,
|
||||
readingMode: ReadingMode,
|
||||
isCurrentChapter: boolean,
|
||||
isPreviousChapter: boolean,
|
||||
isNextChapter: boolean,
|
||||
): number[] => {
|
||||
if (!isCurrentChapter && !isPreviousChapter && !isNextChapter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentPagesIndex = getPage(currentPageIndex, pages).pagesIndex;
|
||||
const finalImagePreLoadAmount = getImagePreLoadAmount(
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
imagePreLoadAmount,
|
||||
);
|
||||
|
||||
const directionInvert = previousCurrentPageIndex <= currentPageIndex && !isPreviousChapter ? 1 : -1;
|
||||
// load at most PREVIOUS_IMAGE_LOAD_AMOUNT of the previous pages to ensure that you do not have to wait too long
|
||||
// when going back to the previous pages
|
||||
const startPagesIndexTrailingIncluded = Math.max(
|
||||
0,
|
||||
currentPagesIndex - Math.min(PREVIOUS_IMAGE_LOAD_AMOUNT, finalImagePreLoadAmount) * directionInvert,
|
||||
);
|
||||
// do not load previous pages for continuous pagers to prevent layout shifts due to leading pages getting loaded
|
||||
const startPagesIndex = !isContinuousReadingMode(readingMode) ? startPagesIndexTrailingIncluded : currentPageIndex;
|
||||
const endPagesIndex = currentPagesIndex + finalImagePreLoadAmount * directionInvert;
|
||||
// add 1 for the current page in case it's the current chapter, otherwise, only the preload amount should get rendered
|
||||
const pagesToRenderLength = Math.max(1, Math.abs(endPagesIndex - startPagesIndex) + Number(isCurrentChapter));
|
||||
|
||||
return Array(pagesToRenderLength)
|
||||
.fill(1)
|
||||
.map((_, index) => startPagesIndex + index * directionInvert);
|
||||
};
|
||||
|
||||
export const isTransitionPageVisible = (
|
||||
type: ReaderTransitionPageMode,
|
||||
activeMode: ReaderTransitionPageMode,
|
||||
readingMode: IReaderSettings['readingMode'],
|
||||
): boolean => [ReaderTransitionPageMode.BOTH, type].includes(activeMode) || isContinuousReadingMode(readingMode);
|
||||
|
||||
export const isATransitionPageVisible = (activeMode: ReaderTransitionPageMode, readingMode: ReadingMode): boolean =>
|
||||
activeMode !== ReaderTransitionPageMode.NONE || isContinuousReadingMode(readingMode);
|
||||
|
||||
export const getScrollIntoViewInlineOption = (
|
||||
offset: DirectionOffset,
|
||||
themeDirection: Direction,
|
||||
readingDirection: ReadingDirection,
|
||||
): ScrollIntoViewOptions['inline'] => {
|
||||
const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
||||
|
||||
if (themeDirection === 'ltr') {
|
||||
if (offset === DirectionOffset.PREVIOUS) {
|
||||
return getOptionForDirection('start', 'end', themeDirectionForReadingDirection);
|
||||
}
|
||||
|
||||
return getOptionForDirection('start', 'end', themeDirectionForReadingDirection);
|
||||
}
|
||||
|
||||
if (offset === DirectionOffset.PREVIOUS) {
|
||||
return getOptionForDirection('end', 'start', themeDirectionForReadingDirection);
|
||||
}
|
||||
|
||||
return getOptionForDirection('end', 'start', themeDirectionForReadingDirection);
|
||||
};
|
||||
|
||||
export const getScrollToXForReadingDirection = (
|
||||
element: HTMLElement,
|
||||
themeDirection: Direction,
|
||||
readingDirection: ReadingDirection,
|
||||
): number => {
|
||||
const themeDirectionForReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
||||
|
||||
if (themeDirection === 'ltr') {
|
||||
return getOptionForDirection(0, element.scrollWidth, themeDirectionForReadingDirection);
|
||||
}
|
||||
|
||||
return getOptionForDirection(-element.scrollWidth, 0, themeDirectionForReadingDirection);
|
||||
};
|
||||
|
||||
export const isPageOfOutdatedPageLoadStates = (
|
||||
url: string,
|
||||
pageLoadState: ReaderStatePages['pageLoadStates'][number] | undefined,
|
||||
): boolean => pageLoadState === undefined || pageLoadState.url !== url;
|
||||
160
src/features/reader/viewer/pager/components/BasePager.tsx
Normal file
160
src/features/reader/viewer/pager/components/BasePager.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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 { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import Box, { BoxProps } from '@mui/material/Box';
|
||||
import { getPageIndexesToLoad, isATransitionPageVisible } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderPagerProps,
|
||||
ReaderResumeMode,
|
||||
ReaderTransitionPageMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { isContinuousReadingMode } from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
|
||||
const getPreviousCurrentPageIndex = (resumeMode: ReaderResumeMode): number =>
|
||||
resumeMode === ReaderResumeMode.END ? Number.MAX_SAFE_INTEGER : -1;
|
||||
|
||||
const BaseBasePager = forwardRef<
|
||||
HTMLDivElement,
|
||||
Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix' | 'isPreloadMode'> &
|
||||
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
|
||||
createPage: (
|
||||
page: ReaderStatePages['pages'][number],
|
||||
pagesIndex: number,
|
||||
shouldLoad: boolean,
|
||||
shouldDisplay: boolean,
|
||||
setRef: (pagesIndex: number, element: HTMLElement | null) => void,
|
||||
readingMode: ReaderPagerProps['readingMode'],
|
||||
customFilter: ReaderPagerProps['customFilter'],
|
||||
pageScaleMode: ReaderPagerProps['pageScaleMode'],
|
||||
shouldStretchPage: ReaderPagerProps['shouldStretchPage'],
|
||||
readerWidth: ReaderPagerProps['readerWidth'],
|
||||
scrollbarXSize: ReaderPagerProps['scrollbarXSize'],
|
||||
scrollbarYSize: ReaderPagerProps['scrollbarYSize'],
|
||||
readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'],
|
||||
) => ReactNode;
|
||||
slots?: { boxProps?: BoxProps };
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
currentPageIndex,
|
||||
pages,
|
||||
transitionPageMode,
|
||||
imageRefs,
|
||||
createPage,
|
||||
slots,
|
||||
readingMode,
|
||||
imagePreLoadAmount,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
customFilter,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readerWidth,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
resumeMode,
|
||||
handleAsInitialRender,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const previousCurrentPageIndex = useRef(getPreviousCurrentPageIndex(resumeMode));
|
||||
|
||||
if (handleAsInitialRender) {
|
||||
previousCurrentPageIndex.current = getPreviousCurrentPageIndex(resumeMode);
|
||||
}
|
||||
|
||||
const pagesIndexesToRender = useMemo(
|
||||
() =>
|
||||
getPageIndexesToLoad(
|
||||
currentPageIndex,
|
||||
pages,
|
||||
previousCurrentPageIndex.current,
|
||||
imagePreLoadAmount,
|
||||
readingMode,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
),
|
||||
[
|
||||
currentPageIndex,
|
||||
pages,
|
||||
imagePreLoadAmount,
|
||||
readingMode,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isCurrentChapter) {
|
||||
previousCurrentPageIndex.current = currentPageIndex;
|
||||
}
|
||||
}, [pagesIndexesToRender, isCurrentChapter]);
|
||||
|
||||
const setRef = useCallback(
|
||||
(pagesIndex: number, element: HTMLElement | null) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageRefs.current[pagesIndex] = element;
|
||||
},
|
||||
[imageRefs],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
{...slots?.boxProps}
|
||||
sx={[
|
||||
{
|
||||
width: 'fit-content',
|
||||
height: 'fit-content',
|
||||
},
|
||||
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
|
||||
// hide pager, without actually unmounting it to prevent re-renders, while a chapter transition page is taking up the full screen
|
||||
applyStyles(
|
||||
!isContinuousReadingMode(readingMode) &&
|
||||
isATransitionPageVisible(transitionPageMode, readingMode),
|
||||
{
|
||||
visibility: 'hidden',
|
||||
width: 0,
|
||||
height: 0,
|
||||
m: 0,
|
||||
p: 0,
|
||||
},
|
||||
),
|
||||
]}
|
||||
>
|
||||
{pages.map((page, pagesIndex) =>
|
||||
createPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
pagesIndexesToRender.includes(pagesIndex),
|
||||
[ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode),
|
||||
setRef,
|
||||
readingMode,
|
||||
customFilter,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readerWidth,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const BasePager = memo(BaseBasePager);
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Direction, useTheme } from '@mui/material/styles';
|
||||
import { forwardRef, Fragment, memo, useMemo } from 'react';
|
||||
import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderPagerProps,
|
||||
ReaderPageScaleMode,
|
||||
ReadingDirection,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
|
||||
const getPagePosition = (
|
||||
pageType: 'first' | 'second',
|
||||
themeDirection: Direction,
|
||||
readingDirection: ReadingDirection,
|
||||
): 'left' | 'right' => {
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
if (pageType === 'first') {
|
||||
if (themeDirection === 'ltr') {
|
||||
return isLtrReadingDirection ? 'right' : 'left';
|
||||
}
|
||||
|
||||
return isLtrReadingDirection ? 'left' : 'right';
|
||||
}
|
||||
|
||||
if (themeDirection === 'ltr') {
|
||||
return isLtrReadingDirection ? 'left' : 'right';
|
||||
}
|
||||
|
||||
return isLtrReadingDirection ? 'right' : 'left';
|
||||
};
|
||||
|
||||
const BaseReaderDoublePagedPager = forwardRef<
|
||||
HTMLDivElement,
|
||||
ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'>
|
||||
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props;
|
||||
|
||||
const { direction: themeDirection } = useTheme();
|
||||
|
||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) => {
|
||||
const { primary, secondary } = page;
|
||||
|
||||
const currentSecondaryPageIndex = getNextIndexFromPage(currentPage);
|
||||
|
||||
const hasSecondaryPage = !!secondary;
|
||||
const isPrimaryPage = currentPage.primary.index === primary.index;
|
||||
const isSecondaryPage = !!secondary && currentSecondaryPageIndex === secondary.index;
|
||||
|
||||
return (
|
||||
<Fragment key={`${primary.url}_${secondary?.url}`}>
|
||||
{createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && isPrimaryPage && shouldLoad,
|
||||
currentPage.primary.index,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
hasSecondaryPage ? getPagePosition('first', themeDirection, readingDirection) : undefined,
|
||||
hasSecondaryPage,
|
||||
)}
|
||||
{hasSecondaryPage &&
|
||||
createReaderPage(
|
||||
{ ...page, primary: { ...page.secondary! } },
|
||||
pagesIndex,
|
||||
false,
|
||||
pageLoadStates[secondary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && isSecondaryPage && shouldLoad,
|
||||
currentSecondaryPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
getPagePosition('second', themeDirection, readingDirection),
|
||||
true,
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
slots={{
|
||||
boxProps: {
|
||||
sx: {
|
||||
...applyStyles(pageScaleMode === ReaderPageScaleMode.ORIGINAL, {
|
||||
margin: 'auto',
|
||||
}),
|
||||
width: '100%',
|
||||
minWidth: 'fit-content',
|
||||
height: '100%',
|
||||
minHeight: 'fit-content',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager);
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { forwardRef, memo } from 'react';
|
||||
import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
|
||||
const BaseReaderHorizontalPager = forwardRef<
|
||||
HTMLDivElement,
|
||||
ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>
|
||||
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, totalPages, pageGap, readingDirection } = props;
|
||||
|
||||
const { direction: themeDirection } = useTheme();
|
||||
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
|
||||
createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[page.primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
!isPreloadMode,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
setRef,
|
||||
)
|
||||
}
|
||||
slots={{
|
||||
boxProps: {
|
||||
sx: {
|
||||
my: 'auto',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
gap: `${pageGap}px`,
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
justifyContent: isLtrReadingDirection ? 'flex-start' : 'flex-end',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
justifyContent: isLtrReadingDirection ? 'flex-end' : 'flex-start',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager);
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { forwardRef, memo } from 'react';
|
||||
import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
|
||||
import { ReaderPagerProps } from '@/features/reader/Reader.types.ts';
|
||||
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
|
||||
const BaseReaderPagedPager = forwardRef<HTMLDivElement, ReaderPagerProps>(
|
||||
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, totalPages } = props;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) =>
|
||||
createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[page.primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
)
|
||||
}
|
||||
slots={{
|
||||
boxProps: {
|
||||
sx: {
|
||||
margin: 'auto',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderPagedPager = memo(BaseReaderPagedPager);
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { forwardRef, memo } from 'react';
|
||||
import { BasePager } from '@/features/reader/viewer/pager/components/BasePager.tsx';
|
||||
import { ReaderPagerProps, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import { createReaderPage } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
|
||||
|
||||
const BaseReaderVerticalPager = forwardRef<HTMLDivElement, ReaderPagerProps>(
|
||||
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, totalPages, readingMode, pageGap } = props;
|
||||
|
||||
const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
|
||||
const actualPageGap = isWebtoonMode ? 0 : pageGap;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
|
||||
createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[page.primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
!isPreloadMode,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
page.primary.index !== 0 ? actualPageGap : 0,
|
||||
setRef,
|
||||
)
|
||||
}
|
||||
slots={{ boxProps: { sx: { margin: 'auto' } } }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderVerticalPager = memo(BaseReaderVerticalPager);
|
||||
Reference in New Issue
Block a user