Add infinite scroll to reader
This commit is contained in:
120
src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts
Normal file
120
src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
||||
import { ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
|
||||
|
||||
const OPEN_CHAPTER_INTERSECTION_RATIO = 0.1;
|
||||
|
||||
const getScrollingDirection = (
|
||||
readingMode: ReadingMode,
|
||||
readingDirection: ReadingDirection,
|
||||
{ bottom, left, right }: DOMRect,
|
||||
): 'backward' | 'forward' => {
|
||||
if (isContinuousVerticalReadingMode(readingMode)) {
|
||||
const isScrollingBackward = bottom >= window.innerHeight;
|
||||
if (isScrollingBackward) {
|
||||
return 'backward';
|
||||
}
|
||||
|
||||
return 'forward';
|
||||
}
|
||||
|
||||
const themeDirectionOfReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
||||
const endOfElement = getOptionForDirection(right, left, themeDirectionOfReadingDirection);
|
||||
|
||||
const isScrollingBackward =
|
||||
endOfElement >= getOptionForDirection(window.innerWidth, 0, themeDirectionOfReadingDirection);
|
||||
if (isScrollingBackward) {
|
||||
return 'backward';
|
||||
}
|
||||
|
||||
return 'forward';
|
||||
};
|
||||
|
||||
export const useReaderInfiniteScrollUpdateChapter = (
|
||||
pageType: 'first' | 'last',
|
||||
chapterId: number,
|
||||
chapterToOpenId: number | undefined,
|
||||
isCurrentChapter: boolean,
|
||||
isChapterToOpenVisible: boolean,
|
||||
readingMode: ReadingMode,
|
||||
readingDirection: ReadingDirection,
|
||||
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>,
|
||||
image: HTMLElement | null,
|
||||
) => {
|
||||
useLayoutEffect(() => {
|
||||
if (!image || !isContinuousReadingMode(readingMode) || chapterToOpenId === undefined) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// gets immediately observed once on initial render
|
||||
let isInitialObserve = true;
|
||||
let wasNextChapterOpened = false;
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (isInitialObserve) {
|
||||
isInitialObserve = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wasNextChapterOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = entries[entries.length - 1];
|
||||
|
||||
const wasPageScrolledOutOfView = entry.intersectionRatio < OPEN_CHAPTER_INTERSECTION_RATIO;
|
||||
const isScrollingBackward =
|
||||
getScrollingDirection(readingMode, readingDirection, entry.target.getBoundingClientRect()) ===
|
||||
'backward';
|
||||
const isScrollingForward = !isScrollingBackward;
|
||||
|
||||
// the first page only opens the previous chapter in case it hasn't been loaded yet, otherwise, the last
|
||||
// page handles setting the correct chapter
|
||||
const initialOpenPreviousChapter =
|
||||
pageType === 'first' && isScrollingBackward && !isChapterToOpenVisible;
|
||||
const openPreviousChapter = pageType === 'last' && !isCurrentChapter && isScrollingForward;
|
||||
const openNextChapter = pageType === 'last' && isScrollingForward && wasPageScrolledOutOfView;
|
||||
|
||||
const openChapterToOpen = initialOpenPreviousChapter || openNextChapter;
|
||||
if (openChapterToOpen) {
|
||||
openChapter(chapterToOpenId, !isChapterToOpenVisible, false);
|
||||
wasNextChapterOpened = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (openPreviousChapter) {
|
||||
openChapter(chapterId, false, false);
|
||||
wasNextChapterOpened = true;
|
||||
}
|
||||
},
|
||||
{ threshold: [OPEN_CHAPTER_INTERSECTION_RATIO] },
|
||||
);
|
||||
intersectionObserver.observe(image);
|
||||
|
||||
return () => intersectionObserver.unobserve(image);
|
||||
}, [
|
||||
pageType,
|
||||
chapterId,
|
||||
chapterToOpenId,
|
||||
isCurrentChapter,
|
||||
isChapterToOpenVisible,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
openChapter,
|
||||
image,
|
||||
]);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||
import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
|
||||
export const useReaderResetStates = (
|
||||
setManga: TReaderStateMangaContext['setManga'],
|
||||
@@ -36,10 +37,7 @@ export const useReaderResetStates = (
|
||||
useEffect(
|
||||
() => () => {
|
||||
setManga(undefined);
|
||||
setReaderStateChapters({
|
||||
mangaChapters: [],
|
||||
chapters: [],
|
||||
});
|
||||
setReaderStateChapters(READER_STATE_CHAPTERS_DEFAULTS);
|
||||
|
||||
setCurrentPageIndex(0);
|
||||
setPageToScrollToIndex(null);
|
||||
|
||||
@@ -12,10 +12,12 @@ import { DirectionOffset } from '@/Base.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { GetChaptersReaderQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { IReaderSettings, ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||
import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
|
||||
export const useReaderSetChaptersState = (
|
||||
chaptersResponse: ReturnType<typeof requestManager.useGetMangaChapters<GetChaptersReaderQuery>>,
|
||||
chapterSourceOrder: number,
|
||||
initialChapter: ReaderStateChapters['initialChapter'],
|
||||
chapterForDuplicatesHandling: ReaderStateChapters['chapterForDuplicatesHandling'],
|
||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||
shouldSkipDupChapters: IReaderSettings['shouldSkipDupChapters'],
|
||||
@@ -25,32 +27,55 @@ export const useReaderSetChaptersState = (
|
||||
const newCurrentChapter = newMangaChapters
|
||||
? (newMangaChapters[newMangaChapters.length - chapterSourceOrder] ?? null)
|
||||
: undefined;
|
||||
const newInitialChapter = initialChapter ?? newCurrentChapter;
|
||||
const newChapterForDuplicatesHandling = chapterForDuplicatesHandling ?? newCurrentChapter;
|
||||
|
||||
setReaderStateChapters({
|
||||
mangaChapters: newMangaChapters ?? [],
|
||||
chapters:
|
||||
newChapterForDuplicatesHandling && newMangaChapters
|
||||
? Chapters.removeDuplicates(newChapterForDuplicatesHandling, newMangaChapters)
|
||||
: [],
|
||||
chapterForDuplicatesHandling: newChapterForDuplicatesHandling,
|
||||
currentChapter: newCurrentChapter,
|
||||
nextChapter:
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
}),
|
||||
previousChapter:
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
}),
|
||||
const nextChapter =
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
});
|
||||
const previousChapter =
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
});
|
||||
|
||||
const hasInitialChapterChanged = newInitialChapter != null && newInitialChapter.id !== initialChapter?.id;
|
||||
|
||||
setReaderStateChapters((prevState) => {
|
||||
const hasCurrentChapterChanged = newCurrentChapter?.id !== prevState.currentChapter?.id;
|
||||
return {
|
||||
...prevState,
|
||||
mangaChapters: newMangaChapters ?? [],
|
||||
chapters:
|
||||
newChapterForDuplicatesHandling && newMangaChapters
|
||||
? Chapters.removeDuplicates(newChapterForDuplicatesHandling, newMangaChapters)
|
||||
: [],
|
||||
initialChapter: newInitialChapter,
|
||||
chapterForDuplicatesHandling: newChapterForDuplicatesHandling,
|
||||
currentChapter: newCurrentChapter,
|
||||
nextChapter,
|
||||
previousChapter,
|
||||
isCurrentChapterReady: hasCurrentChapterChanged ? false : prevState.isCurrentChapterReady,
|
||||
visibleChapters: hasInitialChapterChanged
|
||||
? {
|
||||
...READER_STATE_CHAPTERS_DEFAULTS.visibleChapters,
|
||||
lastLeadingChapterSourceOrder:
|
||||
newInitialChapter?.sourceOrder ??
|
||||
READER_STATE_CHAPTERS_DEFAULTS.visibleChapters.lastLeadingChapterSourceOrder,
|
||||
lastTrailingChapterSourceOrder:
|
||||
newInitialChapter?.sourceOrder ??
|
||||
READER_STATE_CHAPTERS_DEFAULTS.visibleChapters.lastTrailingChapterSourceOrder,
|
||||
}
|
||||
: prevState.visibleChapters,
|
||||
};
|
||||
});
|
||||
}, [chaptersResponse.data?.chapters.nodes, chapterSourceOrder, shouldSkipDupChapters]);
|
||||
}, [chaptersResponse.data?.chapters.nodes, chapterSourceOrder, shouldSkipDupChapters, initialChapter]);
|
||||
};
|
||||
|
||||
@@ -6,62 +6,78 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { getInitialReaderPageIndex } from '@/modules/reader/utils/Reader.utils.ts';
|
||||
import { createPagesData } from '@/modules/reader/utils/ReaderPager.utils.tsx';
|
||||
import {
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { READER_STATE_PAGES_DEFAULTS } from '@/modules/reader/constants/ReaderContext.constants.ts';
|
||||
import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
|
||||
export const useReaderSetPagesState = (
|
||||
isCurrentChapter: boolean,
|
||||
pagesResponse: ReturnType<typeof requestManager.useGetChapterPagesFetch>[1],
|
||||
resumeMode: ReaderResumeMode,
|
||||
currentChapter: ReaderStateChapters['currentChapter'],
|
||||
lastPageRead: TChapterReader['lastPageRead'] | undefined,
|
||||
pages: ReaderStatePages['pages'],
|
||||
pageLoadStates: ReaderStatePages['pageLoadStates'],
|
||||
pagesToSpreadState: ReaderPageSpreadState[],
|
||||
arePagesFetched: boolean,
|
||||
setArePagesFetched: (fetched: boolean) => void,
|
||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||
setTotalPages: ReaderStatePages['setTotalPages'],
|
||||
setPages: ReaderStatePages['setPages'],
|
||||
setPageUrls: ReaderStatePages['setPageUrls'],
|
||||
setPageLoadStates: ReaderStatePages['setPageLoadStates'],
|
||||
setPagesToSpreadState: (state: ReaderPageSpreadState[]) => void,
|
||||
setCurrentPageIndex: ReaderStatePages['setCurrentPageIndex'],
|
||||
setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
|
||||
setTransitionPageMode: ReaderStatePages['setTransitionPageMode'],
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const previousPageData = useRef<string[]>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const pagesPayload = pagesResponse.data?.fetchChapterPages;
|
||||
if (pagesPayload) {
|
||||
const { pages } = pagesPayload;
|
||||
const newPages = pages.length ? pages : [''];
|
||||
if (!pagesPayload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialReaderPageIndex = getInitialReaderPageIndex(
|
||||
resumeMode,
|
||||
currentChapter?.lastPageRead ?? 0,
|
||||
newPages.length - 1,
|
||||
);
|
||||
const { pages: pagesFromResponse } = pagesPayload;
|
||||
const newPages = pages.length ? pagesFromResponse : [''];
|
||||
const initialReaderPageIndex = getInitialReaderPageIndex(resumeMode, lastPageRead ?? 0, newPages.length - 1);
|
||||
|
||||
const didPagesChange = previousPageData.current !== pagesPayload?.pages;
|
||||
if (didPagesChange) {
|
||||
previousPageData.current = pagesPayload.pages;
|
||||
const newPageData = createPagesData(newPages);
|
||||
|
||||
setArePagesFetched(true);
|
||||
setTotalPages(pagesPayload.chapter.pageCount);
|
||||
setPages(newPageData);
|
||||
setPageUrls(newPages);
|
||||
setPageLoadStates(newPageData.map(({ primary: { url } }) => ({ url, loaded: false })));
|
||||
setPagesToSpreadState(newPageData.map(({ primary: { url } }) => ({ url, isSpread: false })));
|
||||
setCurrentPageIndex(initialReaderPageIndex);
|
||||
setPageToScrollToIndex(initialReaderPageIndex);
|
||||
} else {
|
||||
setArePagesFetched(false);
|
||||
setCurrentPageIndex(READER_STATE_PAGES_DEFAULTS.currentPageIndex);
|
||||
setPageToScrollToIndex(READER_STATE_PAGES_DEFAULTS.pageToScrollToIndex);
|
||||
setTotalPages(READER_STATE_PAGES_DEFAULTS.totalPages);
|
||||
setPages(READER_STATE_PAGES_DEFAULTS.pages);
|
||||
setPageUrls(READER_STATE_PAGES_DEFAULTS.pageUrls);
|
||||
setPageLoadStates(READER_STATE_PAGES_DEFAULTS.pageLoadStates);
|
||||
setPages(pages);
|
||||
setPageLoadStates(pageLoadStates);
|
||||
setPagesToSpreadState(pagesToSpreadState);
|
||||
}
|
||||
|
||||
setTotalPages(pagesPayload.pages.length);
|
||||
setPageUrls(newPages);
|
||||
setCurrentPageIndex(initialReaderPageIndex);
|
||||
setPageToScrollToIndex(initialReaderPageIndex);
|
||||
setReaderStateChapters((prevState) => ({
|
||||
...prevState,
|
||||
isCurrentChapterReady: arePagesFetched || didPagesChange,
|
||||
}));
|
||||
|
||||
setTransitionPageMode(ReaderTransitionPageMode.NONE);
|
||||
}, [pagesResponse.data?.fetchChapterPages?.pages]);
|
||||
}, [pagesResponse.data?.fetchChapterPages?.pages, isCurrentChapter]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user