Fix: Chapter loading for jump to chapter modal and chapter drawer

* updated debs

* Jump to chapter now loads not preloaded chapters

* chapter drawer scrolls to current chapter

* Load new chapters when reaching end of chapter drawer

* fix JumpToChapterModal rebase

* fix Chapter drawer current chapter position detection

---------

Co-authored-by: CD-Z <github.verify097@passmail.com>
This commit is contained in:
Patrick Loser
2026-01-31 13:11:07 +01:00
committed by GitHub
parent b53ec3352f
commit 4ad0639796
6 changed files with 5273 additions and 1877 deletions
+5135 -1811
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -293,6 +293,25 @@ export const getPageChaptersBatched = (
);
};
export const getNovelChaptersByNumber = (
novelId: number,
chapterNumber: number,
) => {
return db.getAllAsync<ChapterInfo>(
'SELECT * FROM Chapter WHERE novelId = ? AND position = ?',
novelId,
chapterNumber - 1,
);
};
export const getNovelChaptersByName = (novelId: number, searchText: string) => {
return db.getAllAsync<ChapterInfo>(
'SELECT * FROM Chapter WHERE novelId = ? AND name LIKE ?',
novelId,
`%${searchText}%`,
);
};
export const getPrevChapter = (
novelId: number,
chapterPosition: number,
+42
View File
@@ -322,6 +322,46 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => {
settingsSort,
]);
const loadUpToBatch = useCallback(
async (targetBatch: number) => {
const page = pages[pageIndex];
if (!novel || !page || targetBatch <= batchInformation.batch) {
return;
}
// Load all batches from current + 1 up to targetBatch
for (let batch = batchInformation.batch + 1; batch <= targetBatch; batch++) {
if (batch > batchInformation.total) break;
let newChapters: ChapterInfo[] = [];
try {
newChapters =
getPageChaptersBatched(
novel.id,
settingsSort,
novelSettings.filter,
page,
batch,
) || [];
} catch (error) {
console.error('Error loading batch', batch, error);
}
setBatchInformation(prev => ({ ...prev, batch }));
extendChapters(newChapters);
}
},
[
batchInformation,
extendChapters,
novel,
novelSettings.filter,
pageIndex,
pages,
settingsSort,
],
);
// #endregion
// #region Mark chapters
@@ -566,6 +606,7 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => {
novelSettings,
batchInformation,
getNextChapterBatch,
loadUpToBatch,
getNovel,
setPageIndex,
openPage,
@@ -597,6 +638,7 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => {
novelSettings,
batchInformation,
getNextChapterBatch,
loadUpToBatch,
getNovel,
setPageIndex,
openPage,
+4
View File
@@ -40,6 +40,7 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
fetching,
batchInformation,
getNextChapterBatch,
loadUpToBatch,
setNovel,
bookmarkChapters,
markChaptersRead,
@@ -321,6 +322,9 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
novel={novel}
chapterListRef={chapterListRef}
navigation={navigation}
loadUpToBatch={loadUpToBatch}
totalChapters={batchInformation.totalChapters}
chapters={chapters}
/>
<EditInfoModal
modalVisible={editInfoModal}
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import {
StyleSheet,
View,
@@ -12,32 +12,42 @@ import { Portal, Text } from 'react-native-paper';
import { useTheme } from '@hooks/persisted';
import { ChapterInfo, NovelInfo } from '@database/types';
import { NovelScreenProps } from '@navigators/types';
import {
getNovelChaptersByNumber,
getNovelChaptersByName,
} from '@database/queries/ChapterQueries';
import {
LegendList,
LegendListRef,
LegendListRenderItemProps,
} from '@legendapp/list';
import { getNovelChapters } from '@database/queries/ChapterQueries';
interface JumpToChapterModalProps {
hideModal: () => void;
modalVisible: boolean;
navigation: NovelScreenProps['navigation'];
novel: NovelInfo;
chapters: ChapterInfo[];
chapterListRef: React.RefObject<LegendListRef | null>;
loadUpToBatch: (batch: number) => Promise<void>;
totalChapters?: number;
}
const JumpToChapterModal = ({
hideModal,
modalVisible,
chapters: loadedChapters,
navigation,
novel,
chapterListRef,
loadUpToBatch,
totalChapters,
}: JumpToChapterModalProps) => {
const minNumber = 1;
const maxNumber = totalChapters ?? -1;
const theme = useTheme();
const [mode, setMode] = useState(false);
const [openChapter, setOpenChapter] = useState(false);
const [allChapters, setAllChapters] = useState<ChapterInfo[]>([]);
const [text, setText] = useState('');
const [error, setError] = useState('');
@@ -46,30 +56,6 @@ const JumpToChapterModal = ({
const inputRef = useRef<RNTextInput>(null);
const [inputFocused, setInputFocused] = useState(false);
useEffect(() => {
if (modalVisible && novel?.id) {
getNovelChapters(novel.id).then(chapters => {
setAllChapters(chapters);
});
}
}, [modalVisible, novel?.id]);
const minNumber = useMemo(() => {
if (allChapters.length === 0) {
return 1;
}
return Math.min(...allChapters.map(c => c.chapterNumber || -1));
}, [allChapters]);
const maxNumber = useMemo(() => {
if (allChapters.length === 0) {
return 1;
}
return Math.max(...allChapters.map(c => c.chapterNumber || -1));
}, [allChapters]);
const onDismiss = () => {
hideModal();
setText('');
@@ -87,22 +73,28 @@ const JumpToChapterModal = ({
});
};
const scrollToChapter = (chap: ChapterInfo) => {
const scrollToChapter = async (chap: ChapterInfo) => {
onDismiss();
const index = allChapters.findIndex(c => c.id === chap.id);
if (index !== -1) {
const loadedIndex = loadedChapters.findIndex(c => c.id === chap.id);
if (loadedIndex >= 0) {
chapterListRef.current?.scrollToIndex({
animated: true,
index: index,
viewPosition: 0.5,
});
} else {
chapterListRef.current?.scrollToItem({
animated: true,
item: chap,
index: loadedIndex,
viewPosition: 0.5,
});
return;
}
if ((chap.position ?? -1) >= 0) {
const targetBatch = Math.floor(chap.position! / 300);
await loadUpToBatch(targetBatch);
setTimeout(() => {
chapterListRef.current?.scrollToIndex({
animated: true,
index: chap.position!,
viewPosition: 0.5,
});
}, 0);
}
};
@@ -136,46 +128,48 @@ const JumpToChapterModal = ({
);
};
const onSubmit = () => {
const onSubmit = async () => {
if (!mode) {
// Number search
const num = Number(text);
if (num && num >= minNumber && num <= maxNumber) {
if (openChapter) {
const chapter = allChapters.find(c => c.chapterNumber === num);
if (chapter) {
const chapters = await getNovelChaptersByNumber(novel!.id, num);
if (chapters.length > 0) {
const chapter = chapters[0];
if (openChapter) {
return navigateToChapter(chapter);
}
} else {
const chapter = allChapters.find(c => c.chapterNumber === num);
if (chapter) {
} else {
return scrollToChapter(chapter);
}
}
}
return setError(
getString('novelScreen.jumpToChapterModal.error.validChapterNumber') +
` (${num < minNumber ? '≥ ' + minNumber : '≤ ' + maxNumber})`,
);
} else {
const searchedChapters = allChapters.filter(chap =>
chap.name.toLowerCase().includes(text?.toLowerCase()),
// Text search
const chapters = await getNovelChaptersByName(
novel!.id,
text.toLowerCase(),
);
if (!searchedChapters.length) {
if (!chapters.length) {
setError(
getString('novelScreen.jumpToChapterModal.error.validChapterName'),
);
return;
}
if (searchedChapters.length === 1) {
if (chapters.length === 1) {
if (openChapter) {
return navigateToChapter(searchedChapters[0]);
return navigateToChapter(chapters[0]);
} else {
return scrollToChapter(chapters[0]);
}
return scrollToChapter(searchedChapters[0]);
}
return setResult(searchedChapters);
return setResult(chapters);
}
};
@@ -16,6 +16,7 @@ import renderListChapter from './RenderListChapter';
import { useChapterContext } from '@screens/reader/ChapterContext';
import { useNovelContext } from '@screens/novel/NovelContext';
import { LegendList, LegendListRef, ViewToken } from '@legendapp/list';
import { noop } from 'lodash-es';
type ButtonProperties = {
text: string;
@@ -29,7 +30,15 @@ type ButtonsProperties = {
const ChapterDrawer = () => {
const { chapter, getChapter, setLoading } = useChapterContext();
const { chapters, novelSettings, pages, setPageIndex } = useNovelContext();
const {
chapters,
novelSettings,
pages,
fetching,
batchInformation,
getNextChapterBatch,
setPageIndex,
} = useNovelContext();
const theme = useTheme();
const insets = useSafeAreaInsets();
const { defaultChapterSort } = useAppSettings();
@@ -89,9 +98,9 @@ const ChapterDrawer = () => {
const newBtnLayout = Object.create(defaultButtonLayout);
if (viewableItems.length === 0) return;
const cKey = (scrollToIndex.current ?? 0) + 2;
const vKey = parseInt(viewableItems[0].key, 10);
const visible = vKey <= cKey && cKey <= vKey + viewableItems.length - 1;
const visible = viewableItems
.map(v => v.index)
.includes((scrollToIndex.current ?? 0) + 2);
if (!visible && scrollToIndex.current !== undefined) {
if (
@@ -110,12 +119,7 @@ const ChapterDrawer = () => {
};
}
}
if (cKey <= 2 && vKey <= 4) {
newBtnLayout.up = {
text: curChapter,
index: scrollToIndex.current,
};
}
setButtonProperties(newBtnLayout);
},
[defaultButtonLayout, listAscending],
@@ -136,7 +140,10 @@ const ChapterDrawer = () => {
useEffect(() => {
const next = calculateScrollToIndex();
if (next !== undefined) {
if (scrollToIndex.current === undefined) {
if (
scrollToIndex.current === undefined ||
next !== scrollToIndex.current
) {
scroll(next);
}
scrollToIndex.current = next;
@@ -154,7 +161,7 @@ const ChapterDrawer = () => {
recycleItems
viewabilityConfig={{
minimumViewTime: 100,
viewAreaCoveragePercentThreshold: 95,
itemVisiblePercentThreshold: 90,
}}
onViewableItemsChanged={checkViewableItems}
data={chapters}
@@ -176,6 +183,12 @@ const ChapterDrawer = () => {
}
estimatedItemSize={60}
initialScrollIndex={scrollToIndex.current}
onEndReached={
batchInformation.batch < batchInformation.total && !fetching
? getNextChapterBatch
: noop
}
onEndReachedThreshold={6}
/>
)}
<View style={styles.footer}>