From b72cd830e63771d83b6f6640fc8177ace33b4b4e Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:27:00 +0200 Subject: [PATCH 01/18] rename and reposition --- src/hooks/persisted/index.ts | 3 +- src/hooks/persisted/{ => novel}/useNovel.ts | 75 +-- src/hooks/persisted/novel/useTrackedNovel.ts | 73 +++ src/navigators/ReaderStack.tsx | 2 +- .../{NovelContext.tsx => NovelProvider.tsx} | 3 + src/screens/novel/NovelScreen.tsx | 2 +- .../novel/components/Info/NovelInfoHeader.tsx | 2 +- .../novel/components/NovelScreenList.tsx | 464 +++++++++++------- .../reader/components/ChapterDrawer/index.tsx | 2 +- .../reader/components/ReaderAppbar.tsx | 2 +- .../reader/components/ReaderFooter.tsx | 2 +- src/screens/reader/hooks/useChapter.ts | 2 +- .../settings/SettingsAdvancedScreen.tsx | 2 +- src/services/migrate/migrateNovel.ts | 2 +- 14 files changed, 370 insertions(+), 266 deletions(-) rename src/hooks/persisted/{ => novel}/useNovel.ts (90%) create mode 100644 src/hooks/persisted/novel/useTrackedNovel.ts rename src/screens/novel/{NovelContext.tsx => NovelProvider.tsx} (95%) diff --git a/src/hooks/persisted/index.ts b/src/hooks/persisted/index.ts index 566413b08a..14863e3a73 100644 --- a/src/hooks/persisted/index.ts +++ b/src/hooks/persisted/index.ts @@ -11,6 +11,7 @@ export { } from './useSettings'; export { default as usePlugins } from './usePlugins'; export { getTracker, useTracker } from './useTracker'; -export { useTrackedNovel, useNovel } from './useNovel'; +export { useNovel } from './novel/useNovel'; +export { useTrackedNovel } from './novel/useTrackedNovel'; export { default as useDownload } from './useDownload'; export { default as useUserAgent } from './useUserAgent'; diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/novel/useNovel.ts similarity index 90% rename from src/hooks/persisted/useNovel.ts rename to src/hooks/persisted/novel/useNovel.ts index 5102d17a4e..6143f77459 100644 --- a/src/hooks/persisted/useNovel.ts +++ b/src/hooks/persisted/novel/useNovel.ts @@ -1,7 +1,5 @@ /* eslint-disable no-console */ -import { SearchResult, UserListEntry } from '@services/Trackers'; import { useMMKVNumber, useMMKVObject } from 'react-native-mmkv'; -import { TrackerMetadata, getTracker } from './useTracker'; import { ChapterInfo, NovelInfo } from '@database/types'; import { MMKVStorage } from '@utils/mmkv/mmkv'; import { @@ -33,7 +31,7 @@ import { getString } from '@strings/translations'; import dayjs from 'dayjs'; import { parseChapterNumber } from '@utils/parseChapterNumber'; import { NOVEL_STORAGE } from '@utils/Storages'; -import { useAppSettings } from './useSettings'; +import { useAppSettings } from '../useSettings'; import NativeFile from '@specs/NativeFile'; import { useLibraryContext } from '@components/Context/LibraryContext'; @@ -56,83 +54,12 @@ const defaultPageIndex = 0; // #endregion // #region types -type TrackedNovel = SearchResult & UserListEntry; - export interface NovelSettings { sort?: string; filter?: string; showChapterTitles?: boolean; } -// #endregion -// #region definition useTrackedNovel - -export const useTrackedNovel = (novelId: number | 'NO_ID') => { - const [trackedNovel, setValue] = useMMKVObject( - `${TRACKED_NOVEL_PREFIX}_${novelId}`, - ); - const updateNovelProgess = useCallback( - (tracker: TrackerMetadata, chaptersRead: number) => { - if (!trackedNovel || novelId === 'NO_ID') { - return; - } - return getTracker(tracker.name).updateUserListEntry( - trackedNovel.id, - { progress: chaptersRead }, - tracker.auth, - ); - }, - [novelId, trackedNovel], - ); - if (novelId === 'NO_ID') { - return { - trackedNovel: undefined, - trackNovel: () => {}, - untrackNovel: () => {}, - updateTrackedNovel: () => {}, - updateNovelProgess: () => {}, - }; - } - - // #endregion - // #region trackNovel functions - - const trackNovel = (tracker: TrackerMetadata, novel: SearchResult) => { - getTracker(tracker.name) - .getUserListEntry(novel.id, tracker.auth) - .then((data: UserListEntry) => { - setValue({ - ...novel, - ...data, - }); - }); - }; - - const untrackNovel = () => setValue(undefined); - - const updateTrackedNovel = ( - tracker: TrackerMetadata, - data: Partial, - ) => { - if (!trackedNovel) { - return; - } - return getTracker(tracker.name).updateUserListEntry( - trackedNovel.id, - data, - tracker.auth, - ); - }; - - return { - trackedNovel, - trackNovel, - untrackNovel, - updateTrackedNovel, - updateNovelProgess, - }; -}; - // #endregion // #region definition useNovel diff --git a/src/hooks/persisted/novel/useTrackedNovel.ts b/src/hooks/persisted/novel/useTrackedNovel.ts new file mode 100644 index 0000000000..addd403daa --- /dev/null +++ b/src/hooks/persisted/novel/useTrackedNovel.ts @@ -0,0 +1,73 @@ +import { SearchResult, UserListEntry } from '@services/Trackers'; +import { useCallback } from 'react'; +import { useMMKVObject } from 'react-native-mmkv'; +import { TRACKED_NOVEL_PREFIX } from './useNovel'; +import { TrackerMetadata, getTracker } from '../useTracker'; + +type TrackedNovel = SearchResult & UserListEntry; + +export const useTrackedNovel = (novelId: number | 'NO_ID') => { + const [trackedNovel, setValue] = useMMKVObject( + `${TRACKED_NOVEL_PREFIX}_${novelId}`, + ); + const updateNovelProgess = useCallback( + (tracker: TrackerMetadata, chaptersRead: number) => { + if (!trackedNovel || novelId === 'NO_ID') { + return; + } + return getTracker(tracker.name).updateUserListEntry( + trackedNovel.id, + { progress: chaptersRead }, + tracker.auth, + ); + }, + [novelId, trackedNovel], + ); + if (novelId === 'NO_ID') { + return { + trackedNovel: undefined, + trackNovel: () => {}, + untrackNovel: () => {}, + updateTrackedNovel: () => {}, + updateNovelProgess: () => {}, + }; + } + + // #endregion + // #region trackNovel functions + + const trackNovel = (tracker: TrackerMetadata, novel: SearchResult) => { + getTracker(tracker.name) + .getUserListEntry(novel.id, tracker.auth) + .then((data: UserListEntry) => { + setValue({ + ...novel, + ...data, + }); + }); + }; + + const untrackNovel = () => setValue(undefined); + + const updateTrackedNovel = ( + tracker: TrackerMetadata, + data: Partial, + ) => { + if (!trackedNovel) { + return; + } + return getTracker(tracker.name).updateUserListEntry( + trackedNovel.id, + data, + tracker.auth, + ); + }; + + return { + trackedNovel, + trackNovel, + untrackNovel, + updateTrackedNovel, + updateNovelProgess, + }; +}; diff --git a/src/navigators/ReaderStack.tsx b/src/navigators/ReaderStack.tsx index 41a2afe8ce..5c7114640e 100644 --- a/src/navigators/ReaderStack.tsx +++ b/src/navigators/ReaderStack.tsx @@ -11,7 +11,7 @@ import { NovelScreenProps, ReaderStackParamList, } from './types'; -import { NovelContextProvider } from '@screens/novel/NovelContext'; +import { NovelContextProvider } from '@screens/novel/NovelProvider'; const Stack = createNativeStackNavigator(); diff --git a/src/screens/novel/NovelContext.tsx b/src/screens/novel/NovelProvider.tsx similarity index 95% rename from src/screens/novel/NovelContext.tsx rename to src/screens/novel/NovelProvider.tsx index 4ab2a5c949..fef01948ce 100644 --- a/src/screens/novel/NovelContext.tsx +++ b/src/screens/novel/NovelProvider.tsx @@ -68,5 +68,8 @@ export function NovelContextProvider({ export const useNovelContext = () => { const context = useContext(NovelContext); + if (!context) { + throw new Error('useNovelContext must be used within NovelContextProvider'); + } return context; }; diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index c6898102a4..12819a0e5f 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -28,7 +28,7 @@ import { MaterialDesignIconName } from '@type/icon'; import NovelScreenList from './components/NovelScreenList'; import { ThemeColors } from '@theme/types'; import { SafeAreaView } from '@components'; -import { useNovelContext } from './NovelContext'; +import { useNovelContext } from './NovelProvider'; import { FlashList } from '@shopify/flash-list'; const Novel = ({ route, navigation }: NovelScreenProps) => { diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 8ab46dd23b..5a3fd02c90 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -38,7 +38,7 @@ import { NovelMetaSkeleton, VerticalBarSkeleton, } from '@components/Skeleton/Skeleton'; -import { useNovelContext } from '@screens/novel/NovelContext'; +import { useNovelContext } from '@screens/novel/NovelProvider'; interface NovelInfoHeaderProps { chapters: ChapterInfo[]; diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index f0d883e242..4612741158 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ChapterItem from './ChapterItem'; import NovelInfoHeader from './Info/NovelInfoHeader'; -import { useRef, useState } from 'react'; +import { useRef, useState, useCallback, useMemo } from 'react'; import { pickCustomNovelCover } from '@database/queries/NovelQueries'; import { ChapterInfo, NovelInfo } from '@database/types'; import { useBoolean } from '@hooks/index'; @@ -26,7 +26,7 @@ import * as Haptics from 'expo-haptics'; import { AnimatedFAB } from 'react-native-paper'; import { ChapterListSkeleton } from '@components/Skeleton/Skeleton'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; -import { useNovelContext } from '../NovelContext'; +import { useNovelContext } from '../NovelProvider'; import { FlashList } from '@shopify/flash-list'; import FileManager from '@specs/NativeFile'; import { downloadFile } from '@plugins/helpers/fetch'; @@ -48,7 +48,11 @@ type NovelScreenListProps = { }; }; -const ListEmptyComponent = () => ; +// Memoized empty component +const ListEmptyComponent = React.memo(() => ); + +// Memoized header component +const MemoizedNovelInfoHeader = React.memo(NovelInfoHeader); const NovelScreenList = ({ headerOpacity, @@ -79,15 +83,22 @@ const NovelScreenList = ({ } = useNovelContext(); const { pluginId } = routeBaseNovel; - const routeNovel: Omit & { id: 'NO_ID' } = { - inLibrary: false, - isLocal: false, - totalPages: 0, - ...routeBaseNovel, - id: 'NO_ID', - }; + + // Memoize route novel to prevent recreation on every render + const routeNovel: Omit & { id: 'NO_ID' } = useMemo( + () => ({ + inLibrary: false, + isLocal: false, + totalPages: 0, + ...routeBaseNovel, + id: 'NO_ID', + }), + [routeBaseNovel], + ); + const novel = fetchedNovel ?? routeNovel; const [updating, setUpdating] = useState(false); + const { useFabForContinueReading, defaultChapterSort, @@ -114,123 +125,164 @@ const NovelScreenList = ({ const deleteDownloadsSnackbar = useBoolean(); - const onPageScroll = (event: NativeSyntheticEvent) => { - const y = event.nativeEvent.contentOffset.y; + // Memoize selected chapter IDs for faster lookup + const selectedIds = useMemo( + () => new Set(selected.map(chapter => chapter.id)), + [selected], + ); - headerOpacity.set(y < 50 ? 0 : (y - 50) / 150); - const currentScrollPosition = Math.floor(y) ?? 0; - if (useFabForContinueReading && lastRead) { - setIsFabExtended(currentScrollPosition <= 0); - } - }; + // Memoize the isSelected function + const isSelected = useCallback( + (id: number) => selectedIds.has(id), + [selectedIds], + ); - const onRefresh = async () => { - if (novel.id !== 'NO_ID') { - setUpdating(true); - updateNovel(pluginId, novel.path, novel.id, { - downloadNewChapters, - refreshNovelMetadata, - }) - .then(() => getNovel()) - .then(() => - showToast( - getString('novelScreen.updatedToast', { name: novel.name }), - ), - ) - .catch(error => showToast('Failed updating: ' + error.message)) - .finally(() => setUpdating(false)); - } - }; + // Memoize download queue IDs for faster lookup + const downloadingIds = useMemo( + () => new Set(downloadQueue.map(c => c.task.data.chapterId)), + [downloadQueue], + ); + + const onPageScroll = useCallback( + (event: NativeSyntheticEvent) => { + const y = event.nativeEvent.contentOffset.y; - const onRefreshPage = async (page: string) => { + headerOpacity.set(y < 50 ? 0 : (y - 50) / 150); + const currentScrollPosition = Math.floor(y) ?? 0; + if (useFabForContinueReading && lastRead) { + setIsFabExtended(currentScrollPosition <= 0); + } + }, + [headerOpacity, useFabForContinueReading, lastRead], + ); + + const onRefresh = useCallback(async () => { if (novel.id !== 'NO_ID') { setUpdating(true); - updateNovelPage(pluginId, novel.path, novel.id, page, { - downloadNewChapters, - }) - .then(() => getNovel()) - .then(() => showToast(`Updated page: ${page}`)) - .catch(e => showToast('Failed updating: ' + e.message)) - .finally(() => setUpdating(false)); + try { + await updateNovel(pluginId, novel.path, novel.id, { + downloadNewChapters, + refreshNovelMetadata, + }); + await getNovel(); + showToast(getString('novelScreen.updatedToast', { name: novel.name })); + } catch (error: any) { + showToast('Failed updating: ' + error.message); + } finally { + setUpdating(false); + } } - }; + }, [ + novel.id, + novel.path, + novel.name, + pluginId, + downloadNewChapters, + refreshNovelMetadata, + getNovel, + ]); - const refreshControl = () => ( - + const onRefreshPage = useCallback( + async (page: string) => { + if (novel.id !== 'NO_ID') { + setUpdating(true); + try { + await updateNovelPage(pluginId, novel.path, novel.id, page, { + downloadNewChapters, + }); + await getNovel(); + showToast(`Updated page: ${page}`); + } catch (e: any) { + showToast('Failed updating: ' + e.message); + } finally { + setUpdating(false); + } + } + }, + [novel.id, novel.path, pluginId, downloadNewChapters, getNovel], ); - const isSelected = (id: number) => { - return selected.some(obj => obj.id === id); - }; + const refreshControl = useMemo( + () => ( + + ), + [topInset, onRefresh, updating, theme.primary, theme.onPrimary], + ); - const onSelectPress = (chapter: ChapterInfo) => { - if (selected.length === 0) { - navigateToChapter(chapter); - } else { - if (isSelected(chapter.id)) { - setSelected(sel => sel.filter(it => it.id !== chapter.id)); + const navigateToChapter = useCallback( + (chapter: ChapterInfo) => { + navigation.navigate('ReaderStack', { + screen: 'Chapter', + params: { novel, chapter }, + }); + }, + [navigation, novel], + ); + + const onSelectPress = useCallback( + (chapter: ChapterInfo) => { + if (selected.length === 0) { + navigateToChapter(chapter); } else { - setSelected(sel => [...sel, chapter]); + if (isSelected(chapter.id)) { + setSelected(sel => sel.filter(it => it.id !== chapter.id)); + } else { + setSelected(sel => [...sel, chapter]); + } } - } - }; + }, + [selected.length, navigateToChapter, isSelected, setSelected], + ); - const onSelectLongPress = (chapter: ChapterInfo) => { - if (selected.length === 0) { - if (!disableHapticFeedback) { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - } - setSelected(sel => [...sel, chapter]); - } else { - if (selected.length === chapters.length) { - return; - } + const onSelectLongPress = useCallback( + (chapter: ChapterInfo) => { + if (selected.length === 0) { + if (!disableHapticFeedback) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + } + setSelected(sel => [...sel, chapter]); + } else { + if (selected.length === chapters.length) { + return; + } - /** - * Select custom range - */ - const lastSelectedChapter = selected[selected.length - 1]; - - if (lastSelectedChapter.id !== chapter.id) { - if (lastSelectedChapter.id > chapter.id) { - setSelected(sel => [ - ...sel, - chapter, - ...chapters.filter( - (chap: ChapterInfo) => - (chap.id <= chapter.id || chap.id >= lastSelectedChapter.id) === - false, - ), - ]); - } else { - setSelected(sel => [ - ...sel, - chapter, - ...chapters.filter( - (chap: ChapterInfo) => - (chap.id >= chapter.id || chap.id <= lastSelectedChapter.id) === - false, - ), - ]); + const lastSelectedChapter = selected[selected.length - 1]; + + if (lastSelectedChapter.id !== chapter.id) { + if (lastSelectedChapter.id > chapter.id) { + setSelected(sel => [ + ...sel, + chapter, + ...chapters.filter( + (chap: ChapterInfo) => + (chap.id <= chapter.id || + chap.id >= lastSelectedChapter.id) === false, + ), + ]); + } else { + setSelected(sel => [ + ...sel, + chapter, + ...chapters.filter( + (chap: ChapterInfo) => + (chap.id >= chapter.id || + chap.id <= lastSelectedChapter.id) === false, + ), + ]); + } } } - } - }; - - const navigateToChapter = (chapter: ChapterInfo) => { - navigation.navigate('ReaderStack', { - screen: 'Chapter', - params: { novel, chapter }, - }); - }; + }, + [selected, chapters, disableHapticFeedback, setSelected], + ); - const setCustomNovelCover = async () => { + const setCustomNovelCover = useCallback(async () => { if (!novel || novel.id === 'NO_ID') { return; } @@ -241,9 +293,9 @@ const NovelScreenList = ({ cover: newCover, }); } - }; + }, [novel, setNovel]); - const saveNovelCover = async () => { + const saveNovelCover = useCallback(async () => { if (!novel) { showToast(getString('novelScreen.coverNotSaved')); return; @@ -271,7 +323,6 @@ const NovelScreenList = ({ ? imageExtension : 'png'; - // sanitize novel name as app crashes while copying file with ':' character const novelName = novel.name.replace(/[^a-zA-Z0-9]/g, '_'); const fileName = `${novelName}_${novel.id}.${imageExtension}`; const coverDestUri = await StorageAccessFramework.createFileAsync( @@ -295,7 +346,117 @@ const NovelScreenList = ({ FileManager.unlink(tempCoverUri); } } - }; + }, [novel]); + + // Memoize the renderItem function + const renderItem = useCallback( + ({ item, index }: { item: ChapterInfo; index: number }) => { + if (novel.id === 'NO_ID') { + return null; + } + return ( + deleteChapter(item)} + downloadChapter={() => downloadChapter(novel, item)} + isSelected={isSelected(item.id)} + onSelectPress={onSelectPress} + onSelectLongPress={onSelectLongPress} + navigateToChapter={navigateToChapter} + novelName={novel.name} + setChapterDownloaded={(value: boolean) => + updateChapter?.(index, { isDownloaded: value }) + } + /> + ); + }, + [ + novel, + downloadingIds, + theme, + showChapterTitles, + isSelected, + onSelectPress, + onSelectLongPress, + navigateToChapter, + deleteChapter, + downloadChapter, + updateChapter, + ], + ); + + // Optimize extraData to only include what actually affects rendering + const extraData = useMemo( + () => ({ + chaptersLength: chapters.length, + selectedLength: selected.length, + novelId: novel.id, + loading, + downloadingIds: Array.from(downloadingIds).sort().join(','), // Convert to string for stable comparison + }), + [chapters.length, selected.length, novel.id, loading, downloadingIds], + ); + + const keyExtractor = useCallback((item: ChapterInfo) => 'c' + item.id, []); + + // Memoize the FAB navigation function + const navigateToFab = useCallback(() => { + navigation.navigate('ReaderStack', { + screen: 'Chapter', + params: { + novel: novel, + chapter: lastRead ?? chapters[0], + }, + }); + }, [navigation, novel, lastRead, chapters]); + + // Memoize the header component props + const renderHeader = useMemo(() => { + const props = { + chapters, + deleteDownloadsSnackbar, + fetching, + filter, + isLoading: loading, + lastRead, + navigateToChapter, + navigation, + novel, + novelBottomSheetRef, + onRefreshPage, + openDrawer, + page: pages.length > 1 ? pages[pageIndex] : undefined, + setCustomNovelCover, + saveNovelCover, + theme, + totalChapters: batchInformation.totalChapters, + trackerSheetRef, + }; + return ; + }, [ + chapters, + deleteDownloadsSnackbar, + fetching, + filter, + loading, + lastRead, + navigateToChapter, + navigation, + novel, + onRefreshPage, + openDrawer, + pages, + pageIndex, + setCustomNovelCover, + saveNovelCover, + theme, + batchInformation.totalChapters, + ]); return ( <> @@ -303,71 +464,17 @@ const NovelScreenList = ({ ref={listRef} estimatedItemSize={64} data={chapters} - extraData={[ - chapters.length, - selected.length, - novel.id, - loading, - downloadQueue.length, - ]} - // ListEmptyComponent={ListEmptyComponent} + extraData={extraData} ListFooterComponent={!fetching ? undefined : ListEmptyComponent} - renderItem={({ item, index }) => { - if (novel.id === 'NO_ID') { - return null; - } - return ( - c.task.data.chapterId === item.id, - )} - isBookmarked={!!item.bookmark} - isLocal={novel.isLocal} - theme={theme} - chapter={item} - showChapterTitles={showChapterTitles} - deleteChapter={() => deleteChapter(item)} - downloadChapter={() => downloadChapter(novel, item)} - isSelected={isSelected(item.id)} - onSelectPress={onSelectPress} - onSelectLongPress={onSelectLongPress} - navigateToChapter={navigateToChapter} - novelName={novel.name} - setChapterDownloaded={(value: boolean) => - updateChapter?.(index, { isDownloaded: value }) - } - /> - ); - }} - keyExtractor={item => 'c' + item.id} + renderItem={renderItem} + keyExtractor={keyExtractor} contentContainerStyle={styles.contentContainer} - refreshControl={refreshControl()} + refreshControl={refreshControl} onEndReached={getNextChapterBatch} onEndReachedThreshold={6} onScroll={onPageScroll} drawDistance={1000} - ListHeaderComponent={ - 1 ? pages[pageIndex] : undefined} - setCustomNovelCover={setCustomNovelCover} - saveNovelCover={saveNovelCover} - theme={theme} - totalChapters={batchInformation.totalChapters} - trackerSheetRef={trackerSheetRef} - /> - } + ListHeaderComponent={renderHeader} /> {novel.id !== 'NO_ID' && ( <> @@ -402,15 +509,7 @@ const NovelScreenList = ({ }).trim() } icon="play" - onPress={() => { - navigation.navigate('ReaderStack', { - screen: 'Chapter', - params: { - novel: novel, - chapter: lastRead ?? chapters[0], - }, - }); - }} + onPress={navigateToFab} /> ) : null} @@ -418,6 +517,7 @@ const NovelScreenList = ({ ); }; + const styles = StyleSheet.create({ container: { flex: 1 }, contentContainer: { paddingBottom: 100 }, diff --git a/src/screens/reader/components/ChapterDrawer/index.tsx b/src/screens/reader/components/ChapterDrawer/index.tsx index 2a0ef8c903..0e995eb4f7 100644 --- a/src/screens/reader/components/ChapterDrawer/index.tsx +++ b/src/screens/reader/components/ChapterDrawer/index.tsx @@ -14,7 +14,7 @@ import { getString } from '@strings/translations'; import { ThemeColors } from '@theme/types'; import renderListChapter from './RenderListChapter'; import { useChapterContext } from '@screens/reader/ChapterContext'; -import { useNovelContext } from '@screens/novel/NovelContext'; +import { useNovelContext } from '@screens/novel/NovelProvider'; import { FlashList, ViewToken } from '@shopify/flash-list'; import { ChapterInfo } from '@database/types'; diff --git a/src/screens/reader/components/ReaderAppbar.tsx b/src/screens/reader/components/ReaderAppbar.tsx index e957b01bbb..e2c3a04c98 100644 --- a/src/screens/reader/components/ReaderAppbar.tsx +++ b/src/screens/reader/components/ReaderAppbar.tsx @@ -12,7 +12,7 @@ import Animated, { import { ThemeColors } from '@theme/types'; import { bookmarkChapter } from '@database/queries/ChapterQueries'; import { useChapterContext } from '../ChapterContext'; -import { useNovelContext } from '@screens/novel/NovelContext'; +import { useNovelContext } from '@screens/novel/NovelProvider'; interface ReaderAppbarProps { theme: ThemeColors; diff --git a/src/screens/reader/components/ReaderFooter.tsx b/src/screens/reader/components/ReaderFooter.tsx index 29bb1fc25b..295634baa8 100644 --- a/src/screens/reader/components/ReaderFooter.tsx +++ b/src/screens/reader/components/ReaderFooter.tsx @@ -11,7 +11,7 @@ import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/typ import { ChapterScreenProps } from '@navigators/types'; import { useChapterContext } from '../ChapterContext'; import { SCREEN_HEIGHT } from '@gorhom/bottom-sheet'; -import { useNovelContext } from '@screens/novel/NovelContext'; +import { useNovelContext } from '@screens/novel/NovelProvider'; import { useTheme } from '@hooks/persisted'; interface ChapterFooterProps { diff --git a/src/screens/reader/hooks/useChapter.ts b/src/screens/reader/hooks/useChapter.ts index 24653ac016..611fda9b9b 100644 --- a/src/screens/reader/hooks/useChapter.ts +++ b/src/screens/reader/hooks/useChapter.ts @@ -32,7 +32,7 @@ import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; import NativeVolumeButtonListener from '@specs/NativeVolumeButtonListener'; import NativeFile from '@specs/NativeFile'; -import { useNovelContext } from '@screens/novel/NovelContext'; +import { useNovelContext } from '@screens/novel/NovelProvider'; const emmiter = new NativeEventEmitter(NativeVolumeButtonListener); diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx index 84b8375719..97adafce69 100644 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ b/src/screens/settings/SettingsAdvancedScreen.tsx @@ -5,7 +5,7 @@ import { Portal, Text, TextInput } from 'react-native-paper'; import { useTheme, useUserAgent } from '@hooks/persisted'; import { showToast } from '@utils/showToast'; -import { deleteCachedNovels } from '@hooks/persisted/useNovel'; +import { deleteCachedNovels } from '@hooks/persisted/novel/useNovel'; import { getString } from '@strings/translations'; import { useBoolean } from '@hooks'; import ConfirmationDialog from '@components/ConfirmationDialog/ConfirmationDialog'; diff --git a/src/services/migrate/migrateNovel.ts b/src/services/migrate/migrateNovel.ts index a75d8cb549..8a42a338a0 100644 --- a/src/services/migrate/migrateNovel.ts +++ b/src/services/migrate/migrateNovel.ts @@ -12,7 +12,7 @@ import { getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv'; import { LAST_READ_PREFIX, NOVEL_SETTINSG_PREFIX, -} from '@hooks/persisted/useNovel'; +} from '@hooks/persisted/novel/useNovel'; import { sleep } from '@utils/sleep'; import ServiceManager, { BackgroundTaskMetadata, From 7296328d6124652ed32dfb7eba8da162f0c4a830 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:06:12 +0200 Subject: [PATCH 02/18] added useNovelPages and useNovelState --- src/hooks/persisted/novel/useNovel.ts | 30 ---- src/hooks/persisted/novel/useNovelPages.ts | 37 +++++ src/hooks/persisted/novel/useNovelState.ts | 133 ++++++++++++++++++ .../novel/components/NovelScreenList.tsx | 66 --------- .../novel/context/NovelPageContext.tsx | 41 ++++++ .../novel/context/NovelStateContext.tsx | 57 ++++++++ 6 files changed, 268 insertions(+), 96 deletions(-) create mode 100644 src/hooks/persisted/novel/useNovelPages.ts create mode 100644 src/hooks/persisted/novel/useNovelState.ts create mode 100644 src/screens/novel/context/NovelPageContext.tsx create mode 100644 src/screens/novel/context/NovelStateContext.tsx diff --git a/src/hooks/persisted/novel/useNovel.ts b/src/hooks/persisted/novel/useNovel.ts index 6143f77459..b4d097e21e 100644 --- a/src/hooks/persisted/novel/useNovel.ts +++ b/src/hooks/persisted/novel/useNovel.ts @@ -207,39 +207,9 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { [novelSettings, setNovelSettings], ); - const followNovel = useCallback(() => { - switchNovelToLibrary(novelPath, pluginId).then(() => { - if (novel) { - setNovel({ - ...novel, - inLibrary: !novel?.inLibrary, - }); - } - }); - }, [novel, novelPath, pluginId, switchNovelToLibrary]); - // #endregion // #region getters - const getNovel = useCallback(async () => { - let tmpNovel = getNovelByPath(novelPath, pluginId); - if (!tmpNovel) { - const sourceNovel = await fetchNovel(pluginId, novelPath).catch(() => { - throw new Error(getString('updatesScreen.unableToGetNovel')); - }); - - await insertNovelAndChapters(pluginId, sourceNovel); - tmpNovel = getNovelByPath(novelPath, pluginId); - - if (!tmpNovel) { - return; - } - } - setPages(calculatePages(tmpNovel)); - - setNovel(tmpNovel); - }, [novelPath, pluginId]); - const getChapters = useCallback(async () => { const page = pages[pageIndex]; diff --git a/src/hooks/persisted/novel/useNovelPages.ts b/src/hooks/persisted/novel/useNovelPages.ts new file mode 100644 index 0000000000..f121d90068 --- /dev/null +++ b/src/hooks/persisted/novel/useNovelPages.ts @@ -0,0 +1,37 @@ +import { getCustomPages } from '@database/queries/ChapterQueries'; +import { NovelInfo } from '@database/types'; +import { NovelPageContext } from '@screens/novel/context/NovelPageContext'; +import { useCallback, useContext } from 'react'; + +const useNovelPages = () => { + const novelPage = useContext(NovelPageContext); + if (!novelPage) { + throw new Error( + 'useNovelState must be used within NovelPageContextProvider', + ); + } + const { setPages } = novelPage; + + const calculatePages = useCallback( + (tmpNovel: NovelInfo, setNewPages?: boolean) => { + let tmpPages: string[]; + if (tmpNovel.totalPages > 0) { + tmpPages = Array(tmpNovel.totalPages) + .fill(0) + .map((_, idx) => String(idx + 1)); + } else { + tmpPages = getCustomPages(tmpNovel.id).map(c => c.page); + } + const res = tmpPages.length > 1 ? tmpPages : ['1']; + + if (setNewPages) { + setPages(res); + } + return res; + }, + [setPages], + ); + return { calculatePages }; +}; + +export default useNovelPages; diff --git a/src/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts new file mode 100644 index 0000000000..2858bc710c --- /dev/null +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -0,0 +1,133 @@ +import { useLibraryContext } from '@components/Context/LibraryContext'; +import { + getNovelByPath, + insertNovelAndChapters, + pickCustomNovelCover, +} from '@database/queries/NovelQueries'; +import { downloadFile } from '@plugins/helpers/fetch'; +import FileManager from '@specs/NativeFile'; +import { NovelStateContext } from '@screens/novel/context/NovelStateContext'; +import { fetchNovel } from '@services/plugin/fetch'; +import { getString } from '@strings/translations'; +import { showToast } from '@utils/showToast'; +import { StorageAccessFramework } from 'expo-file-system'; +import { useCallback, useContext } from 'react'; +import useNovelPages from './useNovelPages'; + +const useNovelState = () => { + const novelState = useContext(NovelStateContext); + if (!novelState) { + throw new Error( + 'useNovelState must be used within NovelStateContextProvider', + ); + } + + const { novel, setNovel, path, pluginId } = novelState; + const { calculatePages } = useNovelPages(); + const { switchNovelToLibrary } = useLibraryContext(); + + const getNovel = useCallback(async () => { + let tmpNovel = getNovelByPath(path, pluginId); + if (!tmpNovel) { + const sourceNovel = await fetchNovel(pluginId, path).catch(() => { + throw new Error(getString('updatesScreen.unableToGetNovel')); + }); + + await insertNovelAndChapters(pluginId, sourceNovel); + tmpNovel = getNovelByPath(path, pluginId); + + if (!tmpNovel) { + return; + } + } + calculatePages(tmpNovel, true); + + setNovel(tmpNovel); + }, [calculatePages, path, pluginId, setNovel]); + + const followNovel = useCallback(() => { + switchNovelToLibrary(path, pluginId).then(() => { + if (novel) { + setNovel({ + ...novel, + inLibrary: !novel?.inLibrary, + }); + } + }); + }, [novel, path, pluginId, setNovel, switchNovelToLibrary]); + + const setCustomNovelCover = async () => { + if (!novel) { + return; + } + const newCover = await pickCustomNovelCover(novel); + if (newCover) { + setNovel({ + ...novel, + cover: newCover, + }); + } + }; + + const saveNovelCover = useCallback(async () => { + if (!novel) { + showToast(getString('novelScreen.coverNotSaved')); + return; + } + if (!novel.cover) { + showToast(getString('novelScreen.noCoverFound')); + return; + } + const permissions = + await StorageAccessFramework.requestDirectoryPermissionsAsync(); + if (!permissions.granted) { + showToast(getString('novelScreen.coverNotSaved')); + return; + } + const cover = novel.cover; + let tempCoverUri: string | null = null; + try { + let imageExtension = cover.split('.').pop() || 'png'; + if (imageExtension.includes('?')) { + imageExtension = imageExtension.split('?')[0] || 'png'; + } + imageExtension = ['jpg', 'jpeg', 'png', 'webp'].includes( + imageExtension || '', + ) + ? imageExtension + : 'png'; + + const novelName = novel.name.replace(/[^a-zA-Z0-9]/g, '_'); + const fileName = `${novelName}_${novel.id}.${imageExtension}`; + const coverDestUri = await StorageAccessFramework.createFileAsync( + permissions.directoryUri, + fileName, + 'image/' + imageExtension, + ); + if (cover.startsWith('http')) { + const { ExternalCachesDirectoryPath } = FileManager.getConstants(); + tempCoverUri = ExternalCachesDirectoryPath + '/' + fileName; + await downloadFile(cover, tempCoverUri); + FileManager.copyFile(tempCoverUri, coverDestUri); + } else { + FileManager.copyFile(cover, coverDestUri); + } + showToast(getString('novelScreen.coverSaved')); + } catch (err: any) { + showToast(err.message); + } finally { + if (tempCoverUri) { + FileManager.unlink(tempCoverUri); + } + } + }, [novel]); + + return { + getNovel, + followNovel, + setCustomNovelCover, + saveNovelCover, + }; +}; + +export default useNovelState; diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index 4612741158..8e5be04c36 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -282,72 +282,6 @@ const NovelScreenList = ({ [selected, chapters, disableHapticFeedback, setSelected], ); - const setCustomNovelCover = useCallback(async () => { - if (!novel || novel.id === 'NO_ID') { - return; - } - const newCover = await pickCustomNovelCover(novel); - if (newCover) { - setNovel({ - ...novel, - cover: newCover, - }); - } - }, [novel, setNovel]); - - const saveNovelCover = useCallback(async () => { - if (!novel) { - showToast(getString('novelScreen.coverNotSaved')); - return; - } - if (!novel.cover) { - showToast(getString('novelScreen.noCoverFound')); - return; - } - const permissions = - await StorageAccessFramework.requestDirectoryPermissionsAsync(); - if (!permissions.granted) { - showToast(getString('novelScreen.coverNotSaved')); - return; - } - const cover = novel.cover; - let tempCoverUri: string | null = null; - try { - let imageExtension = cover.split('.').pop() || 'png'; - if (imageExtension.includes('?')) { - imageExtension = imageExtension.split('?')[0] || 'png'; - } - imageExtension = ['jpg', 'jpeg', 'png', 'webp'].includes( - imageExtension || '', - ) - ? imageExtension - : 'png'; - - const novelName = novel.name.replace(/[^a-zA-Z0-9]/g, '_'); - const fileName = `${novelName}_${novel.id}.${imageExtension}`; - const coverDestUri = await StorageAccessFramework.createFileAsync( - permissions.directoryUri, - fileName, - 'image/' + imageExtension, - ); - if (cover.startsWith('http')) { - const { ExternalCachesDirectoryPath } = FileManager.getConstants(); - tempCoverUri = ExternalCachesDirectoryPath + '/' + fileName; - await downloadFile(cover, tempCoverUri); - FileManager.copyFile(tempCoverUri, coverDestUri); - } else { - FileManager.copyFile(cover, coverDestUri); - } - showToast(getString('novelScreen.coverSaved')); - } catch (err: any) { - showToast(err.message); - } finally { - if (tempCoverUri) { - FileManager.unlink(tempCoverUri); - } - } - }, [novel]); - // Memoize the renderItem function const renderItem = useCallback( ({ item, index }: { item: ChapterInfo; index: number }) => { diff --git a/src/screens/novel/context/NovelPageContext.tsx b/src/screens/novel/context/NovelPageContext.tsx new file mode 100644 index 0000000000..aeabd64ae2 --- /dev/null +++ b/src/screens/novel/context/NovelPageContext.tsx @@ -0,0 +1,41 @@ +import { createContext, useMemo, useState } from 'react'; + +// PageStateContext.tsx +interface PageState { + pages: string[]; + pageIndex: number; +} + +interface PageActions { + setPages: (pages: string[]) => void; + setPageIndex: (index: number) => void; +} + +export const NovelPageContext = createContext<(PageState & PageActions) | null>( + null, +); + +export function NovelPageContextProvider({ + children, +}: { + children: React.JSX.Element; +}) { + const [pages, setPages] = useState([]); + const [pageIndex, setPageIndex] = useState(0); + + const contextValue = useMemo( + () => ({ + pages, + pageIndex, + setPages, + setPageIndex, + }), + [pageIndex, pages], + ); + + return ( + + {children} + + ); +} diff --git a/src/screens/novel/context/NovelStateContext.tsx b/src/screens/novel/context/NovelStateContext.tsx new file mode 100644 index 0000000000..d6e67064f8 --- /dev/null +++ b/src/screens/novel/context/NovelStateContext.tsx @@ -0,0 +1,57 @@ +import { NovelInfo } from '@database/types'; +import { ReaderStackParamList } from '@navigators/types'; +import { RouteProp } from '@react-navigation/native'; +import { createContext, useMemo, useState } from 'react'; + +type Route = + | RouteProp['params'] + | RouteProp['params']['novel']; +type Path = Route['path']; +type PluginId = Route['pluginId']; + +interface NovelState { + novel: NovelInfo | undefined; + loading: boolean; // for novel loading only + path: Path; + pluginId: PluginId; +} + +interface NovelActions { + setNovel: (novel: NovelInfo) => void; + setLoading: (loading: boolean) => void; +} + +export const NovelStateContext = createContext< + (NovelState & NovelActions) | null +>(null); + +export function NovelStateContextProvider({ + children, + path, + pluginId, +}: { + children: React.JSX.Element; + path: Path; + pluginId: PluginId; +}) { + const [novel, setNovel] = useState(undefined); + const [loading, setLoading] = useState(true); + + const contextValue = useMemo( + () => ({ + novel, + loading, + path, + pluginId, + setNovel, + setLoading, + }), + [loading, novel, path, pluginId], + ); + + return ( + + {children} + + ); +} From 0a3e89e874e542fa92cea9ccfad4a74e6c51bdc2 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:24:17 +0200 Subject: [PATCH 03/18] added useNovelChapters --- src/hooks/persisted/novel/useNovel.ts | 377 +---------------- src/hooks/persisted/novel/useNovelChapters.ts | 400 ++++++++++++++++++ src/hooks/persisted/novel/useNovelPages.ts | 4 +- src/hooks/persisted/novel/useNovelState.ts | 3 + .../novel/context/NovelChaptersContext.tsx | 132 ++++++ 5 files changed, 538 insertions(+), 378 deletions(-) create mode 100644 src/hooks/persisted/novel/useNovelChapters.ts create mode 100644 src/screens/novel/context/NovelChaptersContext.tsx diff --git a/src/hooks/persisted/novel/useNovel.ts b/src/hooks/persisted/novel/useNovel.ts index b4d097e21e..b4c970b544 100644 --- a/src/hooks/persisted/novel/useNovel.ts +++ b/src/hooks/persisted/novel/useNovel.ts @@ -43,7 +43,7 @@ import { useLibraryContext } from '@components/Context/LibraryContext'; export const TRACKED_NOVEL_PREFIX = 'TRACKED_NOVEL_PREFIX'; export const NOVEL_PAGE_INDEX_PREFIX = 'NOVEL_PAGE_INDEX_PREFIX'; -export const NOVEL_SETTINSG_PREFIX = 'NOVEL_SETTINGS'; + export const LAST_READ_PREFIX = 'LAST_READ_PREFIX'; const defaultNovelSettings: NovelSettings = { @@ -86,10 +86,6 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { const [lastRead, setLastRead] = useMMKVObject( `${LAST_READ_PREFIX}_${pluginId}_${novelPath}`, ); - const [novelSettings = defaultNovelSettings, setNovelSettings] = - useMMKVObject( - `${NOVEL_SETTINSG_PREFIX}_${pluginId}_${novelPath}`, - ); const [chapters, _setChapters] = useState([]); const [batchInformation, setBatchInformation] = useState<{ @@ -106,44 +102,9 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { : { batch: 0, total: 0 }, ); - const settingsSort = novelSettings.sort || defaultChapterSort; // #endregion // #region setters - function calculatePages(tmpNovel: NovelInfo) { - let tmpPages: string[]; - if (tmpNovel.totalPages > 0) { - tmpPages = Array(tmpNovel.totalPages) - .fill(0) - .map((_, idx) => String(idx + 1)); - } else { - tmpPages = getCustomPages(tmpNovel.id).map(c => c.page); - } - - return tmpPages.length > 1 ? tmpPages : ['1']; - } - - const mutateChapters = useCallback( - (mutation: (chs: ChapterInfo[]) => ChapterInfo[]) => { - if (novel) { - _setChapters(mutation); - } - }, - [novel], - ); - - const updateChapter = useCallback( - (index: number, update: Partial) => { - if (novel) { - _setChapters(chs => { - chs[index] = { ...chs[index], ...update }; - return chs; - }); - } - }, - [novel], - ); - const openPage = useCallback( (index: number) => { setPageIndex(index); @@ -151,42 +112,6 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { [setPageIndex], ); - const transformChapters = useCallback( - (chs: ChapterInfo[]) => { - if (!novel) return chs; - const newChapters = chs.map(chapter => { - const parsedTime = dayjs(chapter.releaseTime); - const releaseTime = parsedTime.isValid() - ? parsedTime.format('LL') - : chapter.releaseTime; - const chapterNumber = chapter.chapterNumber - ? chapter.chapterNumber - : parseChapterNumber(novel.name, chapter.name); - return { - ...chapter, - releaseTime, - chapterNumber, - }; - }); - return newChapters; - }, - [novel], - ); - - const setChapters = useCallback( - async (chs: ChapterInfo[]) => { - _setChapters(transformChapters(chs)); - }, - [transformChapters], - ); - - const extendChapters = useCallback( - async (chs: ChapterInfo[]) => { - _setChapters(prev => prev.concat(transformChapters(chs))); - }, - [transformChapters], - ); - const sortAndFilterChapters = useCallback( async (sort?: string, filter?: string) => { if (novel) { @@ -210,293 +135,8 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { // #endregion // #region getters - const getChapters = useCallback(async () => { - const page = pages[pageIndex]; - - if (novel && page) { - let newChapters: ChapterInfo[] = []; - - const config = [ - novel.id, - settingsSort, - novelSettings.filter, - page, - ] as const; - - let chapterCount = getChapterCount(novel.id, page); - - if (chapterCount) { - try { - newChapters = getPageChaptersBatched(...config) || []; - } catch (error) { - console.error('teaser', error); - } - } - // Fetch next page if no chapters - else if (Number(page)) { - _setChapters([]); - const sourcePage = await fetchPage(pluginId, novelPath, page); - const sourceChapters = sourcePage.chapters.map(ch => { - return { - ...ch, - page, - }; - }); - await insertChapters(novel.id, sourceChapters); - newChapters = await _getPageChapters(...config); - chapterCount = getChapterCount(novel.id, page); - } - - setBatchInformation({ - batch: 0, - total: Math.floor(chapterCount / 300), - totalChapters: chapterCount, - }); - setChapters(newChapters); - } - }, [ - novel, - novelPath, - novelSettings.filter, - pageIndex, - pages, - pluginId, - setChapters, - settingsSort, - ]); - - const getNextChapterBatch = useCallback(() => { - const page = pages[pageIndex]; - const nextBatch = batchInformation.batch + 1; - if (novel && page && nextBatch <= batchInformation.total) { - let newChapters: ChapterInfo[] = []; - - try { - newChapters = - getPageChaptersBatched( - novel.id, - settingsSort, - novelSettings.filter, - page, - nextBatch, - ) || []; - } catch (error) { - console.error('teaser', error); - } - setBatchInformation({ ...batchInformation, batch: nextBatch }); - extendChapters(newChapters); - } - }, [ - batchInformation, - extendChapters, - novel, - novelSettings.filter, - pageIndex, - pages, - settingsSort, - ]); - - // #endregion - // #region Mark chapters - - const bookmarkChapters = useCallback( - (_chapters: ChapterInfo[]) => { - _chapters.map(_chapter => { - _bookmarkChapter(_chapter.id); - }); - mutateChapters(chs => - chs.map(chapter => { - if (_chapters.some(_c => _c.id === chapter.id)) { - return { - ...chapter, - bookmark: !chapter.bookmark, - }; - } - return chapter; - }), - ); - }, - [mutateChapters], - ); - - const markPreviouschaptersRead = useCallback( - (chapterId: number) => { - if (novel) { - _markPreviuschaptersRead(chapterId, novel.id); - mutateChapters(chs => - chs.map(chapter => - chapter.id <= chapterId ? { ...chapter, unread: false } : chapter, - ), - ); - } - }, - [mutateChapters, novel], - ); - - const markChapterRead = useCallback( - (chapterId: number) => { - _markChapterRead(chapterId); - - mutateChapters(chs => - chs.map(c => { - if (c.id !== chapterId) { - return c; - } - return { - ...c, - unread: false, - }; - }), - ); - }, - [mutateChapters], - ); - - const updateChapterProgress = useCallback( - (chapterId: number, progress: number) => { - _updateChapterProgress(chapterId, Math.min(progress, 100)); - - mutateChapters(chs => - chs.map(c => { - if (c.id !== chapterId) { - return c; - } - return { - ...c, - progress, - }; - }), - ); - }, - [mutateChapters], - ); - - const markChaptersRead = useCallback( - (_chapters: ChapterInfo[]) => { - const chapterIds = _chapters.map(chapter => chapter.id); - _markChaptersRead(chapterIds); - - mutateChapters(chs => - chs.map(chapter => { - if (chapterIds.includes(chapter.id)) { - return { - ...chapter, - unread: false, - }; - } - return chapter; - }), - ); - }, - [mutateChapters], - ); - - const markPreviousChaptersUnread = useCallback( - (chapterId: number) => { - if (novel) { - _markPreviousChaptersUnread(chapterId, novel.id); - mutateChapters(chs => - chs.map(chapter => - chapter.id <= chapterId ? { ...chapter, unread: true } : chapter, - ), - ); - } - }, - [mutateChapters, novel], - ); - - const markChaptersUnread = useCallback( - (_chapters: ChapterInfo[]) => { - const chapterIds = _chapters.map(chapter => chapter.id); - _markChaptersUnread(chapterIds); - - mutateChapters(chs => - chs.map(chapter => { - if (chapterIds.includes(chapter.id)) { - return { - ...chapter, - unread: true, - }; - } - return chapter; - }), - ); - }, - [mutateChapters], - ); - // #endregion - // #region refresh and delete - - const deleteChapter = useCallback( - (_chapter: ChapterInfo) => { - if (novel) { - _deleteChapter(novel.pluginId, novel.id, _chapter.id).then(() => { - mutateChapters(chs => - chs.map(chapter => { - if (chapter.id !== _chapter.id) { - return chapter; - } - return { - ...chapter, - isDownloaded: false, - }; - }), - ); - showToast(getString('common.deleted', { name: _chapter.name })); - }); - } - }, - [mutateChapters, novel], - ); - - const deleteChapters = useCallback( - (_chaters: ChapterInfo[]) => { - if (novel) { - _deleteChapters(novel.pluginId, novel.id, _chaters).then(() => { - showToast( - getString('updatesScreen.deletedChapters', { - num: _chaters.length, - }), - ); - mutateChapters(chs => - chs.map(chapter => { - if (_chaters.some(_c => _c.id === chapter.id)) { - return { - ...chapter, - isDownloaded: false, - }; - } - return chapter; - }), - ); - }); - } - }, - [novel, mutateChapters], - ); - - const refreshChapters = useCallback(() => { - if (novel?.id && !fetching) { - _getPageChapters( - novel.id, - settingsSort, - novelSettings.filter, - currentPage, - ).then(chs => { - setChapters(chs); - }); - } - }, [ - novel?.id, - fetching, - settingsSort, - novelSettings.filter, - currentPage, - setChapters, - ]); - // #endregion // #region useEffects useEffect(() => { @@ -511,21 +151,6 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { } }, [getNovel, novel]); - useEffect(() => { - if (novel === undefined) return; - setFetching(true); - getChapters() - .catch(e => { - if (__DEV__) console.error(e); - - showToast(e.message); - setFetching(false); - }) - .finally(() => { - setFetching(false); - }); - }, [getChapters, novel, novelOrPath]); - // #endregion return useMemo( diff --git a/src/hooks/persisted/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts new file mode 100644 index 0000000000..f08fa8ffa9 --- /dev/null +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -0,0 +1,400 @@ +/* eslint-disable no-console */ +import { NovelChaptersContext } from '@screens/novel/context/NovelChaptersContext'; +import { useCallback, useContext, useEffect, useMemo } from 'react'; +import { + bookmarkChapter as _bookmarkChapter, + markChapterRead as _markChapterRead, + markChaptersRead as _markChaptersRead, + markPreviuschaptersRead as _markPreviuschaptersRead, + markPreviousChaptersUnread as _markPreviousChaptersUnread, + markChaptersUnread as _markChaptersUnread, + deleteChapter as _deleteChapter, + deleteChapters as _deleteChapters, + getPageChapters as _getPageChapters, + insertChapters, + getChapterCount, + getPageChaptersBatched, + updateChapterProgress as _updateChapterProgress, +} from '@database/queries/ChapterQueries'; +import { ChapterInfo } from '@database/types'; +import { fetchPage } from '@services/plugin/fetch'; +import { getString } from '@strings/translations'; +import { showToast } from '@utils/showToast'; +import useNovelState from './useNovelState'; +import useNovelPages from './useNovelPages'; +import { useMMKVObject } from 'react-native-mmkv'; +import { NovelSettings } from './useNovel'; +import { useAppSettings } from '../useSettings'; + +export const NOVEL_SETTINSG_PREFIX = 'NOVEL_SETTINGS'; +const defaultNovelSettings: NovelSettings = { + showChapterTitles: true, +}; + +const useNovelChapters = () => { + const NovelChapters = useContext(NovelChaptersContext); + if (!NovelChapters) { + throw new Error( + 'useNovelChapters must be used within NovelChaptersContextProvider', + ); + } + const { + chapters, + fetching, + batchInformation, + setChapters, + _setChapters, + extendChapters, + mutateChapters, + setFetching, + setBatchInformation, + } = NovelChapters; + const { novel, path, pluginId } = useNovelState(); + const { pages, pageIndex } = useNovelPages(); + const { defaultChapterSort } = useAppSettings(); + const currentPage = pages[pageIndex]; + + const [novelSettings = defaultNovelSettings] = useMMKVObject( + `${NOVEL_SETTINSG_PREFIX}_${pluginId}_${path}`, + ); + + const settingsSort = novelSettings.sort || defaultChapterSort; + + const getChapters = useCallback(async () => { + if (novel && currentPage) { + let newChapters: ChapterInfo[] = []; + + const config = [ + novel.id, + settingsSort, + novelSettings.filter, + currentPage, + ] as const; + + let chapterCount = getChapterCount(novel.id, currentPage); + + if (chapterCount) { + try { + newChapters = getPageChaptersBatched(...config) || []; + } catch (error) { + console.error('teaser', error); + } + } + // Fetch next page if no chapters + else if (Number(currentPage)) { + _setChapters([]); + const sourcePage = await fetchPage(pluginId, path, currentPage); + const sourceChapters = sourcePage.chapters.map(ch => { + return { + ...ch, + page: currentPage, + }; + }); + await insertChapters(novel.id, sourceChapters); + newChapters = await _getPageChapters(...config); + chapterCount = getChapterCount(novel.id, currentPage); + } + + setBatchInformation({ + batch: 0, + total: Math.floor(chapterCount / 300), + totalChapters: chapterCount, + }); + setChapters(newChapters); + } + }, [ + novel, + currentPage, + settingsSort, + novelSettings.filter, + setBatchInformation, + setChapters, + _setChapters, + pluginId, + path, + ]); + + const getNextChapterBatch = useCallback(() => { + const page = pages[pageIndex]; + const nextBatch = batchInformation.batch + 1; + if (novel && page && nextBatch <= batchInformation.total) { + let newChapters: ChapterInfo[] = []; + + try { + newChapters = + getPageChaptersBatched( + novel.id, + settingsSort, + novelSettings.filter, + page, + nextBatch, + ) || []; + } catch (error) { + console.error('teaser', error); + } + setBatchInformation({ ...batchInformation, batch: nextBatch }); + extendChapters(newChapters); + } + }, [ + batchInformation, + extendChapters, + novel, + novelSettings.filter, + pageIndex, + pages, + setBatchInformation, + settingsSort, + ]); + + // #region Mark chapters + + const bookmarkChapters = useCallback( + (_chapters: ChapterInfo[]) => { + _chapters.map(_chapter => { + _bookmarkChapter(_chapter.id); + }); + mutateChapters(chs => + chs.map(chapter => { + if (_chapters.some(_c => _c.id === chapter.id)) { + return { + ...chapter, + bookmark: !chapter.bookmark, + }; + } + return chapter; + }), + ); + }, + [mutateChapters], + ); + + const markPreviouschaptersRead = useCallback( + (chapterId: number) => { + if (novel) { + _markPreviuschaptersRead(chapterId, novel.id); + mutateChapters(chs => + chs.map(chapter => + chapter.id <= chapterId ? { ...chapter, unread: false } : chapter, + ), + ); + } + }, + [mutateChapters, novel], + ); + + const markChapterRead = useCallback( + (chapterId: number) => { + _markChapterRead(chapterId); + + mutateChapters(chs => + chs.map(c => { + if (c.id !== chapterId) { + return c; + } + return { + ...c, + unread: false, + }; + }), + ); + }, + [mutateChapters], + ); + + const updateChapterProgress = useCallback( + (chapterId: number, progress: number) => { + _updateChapterProgress(chapterId, Math.min(progress, 100)); + + mutateChapters(chs => + chs.map(c => { + if (c.id !== chapterId) { + return c; + } + return { + ...c, + progress, + }; + }), + ); + }, + [mutateChapters], + ); + + const markChaptersRead = useCallback( + (_chapters: ChapterInfo[]) => { + const chapterIds = _chapters.map(chapter => chapter.id); + _markChaptersRead(chapterIds); + + mutateChapters(chs => + chs.map(chapter => { + if (chapterIds.includes(chapter.id)) { + return { + ...chapter, + unread: false, + }; + } + return chapter; + }), + ); + }, + [mutateChapters], + ); + + const markPreviousChaptersUnread = useCallback( + (chapterId: number) => { + if (novel) { + _markPreviousChaptersUnread(chapterId, novel.id); + mutateChapters(chs => + chs.map(chapter => + chapter.id <= chapterId ? { ...chapter, unread: true } : chapter, + ), + ); + } + }, + [mutateChapters, novel], + ); + + const markChaptersUnread = useCallback( + (_chapters: ChapterInfo[]) => { + const chapterIds = _chapters.map(chapter => chapter.id); + _markChaptersUnread(chapterIds); + + mutateChapters(chs => + chs.map(chapter => { + if (chapterIds.includes(chapter.id)) { + return { + ...chapter, + unread: true, + }; + } + return chapter; + }), + ); + }, + [mutateChapters], + ); + + // #endregion + // #region refresh and delete + + const deleteChapter = useCallback( + (_chapter: ChapterInfo) => { + if (novel) { + _deleteChapter(novel.pluginId, novel.id, _chapter.id).then(() => { + mutateChapters(chs => + chs.map(chapter => { + if (chapter.id !== _chapter.id) { + return chapter; + } + return { + ...chapter, + isDownloaded: false, + }; + }), + ); + showToast(getString('common.deleted', { name: _chapter.name })); + }); + } + }, + [mutateChapters, novel], + ); + + const deleteChapters = useCallback( + (_chaters: ChapterInfo[]) => { + if (novel) { + _deleteChapters(novel.pluginId, novel.id, _chaters).then(() => { + showToast( + getString('updatesScreen.deletedChapters', { + num: _chaters.length, + }), + ); + mutateChapters(chs => + chs.map(chapter => { + if (_chaters.some(_c => _c.id === chapter.id)) { + return { + ...chapter, + isDownloaded: false, + }; + } + return chapter; + }), + ); + }); + } + }, + [novel, mutateChapters], + ); + + const refreshChapters = useCallback(() => { + if (novel?.id && !fetching) { + _getPageChapters( + novel.id, + settingsSort, + novelSettings.filter, + currentPage, + ).then(chs => { + setChapters(chs); + }); + } + }, [ + novel?.id, + fetching, + settingsSort, + novelSettings.filter, + currentPage, + setChapters, + ]); + + // #endregion + useEffect(() => { + if (novel === undefined) return; + setFetching(true); + getChapters() + .catch(e => { + if (__DEV__) console.error(e); + + showToast(e.message); + setFetching(false); + }) + .finally(() => { + setFetching(false); + }); + }, [getChapters, novel, setFetching]); + + const result = useMemo( + () => ({ + chapters, + getChapters, + getNextChapterBatch, + bookmarkChapters, + markPreviouschaptersRead, + markChapterRead, + updateChapterProgress, + markChaptersRead, + markPreviousChaptersUnread, + markChaptersUnread, + deleteChapter, + deleteChapters, + refreshChapters, + }), + [ + bookmarkChapters, + chapters, + deleteChapter, + deleteChapters, + getChapters, + getNextChapterBatch, + markChapterRead, + markChaptersRead, + markChaptersUnread, + markPreviousChaptersUnread, + markPreviouschaptersRead, + refreshChapters, + updateChapterProgress, + ], + ); + + return result; +}; + +export default useNovelChapters; diff --git a/src/hooks/persisted/novel/useNovelPages.ts b/src/hooks/persisted/novel/useNovelPages.ts index f121d90068..7ace296433 100644 --- a/src/hooks/persisted/novel/useNovelPages.ts +++ b/src/hooks/persisted/novel/useNovelPages.ts @@ -10,7 +10,7 @@ const useNovelPages = () => { 'useNovelState must be used within NovelPageContextProvider', ); } - const { setPages } = novelPage; + const { setPages, pages, pageIndex } = novelPage; const calculatePages = useCallback( (tmpNovel: NovelInfo, setNewPages?: boolean) => { @@ -31,7 +31,7 @@ const useNovelPages = () => { }, [setPages], ); - return { calculatePages }; + return { pages, pageIndex, calculatePages }; }; export default useNovelPages; diff --git a/src/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts index 2858bc710c..f4f0bb5828 100644 --- a/src/hooks/persisted/novel/useNovelState.ts +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -123,6 +123,9 @@ const useNovelState = () => { }, [novel]); return { + novel, + path, + pluginId, getNovel, followNovel, setCustomNovelCover, diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx new file mode 100644 index 0000000000..cfd9d09b41 --- /dev/null +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -0,0 +1,132 @@ +import { ChapterInfo } from '@database/types'; +import useNovelState from '@hooks/persisted/novel/useNovelState'; +import { parseChapterNumber } from '@utils/parseChapterNumber'; +import dayjs from 'dayjs'; +import { createContext, useCallback, useMemo, useState } from 'react'; + +// ChapterStateContext.tsx +interface ChapterState { + chapters: ChapterInfo[]; + fetching: boolean; // for chapter loading only + batchInformation: { + batch: number; + total: number; + totalChapters?: number; + }; +} + +interface ChapterActions { + setChapters: (chapters: ChapterInfo[]) => void; + _setChapters: (chapters: ChapterInfo[]) => void; + + extendChapters: (chapters: ChapterInfo[]) => void; + mutateChapters: (mutation: (chs: ChapterInfo[]) => ChapterInfo[]) => void; + updateChapter: (index: number, update: Partial) => void; + setFetching: (fetching: boolean) => void; + setBatchInformation: (batch: ChapterState['batchInformation']) => void; +} + +export const NovelChaptersContext = createContext< + (ChapterState & ChapterActions) | null +>(null); + +export function NovelStateContextProvider({ + children, +}: { + children: React.JSX.Element; +}) { + const { novel } = useNovelState(); + const [chapters, _setChapters] = useState([]); + const [fetching, setFetching] = useState(true); + const [batchInformation, setBatchInformation] = useState<{ + batch: number; + total: number; + totalChapters?: number; + }>({ batch: 0, total: 0 }); + + const mutateChapters = useCallback( + (mutation: (chs: ChapterInfo[]) => ChapterInfo[]) => { + if (novel) { + _setChapters(mutation); + } + }, + [novel], + ); + + const updateChapter = useCallback( + (index: number, update: Partial) => { + if (novel) { + _setChapters(chs => { + chs[index] = { ...chs[index], ...update }; + return chs; + }); + } + }, + [novel], + ); + const transformChapters = useCallback( + (chs: ChapterInfo[]) => { + if (!novel) return chs; + const newChapters = chs.map(chapter => { + const parsedTime = dayjs(chapter.releaseTime); + const releaseTime = parsedTime.isValid() + ? parsedTime.format('LL') + : chapter.releaseTime; + const chapterNumber = chapter.chapterNumber + ? chapter.chapterNumber + : parseChapterNumber(novel.name, chapter.name); + return { + ...chapter, + releaseTime, + chapterNumber, + }; + }); + return newChapters; + }, + [novel], + ); + + const setChapters = useCallback( + async (chs: ChapterInfo[]) => { + _setChapters(transformChapters(chs)); + }, + [transformChapters], + ); + + const extendChapters = useCallback( + async (chs: ChapterInfo[]) => { + _setChapters(prev => prev.concat(transformChapters(chs))); + }, + [transformChapters], + ); + + const contextValue = useMemo( + () => ({ + chapters, + fetching, + batchInformation, + setChapters, + _setChapters, + extendChapters, + mutateChapters, + updateChapter, + setFetching, + setBatchInformation, + }), + [ + batchInformation, + chapters, + extendChapters, + fetching, + mutateChapters, + setChapters, + updateChapter, + ], + ); + + return ( + + {children} + + ); +} From e3eb4f3bf9e32305c24e8fa4baca5dc0b6f0beb1 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:19:52 +0200 Subject: [PATCH 04/18] added useNovelSettings --- src/hooks/persisted/novel/useNovel.ts | 49 -------------- src/hooks/persisted/novel/useNovelChapters.ts | 29 +++----- src/hooks/persisted/novel/useNovelSettings.ts | 43 ++++++++++++ .../novel/context/NovelSettingsContext.tsx | 66 +++++++++++++++++++ src/utils/constants/mmkv.ts | 7 ++ src/utils/mmkv/deleteCachedNovels.ts | 32 +++++++++ 6 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 src/hooks/persisted/novel/useNovelSettings.ts create mode 100644 src/screens/novel/context/NovelSettingsContext.tsx create mode 100644 src/utils/constants/mmkv.ts create mode 100644 src/utils/mmkv/deleteCachedNovels.ts diff --git a/src/hooks/persisted/novel/useNovel.ts b/src/hooks/persisted/novel/useNovel.ts index b4c970b544..34d8ade59b 100644 --- a/src/hooks/persisted/novel/useNovel.ts +++ b/src/hooks/persisted/novel/useNovel.ts @@ -40,17 +40,6 @@ import { useLibraryContext } from '@components/Context/LibraryContext'; // store key: '__', // store key: '_', -export const TRACKED_NOVEL_PREFIX = 'TRACKED_NOVEL_PREFIX'; - -export const NOVEL_PAGE_INDEX_PREFIX = 'NOVEL_PAGE_INDEX_PREFIX'; - -export const LAST_READ_PREFIX = 'LAST_READ_PREFIX'; - -const defaultNovelSettings: NovelSettings = { - showChapterTitles: true, -}; -const defaultPageIndex = 0; - // #endregion // #region types @@ -112,26 +101,6 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { [setPageIndex], ); - const sortAndFilterChapters = useCallback( - async (sort?: string, filter?: string) => { - if (novel) { - setNovelSettings({ - showChapterTitles: novelSettings?.showChapterTitles, - sort, - filter, - }); - } - }, - [novel, novelSettings?.showChapterTitles, setNovelSettings], - ); - - const setShowChapterTitles = useCallback( - (v: boolean) => { - setNovelSettings({ ...novelSettings, showChapterTitles: v }); - }, - [novelSettings, setNovelSettings], - ); - // #endregion // #region getters @@ -220,22 +189,4 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { // #region DeleteCachedNovels -export const deleteCachedNovels = async () => { - const cachedNovels = await _getCachedNovels(); - for (const novel of cachedNovels) { - MMKVStorage.delete(`${TRACKED_NOVEL_PREFIX}_${novel.id}`); - MMKVStorage.delete( - `${NOVEL_PAGE_INDEX_PREFIX}_${novel.pluginId}_${novel.path}`, - ); - MMKVStorage.delete( - `${NOVEL_SETTINSG_PREFIX}_${novel.pluginId}_${novel.path}`, - ); - MMKVStorage.delete(`${LAST_READ_PREFIX}_${novel.pluginId}_${novel.path}`); - const novelDir = NOVEL_STORAGE + '/' + novel.pluginId + '/' + novel.id; - if (NativeFile.exists(novelDir)) { - NativeFile.unlink(novelDir); - } - } - _deleteCachedNovels(); -}; // #endregion diff --git a/src/hooks/persisted/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts index f08fa8ffa9..dc8418f70f 100644 --- a/src/hooks/persisted/novel/useNovelChapters.ts +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -22,14 +22,7 @@ import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; import useNovelState from './useNovelState'; import useNovelPages from './useNovelPages'; -import { useMMKVObject } from 'react-native-mmkv'; -import { NovelSettings } from './useNovel'; -import { useAppSettings } from '../useSettings'; - -export const NOVEL_SETTINSG_PREFIX = 'NOVEL_SETTINGS'; -const defaultNovelSettings: NovelSettings = { - showChapterTitles: true, -}; +import useNovelSettings from './useNovelSettings'; const useNovelChapters = () => { const NovelChapters = useContext(NovelChaptersContext); @@ -51,22 +44,16 @@ const useNovelChapters = () => { } = NovelChapters; const { novel, path, pluginId } = useNovelState(); const { pages, pageIndex } = useNovelPages(); - const { defaultChapterSort } = useAppSettings(); + const { novelSettings } = useNovelSettings(); const currentPage = pages[pageIndex]; - const [novelSettings = defaultNovelSettings] = useMMKVObject( - `${NOVEL_SETTINSG_PREFIX}_${pluginId}_${path}`, - ); - - const settingsSort = novelSettings.sort || defaultChapterSort; - const getChapters = useCallback(async () => { if (novel && currentPage) { let newChapters: ChapterInfo[] = []; const config = [ novel.id, - settingsSort, + novelSettings.sort, novelSettings.filter, currentPage, ] as const; @@ -105,7 +92,7 @@ const useNovelChapters = () => { }, [ novel, currentPage, - settingsSort, + novelSettings.sort, novelSettings.filter, setBatchInformation, setChapters, @@ -124,7 +111,7 @@ const useNovelChapters = () => { newChapters = getPageChaptersBatched( novel.id, - settingsSort, + novelSettings.sort, novelSettings.filter, page, nextBatch, @@ -143,7 +130,7 @@ const useNovelChapters = () => { pageIndex, pages, setBatchInformation, - settingsSort, + novelSettings.sort, ]); // #region Mark chapters @@ -329,7 +316,7 @@ const useNovelChapters = () => { if (novel?.id && !fetching) { _getPageChapters( novel.id, - settingsSort, + novelSettings.sort, novelSettings.filter, currentPage, ).then(chs => { @@ -339,7 +326,7 @@ const useNovelChapters = () => { }, [ novel?.id, fetching, - settingsSort, + novelSettings.sort, novelSettings.filter, currentPage, setChapters, diff --git a/src/hooks/persisted/novel/useNovelSettings.ts b/src/hooks/persisted/novel/useNovelSettings.ts new file mode 100644 index 0000000000..9b317af2ee --- /dev/null +++ b/src/hooks/persisted/novel/useNovelSettings.ts @@ -0,0 +1,43 @@ +import { NovelSettingsContext } from '@screens/novel/context/NovelSettingsContext'; +import { useCallback, useContext, useMemo } from 'react'; + +const useNovelSettings = () => { + const novelPage = useContext(NovelSettingsContext); + if (!novelPage) { + throw new Error( + 'useNovelState must be used within NovelSettingsContextProvider', + ); + } + const { novelSettings, setNovelSettings } = novelPage; + + const sortAndFilterChapters = useCallback( + async (sort?: string, filter?: string) => { + setNovelSettings({ + showChapterTitles: novelSettings?.showChapterTitles, + sort, + filter, + }); + }, + [novelSettings?.showChapterTitles, setNovelSettings], + ); + + const setShowChapterTitles = useCallback( + (v: boolean) => { + setNovelSettings({ ...novelSettings, showChapterTitles: v }); + }, + [novelSettings, setNovelSettings], + ); + + const result = useMemo( + () => ({ + novelSettings, + sortAndFilterChapters, + setShowChapterTitles, + }), + [novelSettings, setShowChapterTitles, sortAndFilterChapters], + ); + + return result; +}; + +export default useNovelSettings; diff --git a/src/screens/novel/context/NovelSettingsContext.tsx b/src/screens/novel/context/NovelSettingsContext.tsx new file mode 100644 index 0000000000..bb9b5017db --- /dev/null +++ b/src/screens/novel/context/NovelSettingsContext.tsx @@ -0,0 +1,66 @@ +import { useAppSettings } from '@hooks/persisted'; +import { NovelSettings } from '@hooks/persisted/novel/useNovel'; +import { ReaderStackParamList } from '@navigators/types'; +import { RouteProp } from '@react-navigation/native'; +import { createContext, useMemo } from 'react'; +import { useMMKVObject } from 'react-native-mmkv'; + +type Route = + | RouteProp['params'] + | RouteProp['params']['novel']; +type Path = Route['path']; +type PluginId = Route['pluginId']; + +interface SettingsState { + novelSettings: NovelSettings; +} + +interface SettingsActions { + setNovelSettings: (settings: NovelSettings) => void; +} + +export const NovelSettingsContext = createContext< + (SettingsState & SettingsActions) | null +>(null); + +const defaultNovelSettings: NovelSettings = { + showChapterTitles: true, +}; + +export function NovelSettingsContextProvider({ + children, + path, + pluginId, +}: { + children: React.JSX.Element; + path: Path; + pluginId: PluginId; +}) { + const { defaultChapterSort } = useAppSettings(); + + const [mmkvNovelSettings = defaultNovelSettings, setNovelSettings] = + useMMKVObject( + `${NOVEL_SETTINSG_PREFIX}_${pluginId}_${path}`, + ); + + const novelSettings = useMemo(() => { + if (!mmkvNovelSettings.sort) { + mmkvNovelSettings.sort = defaultChapterSort; + } + return mmkvNovelSettings; + }, [defaultChapterSort, mmkvNovelSettings]); + + const contextValue = useMemo( + () => ({ + novelSettings, + setNovelSettings, + }), + [novelSettings, setNovelSettings], + ); + + return ( + + {children} + + ); +} diff --git a/src/utils/constants/mmkv.ts b/src/utils/constants/mmkv.ts new file mode 100644 index 0000000000..95ab05687d --- /dev/null +++ b/src/utils/constants/mmkv.ts @@ -0,0 +1,7 @@ +export const TRACKED_NOVEL_PREFIX = 'TRACKED_NOVEL_PREFIX'; + +export const NOVEL_PAGE_INDEX_PREFIX = 'NOVEL_PAGE_INDEX_PREFIX'; + +export const LAST_READ_PREFIX = 'LAST_READ_PREFIX'; + +export const NOVEL_SETTINSG_PREFIX = 'NOVEL_SETTINGS'; diff --git a/src/utils/mmkv/deleteCachedNovels.ts b/src/utils/mmkv/deleteCachedNovels.ts new file mode 100644 index 0000000000..6fa23a5e32 --- /dev/null +++ b/src/utils/mmkv/deleteCachedNovels.ts @@ -0,0 +1,32 @@ +import { MMKVStorage } from '@utils/mmkv/mmkv'; +import { + deleteCachedNovels as _deleteCachedNovels, + getCachedNovels as _getCachedNovels, +} from '@database/queries/NovelQueries'; +import { NOVEL_STORAGE } from '@utils/Storages'; +import NativeFile from '@specs/NativeFile'; +import { + LAST_READ_PREFIX, + NOVEL_PAGE_INDEX_PREFIX, + NOVEL_SETTINSG_PREFIX, + TRACKED_NOVEL_PREFIX, +} from '@utils/constants/mmkv'; + +export const deleteCachedNovels = async () => { + const cachedNovels = await _getCachedNovels(); + for (const novel of cachedNovels) { + MMKVStorage.delete(`${TRACKED_NOVEL_PREFIX}_${novel.id}`); + MMKVStorage.delete( + `${NOVEL_PAGE_INDEX_PREFIX}_${novel.pluginId}_${novel.path}`, + ); + MMKVStorage.delete( + `${NOVEL_SETTINSG_PREFIX}_${novel.pluginId}_${novel.path}`, + ); + MMKVStorage.delete(`${LAST_READ_PREFIX}_${novel.pluginId}_${novel.path}`); + const novelDir = NOVEL_STORAGE + '/' + novel.pluginId + '/' + novel.id; + if (NativeFile.exists(novelDir)) { + NativeFile.unlink(novelDir); + } + } + _deleteCachedNovels(); +}; From 0c8e0309af76df0f8e5b92671f3884c71c78753f Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:56:28 +0200 Subject: [PATCH 05/18] delete useNovel --- src/hooks/persisted/novel/useNovel.ts | 192 ------------------ src/hooks/persisted/novel/useNovelPages.ts | 24 ++- src/hooks/persisted/novel/useNovelState.ts | 53 +++-- src/hooks/persisted/novel/useTrackedNovel.ts | 2 +- .../novel/context/NovelSettingsContext.tsx | 1 + 5 files changed, 63 insertions(+), 209 deletions(-) delete mode 100644 src/hooks/persisted/novel/useNovel.ts diff --git a/src/hooks/persisted/novel/useNovel.ts b/src/hooks/persisted/novel/useNovel.ts deleted file mode 100644 index 34d8ade59b..0000000000 --- a/src/hooks/persisted/novel/useNovel.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* eslint-disable no-console */ -import { useMMKVNumber, useMMKVObject } from 'react-native-mmkv'; -import { ChapterInfo, NovelInfo } from '@database/types'; -import { MMKVStorage } from '@utils/mmkv/mmkv'; -import { - getNovelByPath, - deleteCachedNovels as _deleteCachedNovels, - getCachedNovels as _getCachedNovels, - insertNovelAndChapters, -} from '@database/queries/NovelQueries'; -import { - bookmarkChapter as _bookmarkChapter, - markChapterRead as _markChapterRead, - markChaptersRead as _markChaptersRead, - markPreviuschaptersRead as _markPreviuschaptersRead, - markPreviousChaptersUnread as _markPreviousChaptersUnread, - markChaptersUnread as _markChaptersUnread, - deleteChapter as _deleteChapter, - deleteChapters as _deleteChapters, - getPageChapters as _getPageChapters, - insertChapters, - getCustomPages, - getChapterCount, - getPageChaptersBatched, - updateChapterProgress as _updateChapterProgress, -} from '@database/queries/ChapterQueries'; -import { fetchNovel, fetchPage } from '@services/plugin/fetch'; -import { showToast } from '@utils/showToast'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { getString } from '@strings/translations'; -import dayjs from 'dayjs'; -import { parseChapterNumber } from '@utils/parseChapterNumber'; -import { NOVEL_STORAGE } from '@utils/Storages'; -import { useAppSettings } from '../useSettings'; -import NativeFile from '@specs/NativeFile'; -import { useLibraryContext } from '@components/Context/LibraryContext'; - -// #region constants - -// store key: '__', -// store key: '_', - -// #endregion -// #region types - -export interface NovelSettings { - sort?: string; - filter?: string; - showChapterTitles?: boolean; -} - -// #endregion -// #region definition useNovel - -export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { - const { switchNovelToLibrary } = useLibraryContext(); - const [loading, setLoading] = useState(true); - const [fetching, setFetching] = useState(true); - const [novel, setNovel] = useState( - typeof novelOrPath === 'object' ? novelOrPath : undefined, - ); - const [pages, setPages] = useState( - novel ? calculatePages(novel) : [], - ); - - const { defaultChapterSort } = useAppSettings(); - - const novelPath = novel?.path ?? (novelOrPath as string); - - const [pageIndex = defaultPageIndex, setPageIndex] = useMMKVNumber(` - ${NOVEL_PAGE_INDEX_PREFIX}_${pluginId}_${novelPath} - `); - const currentPage = pages[pageIndex]; - - const [lastRead, setLastRead] = useMMKVObject( - `${LAST_READ_PREFIX}_${pluginId}_${novelPath}`, - ); - - const [chapters, _setChapters] = useState([]); - const [batchInformation, setBatchInformation] = useState<{ - batch: number; - total: number; - totalChapters?: number; - }>( - typeof novelOrPath === 'object' - ? { - batch: 0, - total: 0, - totalChapters: getChapterCount(novelOrPath.id, pages[pageIndex]), - } - : { batch: 0, total: 0 }, - ); - - // #endregion - // #region setters - - const openPage = useCallback( - (index: number) => { - setPageIndex(index); - }, - [setPageIndex], - ); - - // #endregion - // #region getters - - // #endregion - - // #region useEffects - - useEffect(() => { - if (novel) { - setLoading(false); - } else { - getNovel().finally(() => { - //? Sometimes loading state changes doesn't trigger rerender causing NovelScreen to be in endless loading state - setLoading(false); - // getNovel(); - }); - } - }, [getNovel, novel]); - - // #endregion - - return useMemo( - () => ({ - loading, - fetching, - pageIndex, - pages, - novel, - lastRead, - chapters, - novelSettings, - batchInformation, - getNextChapterBatch, - getNovel, - setPageIndex, - openPage, - setNovel, - setLastRead, - sortAndFilterChapters, - followNovel, - bookmarkChapters, - markPreviouschaptersRead, - markChapterRead, - markChaptersRead, - markPreviousChaptersUnread, - markChaptersUnread, - setShowChapterTitles, - refreshChapters, - updateChapter, - updateChapterProgress, - deleteChapter, - deleteChapters, - }), - [ - loading, - fetching, - pageIndex, - pages, - novel, - lastRead, - chapters, - novelSettings, - batchInformation, - getNextChapterBatch, - getNovel, - setPageIndex, - openPage, - setLastRead, - sortAndFilterChapters, - followNovel, - bookmarkChapters, - markPreviouschaptersRead, - markChapterRead, - markChaptersRead, - markPreviousChaptersUnread, - markChaptersUnread, - setShowChapterTitles, - refreshChapters, - updateChapter, - updateChapterProgress, - deleteChapter, - deleteChapters, - ], - ); -}; - -// #region DeleteCachedNovels - -// #endregion diff --git a/src/hooks/persisted/novel/useNovelPages.ts b/src/hooks/persisted/novel/useNovelPages.ts index 7ace296433..00b13333fe 100644 --- a/src/hooks/persisted/novel/useNovelPages.ts +++ b/src/hooks/persisted/novel/useNovelPages.ts @@ -1,7 +1,7 @@ import { getCustomPages } from '@database/queries/ChapterQueries'; import { NovelInfo } from '@database/types'; import { NovelPageContext } from '@screens/novel/context/NovelPageContext'; -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; const useNovelPages = () => { const novelPage = useContext(NovelPageContext); @@ -10,7 +10,7 @@ const useNovelPages = () => { 'useNovelState must be used within NovelPageContextProvider', ); } - const { setPages, pages, pageIndex } = novelPage; + const { pages, setPages, pageIndex, setPageIndex } = novelPage; const calculatePages = useCallback( (tmpNovel: NovelInfo, setNewPages?: boolean) => { @@ -31,7 +31,25 @@ const useNovelPages = () => { }, [setPages], ); - return { pages, pageIndex, calculatePages }; + + const openPage = useCallback( + (index: number) => { + setPageIndex(index); + }, + [setPageIndex], + ); + + const result = useMemo( + () => ({ + pages, + pageIndex, + calculatePages, + openPage, + }), + [calculatePages, pages, pageIndex, openPage], + ); + + return result; }; export default useNovelPages; diff --git a/src/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts index f4f0bb5828..547a27aea2 100644 --- a/src/hooks/persisted/novel/useNovelState.ts +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -11,7 +11,7 @@ import { fetchNovel } from '@services/plugin/fetch'; import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; import { StorageAccessFramework } from 'expo-file-system'; -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useEffect, useMemo } from 'react'; import useNovelPages from './useNovelPages'; const useNovelState = () => { @@ -22,7 +22,7 @@ const useNovelState = () => { ); } - const { novel, setNovel, path, pluginId } = novelState; + const { novel, setNovel, path, pluginId, loading, setLoading } = novelState; const { calculatePages } = useNovelPages(); const { switchNovelToLibrary } = useLibraryContext(); @@ -56,7 +56,7 @@ const useNovelState = () => { }); }, [novel, path, pluginId, setNovel, switchNovelToLibrary]); - const setCustomNovelCover = async () => { + const setCustomNovelCover = useCallback(async () => { if (!novel) { return; } @@ -67,7 +67,7 @@ const useNovelState = () => { cover: newCover, }); } - }; + }, [novel, setNovel]); const saveNovelCover = useCallback(async () => { if (!novel) { @@ -122,15 +122,42 @@ const useNovelState = () => { } }, [novel]); - return { - novel, - path, - pluginId, - getNovel, - followNovel, - setCustomNovelCover, - saveNovelCover, - }; + useEffect(() => { + if (novel) { + setLoading(false); + } else { + getNovel().finally(() => { + //? Sometimes loading state changes doesn't trigger rerender causing NovelScreen to be in endless loading state + setLoading(false); + // getNovel(); + }); + } + }, [getNovel, novel, setLoading]); + + const result = useMemo( + () => ({ + novel, + loading, + path, + pluginId, + getNovel, + followNovel, + setCustomNovelCover, + saveNovelCover, + }), + [ + loading, + novel, + path, + pluginId, + getNovel, + followNovel, + setCustomNovelCover, + saveNovelCover, + ], + ); + + return result; }; export default useNovelState; diff --git a/src/hooks/persisted/novel/useTrackedNovel.ts b/src/hooks/persisted/novel/useTrackedNovel.ts index addd403daa..7ba87384be 100644 --- a/src/hooks/persisted/novel/useTrackedNovel.ts +++ b/src/hooks/persisted/novel/useTrackedNovel.ts @@ -1,8 +1,8 @@ import { SearchResult, UserListEntry } from '@services/Trackers'; import { useCallback } from 'react'; import { useMMKVObject } from 'react-native-mmkv'; -import { TRACKED_NOVEL_PREFIX } from './useNovel'; import { TrackerMetadata, getTracker } from '../useTracker'; +import { TRACKED_NOVEL_PREFIX } from '@utils/constants/mmkv'; type TrackedNovel = SearchResult & UserListEntry; diff --git a/src/screens/novel/context/NovelSettingsContext.tsx b/src/screens/novel/context/NovelSettingsContext.tsx index bb9b5017db..0a3b4c81df 100644 --- a/src/screens/novel/context/NovelSettingsContext.tsx +++ b/src/screens/novel/context/NovelSettingsContext.tsx @@ -2,6 +2,7 @@ import { useAppSettings } from '@hooks/persisted'; import { NovelSettings } from '@hooks/persisted/novel/useNovel'; import { ReaderStackParamList } from '@navigators/types'; import { RouteProp } from '@react-navigation/native'; +import { NOVEL_SETTINSG_PREFIX } from '@utils/constants/mmkv'; import { createContext, useMemo } from 'react'; import { useMMKVObject } from 'react-native-mmkv'; From 2e90dc91321c53f60e7e84352e03db118ec2bbe6 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 22:05:34 +0200 Subject: [PATCH 06/18] added NovelLastReadContext --- src/hooks/persisted/index.ts | 6 +- src/hooks/persisted/novel/useNovelLastRead.ts | 24 ++++++ src/screens/novel/NovelProvider.tsx | 82 ++++++------------- .../novel/context/NovelLastReadContext.tsx | 52 ++++++++++++ 4 files changed, 106 insertions(+), 58 deletions(-) create mode 100644 src/hooks/persisted/novel/useNovelLastRead.ts create mode 100644 src/screens/novel/context/NovelLastReadContext.tsx diff --git a/src/hooks/persisted/index.ts b/src/hooks/persisted/index.ts index 14863e3a73..dcbe2ad728 100644 --- a/src/hooks/persisted/index.ts +++ b/src/hooks/persisted/index.ts @@ -11,7 +11,11 @@ export { } from './useSettings'; export { default as usePlugins } from './usePlugins'; export { getTracker, useTracker } from './useTracker'; -export { useNovel } from './novel/useNovel'; +export { default as useNovelChapters } from './novel/useNovelChapters'; +export { default as useNovelPages } from './novel/useNovelPages'; +export { default as useNovelSettings } from './novel/useNovelSettings'; +export { default as useNovelState } from './novel/useNovelState'; +export { default as useNovelLastRead } from './novel/useNovelLastRead'; export { useTrackedNovel } from './novel/useTrackedNovel'; export { default as useDownload } from './useDownload'; export { default as useUserAgent } from './useUserAgent'; diff --git a/src/hooks/persisted/novel/useNovelLastRead.ts b/src/hooks/persisted/novel/useNovelLastRead.ts new file mode 100644 index 0000000000..d8247e87b9 --- /dev/null +++ b/src/hooks/persisted/novel/useNovelLastRead.ts @@ -0,0 +1,24 @@ +import { NovelLastReadContext } from '@screens/novel/context/NovelLastReadContext'; +import { useContext, useMemo } from 'react'; + +const useNovelLastRead = () => { + const novelPage = useContext(NovelLastReadContext); + if (!novelPage) { + throw new Error( + 'useNovelState must be used within NovelLastReadContextProvider', + ); + } + const { lastRead, setLastRead } = novelPage; + + const result = useMemo( + () => ({ + lastRead, + setLastRead, + }), + [lastRead, setLastRead], + ); + + return result; +}; + +export default useNovelLastRead; diff --git a/src/screens/novel/NovelProvider.tsx b/src/screens/novel/NovelProvider.tsx index fef01948ce..93f3e7de7a 100644 --- a/src/screens/novel/NovelProvider.tsx +++ b/src/screens/novel/NovelProvider.tsx @@ -1,21 +1,13 @@ -import React, { createContext, useContext, useMemo, useRef } from 'react'; -import { useNovel } from '@hooks/persisted'; +import React from 'react'; import { RouteProp } from '@react-navigation/native'; import { ReaderStackParamList } from '@navigators/types'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useDeviceOrientation } from '@hooks/index'; +import { NovelStateContextProvider } from './context/NovelStateContext'; +import { NovelChaptersContextProvider } from './context/NovelChaptersContext'; +import { NovelPageContextProvider } from './context/NovelPageContext'; +import { NovelSettingsContextProvider } from './context/NovelSettingsContext'; +import { NovelLastReadContextProvider } from './context/NovelLastReadContext'; -type NovelContextType = ReturnType & { - navigationBarHeight: number; - statusBarHeight: number; - chapterTextCache: Map>; -}; - -const defaultValue = {} as NovelContextType; - -const NovelContext = createContext(defaultValue); - -export function NovelContextProvider({ +export function NovelProvider({ children, route, @@ -26,50 +18,26 @@ export function NovelContextProvider({ | RouteProp | RouteProp; }) { - const { path, pluginId } = + const RouteNovelParams = 'novel' in route.params ? route.params.novel : route.params; - const novelHookContent = useNovel( - 'id' in route.params ? route.params : path, - pluginId, - ); - - const { bottom, top } = useSafeAreaInsets(); - const orientation = useDeviceOrientation(); - const NavigationBarHeight = useRef(bottom); - const StatusBarHeight = useRef(top); - const chapterTextCache = useRef>>( - new Map(), - ); - - if (bottom < NavigationBarHeight.current && orientation === 'landscape') { - NavigationBarHeight.current = bottom; - } else if (bottom > NavigationBarHeight.current) { - NavigationBarHeight.current = bottom; - } - if (top > StatusBarHeight.current) { - StatusBarHeight.current = top; - } - const contextValue = useMemo( - () => ({ - ...novelHookContent, - navigationBarHeight: NavigationBarHeight.current, - statusBarHeight: StatusBarHeight.current, - chapterTextCache: chapterTextCache.current, - }), - [novelHookContent], - ); return ( - - {children} - + + + + + + {children} + + + + + ); } - -export const useNovelContext = () => { - const context = useContext(NovelContext); - if (!context) { - throw new Error('useNovelContext must be used within NovelContextProvider'); - } - return context; -}; diff --git a/src/screens/novel/context/NovelLastReadContext.tsx b/src/screens/novel/context/NovelLastReadContext.tsx new file mode 100644 index 0000000000..5294be83c5 --- /dev/null +++ b/src/screens/novel/context/NovelLastReadContext.tsx @@ -0,0 +1,52 @@ +import { ChapterInfo } from '@database/types'; +import { ReaderStackParamList } from '@navigators/types'; +import { RouteProp } from '@react-navigation/native'; +import { LAST_READ_PREFIX } from '@utils/constants/mmkv'; +import { createContext, useMemo } from 'react'; +import { useMMKVObject } from 'react-native-mmkv'; + +type Route = + | RouteProp['params'] + | RouteProp['params']['novel']; +type Path = Route['path']; +type PluginId = Route['pluginId']; + +interface ReadingProgressState { + lastRead: ChapterInfo | undefined; +} + +interface ReadingProgressActions { + setLastRead: (chapter: ChapterInfo | undefined) => void; +} + +export const NovelLastReadContext = createContext< + (ReadingProgressState & ReadingProgressActions) | null +>(null); + +export function NovelLastReadContextProvider({ + children, + path, + pluginId, +}: { + children: React.JSX.Element; + path: Path; + pluginId: PluginId; +}) { + const [lastRead, setLastRead] = useMMKVObject( + `${LAST_READ_PREFIX}_${pluginId}_${path}`, + ); + + const contextValue = useMemo( + () => ({ + lastRead, + setLastRead, + }), + [lastRead, setLastRead], + ); + + return ( + + {children} + + ); +} From f97de5d2bb99f0ae68203fccea988d13b4681db8 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 22:27:59 +0200 Subject: [PATCH 07/18] Added HeightsContext and ChapterCacheContext --- src/screens/novel/NovelProvider.tsx | 40 ++++++++------ src/screens/novel/context/HeightsContext.tsx | 53 +++++++++++++++++++ .../context/NovelChapterCacheContext.tsx | 40 ++++++++++++++ 3 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 src/screens/novel/context/HeightsContext.tsx create mode 100644 src/screens/novel/context/NovelChapterCacheContext.tsx diff --git a/src/screens/novel/NovelProvider.tsx b/src/screens/novel/NovelProvider.tsx index 93f3e7de7a..60efe4a637 100644 --- a/src/screens/novel/NovelProvider.tsx +++ b/src/screens/novel/NovelProvider.tsx @@ -6,6 +6,8 @@ import { NovelChaptersContextProvider } from './context/NovelChaptersContext'; import { NovelPageContextProvider } from './context/NovelPageContext'; import { NovelSettingsContextProvider } from './context/NovelSettingsContext'; import { NovelLastReadContextProvider } from './context/NovelLastReadContext'; +import { HeightContextProvider } from './context/HeightsContext'; +import { NovelChapterCacheContextProvider } from './context/NovelChapterCacheContext'; export function NovelProvider({ children, @@ -22,22 +24,26 @@ export function NovelProvider({ 'novel' in route.params ? route.params.novel : route.params; return ( - - - - - - {children} - - - - - + + + + + + + + {children} + + + + + + + ); } diff --git a/src/screens/novel/context/HeightsContext.tsx b/src/screens/novel/context/HeightsContext.tsx new file mode 100644 index 0000000000..832d913cb5 --- /dev/null +++ b/src/screens/novel/context/HeightsContext.tsx @@ -0,0 +1,53 @@ +import React, { createContext, useContext, useMemo, useRef } from 'react'; + +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useDeviceOrientation } from '@hooks/index'; + +type HeightContextType = { + navigationBarHeight: number; + statusBarHeight: number; +}; + +const HeightContext = createContext(null); + +export function HeightContextProvider({ + children, +}: { + children: React.JSX.Element; +}) { + const { bottom, top } = useSafeAreaInsets(); + const orientation = useDeviceOrientation(); + const NavigationBarHeight = useRef(bottom); + const StatusBarHeight = useRef(top); + + if (bottom < NavigationBarHeight.current && orientation === 'landscape') { + NavigationBarHeight.current = bottom; + } else if (bottom > NavigationBarHeight.current) { + NavigationBarHeight.current = bottom; + } + if (top > StatusBarHeight.current) { + StatusBarHeight.current = top; + } + const contextValue = useMemo( + () => ({ + navigationBarHeight: NavigationBarHeight.current, + statusBarHeight: StatusBarHeight.current, + }), + [], + ); + return ( + + {children} + + ); +} + +export const useHeightContext = () => { + const context = useContext(HeightContext); + if (!context) { + throw new Error( + 'useHeightContext must be used within HeightContextProvider', + ); + } + return context; +}; diff --git a/src/screens/novel/context/NovelChapterCacheContext.tsx b/src/screens/novel/context/NovelChapterCacheContext.tsx new file mode 100644 index 0000000000..4e6facdbf2 --- /dev/null +++ b/src/screens/novel/context/NovelChapterCacheContext.tsx @@ -0,0 +1,40 @@ +import React, { createContext, useContext, useMemo, useRef } from 'react'; + +type NovelChapterCacheContextType = { + chapterTextCache: Map>; +}; + +const NovelChapterCacheContext = + createContext(null); + +export function NovelChapterCacheContextProvider({ + children, +}: { + children: React.JSX.Element; +}) { + const chapterTextCache = useRef>>( + new Map(), + ); + + const contextValue = useMemo( + () => ({ + chapterTextCache: chapterTextCache.current, + }), + [], + ); + return ( + + {children} + + ); +} + +export const useNovelChapterCache = () => { + const context = useContext(NovelChapterCacheContext); + if (!context) { + throw new Error( + 'useNovelChapterCacheContext must be used within NovelChapterCacheContextProvider', + ); + } + return context; +}; From 338c78c6b0485acd862f8af3b2b246b917561b4e Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 22:28:39 +0200 Subject: [PATCH 08/18] replaced useNovel integration --- src/hooks/persisted/novel/useNovelChapters.ts | 64 +++++----- src/hooks/persisted/novel/useNovelPages.ts | 6 +- src/hooks/persisted/novel/useNovelState.ts | 37 ++++-- src/navigators/ReaderStack.tsx | 6 +- src/navigators/types/index.ts | 9 +- src/screens/novel/NovelScreen.tsx | 119 ++++++------------ .../components/DownloadCustomChapterModal.tsx | 14 +-- .../novel/components/EditInfoModal.tsx | 23 ++-- .../novel/components/EpubIconButton.tsx | 11 +- .../novel/components/Info/NovelInfoHeader.tsx | 47 +++---- .../novel/components/JumpToChapterModal.tsx | 11 +- src/screens/novel/components/NovelAppbar.tsx | 63 +++++++--- .../novel/components/NovelScreenList.tsx | 55 +++----- .../novel/context/NovelChaptersContext.tsx | 2 +- .../novel/context/NovelSettingsContext.tsx | 7 +- .../novel/context/NovelStateContext.tsx | 66 ++++++---- .../reader/components/ChapterDrawer/index.tsx | 15 ++- .../reader/components/ReaderAppbar.tsx | 4 +- .../reader/components/ReaderFooter.tsx | 4 +- src/screens/reader/hooks/useChapter.ts | 13 +- .../settings/SettingsAdvancedScreen.tsx | 2 +- src/services/migrate/migrateNovel.ts | 6 +- 22 files changed, 301 insertions(+), 283 deletions(-) diff --git a/src/hooks/persisted/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts index dc8418f70f..1054e7cfc1 100644 --- a/src/hooks/persisted/novel/useNovelChapters.ts +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -37,18 +37,19 @@ const useNovelChapters = () => { batchInformation, setChapters, _setChapters, + updateChapter, extendChapters, mutateChapters, setFetching, setBatchInformation, } = NovelChapters; - const { novel, path, pluginId } = useNovelState(); - const { pages, pageIndex } = useNovelPages(); + const { novel, path, pluginId, loading } = useNovelState(); + const { pages, pageIndex, page } = useNovelPages(); const { novelSettings } = useNovelSettings(); const currentPage = pages[pageIndex]; const getChapters = useCallback(async () => { - if (novel && currentPage) { + if (!loading) { let newChapters: ChapterInfo[] = []; const config = [ @@ -90,10 +91,11 @@ const useNovelChapters = () => { setChapters(newChapters); } }, [ - novel, - currentPage, + loading, + novel.id, novelSettings.sort, novelSettings.filter, + currentPage, setBatchInformation, setChapters, _setChapters, @@ -102,9 +104,8 @@ const useNovelChapters = () => { ]); const getNextChapterBatch = useCallback(() => { - const page = pages[pageIndex]; const nextBatch = batchInformation.batch + 1; - if (novel && page && nextBatch <= batchInformation.total) { + if (!loading && page && nextBatch <= batchInformation.total) { let newChapters: ChapterInfo[] = []; try { @@ -124,13 +125,13 @@ const useNovelChapters = () => { } }, [ batchInformation, - extendChapters, - novel, - novelSettings.filter, - pageIndex, - pages, + loading, + page, setBatchInformation, + extendChapters, + novel.id, novelSettings.sort, + novelSettings.filter, ]); // #region Mark chapters @@ -157,7 +158,7 @@ const useNovelChapters = () => { const markPreviouschaptersRead = useCallback( (chapterId: number) => { - if (novel) { + if (!loading) { _markPreviuschaptersRead(chapterId, novel.id); mutateChapters(chs => chs.map(chapter => @@ -166,7 +167,7 @@ const useNovelChapters = () => { ); } }, - [mutateChapters, novel], + [loading, mutateChapters, novel.id], ); const markChapterRead = useCallback( @@ -229,7 +230,7 @@ const useNovelChapters = () => { const markPreviousChaptersUnread = useCallback( (chapterId: number) => { - if (novel) { + if (!loading) { _markPreviousChaptersUnread(chapterId, novel.id); mutateChapters(chs => chs.map(chapter => @@ -238,7 +239,7 @@ const useNovelChapters = () => { ); } }, - [mutateChapters, novel], + [loading, mutateChapters, novel.id], ); const markChaptersUnread = useCallback( @@ -266,7 +267,7 @@ const useNovelChapters = () => { const deleteChapter = useCallback( (_chapter: ChapterInfo) => { - if (novel) { + if (!loading) { _deleteChapter(novel.pluginId, novel.id, _chapter.id).then(() => { mutateChapters(chs => chs.map(chapter => { @@ -283,12 +284,12 @@ const useNovelChapters = () => { }); } }, - [mutateChapters, novel], + [loading, mutateChapters, novel.id, novel.pluginId], ); const deleteChapters = useCallback( (_chaters: ChapterInfo[]) => { - if (novel) { + if (!loading) { _deleteChapters(novel.pluginId, novel.id, _chaters).then(() => { showToast( getString('updatesScreen.deletedChapters', { @@ -309,11 +310,11 @@ const useNovelChapters = () => { }); } }, - [novel, mutateChapters], + [loading, novel.pluginId, novel.id, mutateChapters], ); const refreshChapters = useCallback(() => { - if (novel?.id && !fetching) { + if (!loading && !fetching) { _getPageChapters( novel.id, novelSettings.sort, @@ -324,8 +325,9 @@ const useNovelChapters = () => { }); } }, [ - novel?.id, + loading, fetching, + novel.id, novelSettings.sort, novelSettings.filter, currentPage, @@ -351,7 +353,10 @@ const useNovelChapters = () => { const result = useMemo( () => ({ chapters, + fetching, + batchInformation, getChapters, + updateChapter, getNextChapterBatch, bookmarkChapters, markPreviouschaptersRead, @@ -365,19 +370,22 @@ const useNovelChapters = () => { refreshChapters, }), [ - bookmarkChapters, chapters, - deleteChapter, - deleteChapters, + fetching, + batchInformation, getChapters, + updateChapter, getNextChapterBatch, + bookmarkChapters, + markPreviouschaptersRead, markChapterRead, + updateChapterProgress, markChaptersRead, - markChaptersUnread, markPreviousChaptersUnread, - markPreviouschaptersRead, + markChaptersUnread, + deleteChapter, + deleteChapters, refreshChapters, - updateChapterProgress, ], ); diff --git a/src/hooks/persisted/novel/useNovelPages.ts b/src/hooks/persisted/novel/useNovelPages.ts index 00b13333fe..f8c7c71415 100644 --- a/src/hooks/persisted/novel/useNovelPages.ts +++ b/src/hooks/persisted/novel/useNovelPages.ts @@ -39,14 +39,18 @@ const useNovelPages = () => { [setPageIndex], ); + const page = pages[pageIndex]; + const result = useMemo( () => ({ + page, pages, pageIndex, + setPageIndex, calculatePages, openPage, }), - [calculatePages, pages, pageIndex, openPage], + [page, pages, pageIndex, setPageIndex, calculatePages, openPage], ); return result; diff --git a/src/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts index 547a27aea2..7555dc35a5 100644 --- a/src/hooks/persisted/novel/useNovelState.ts +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -6,15 +6,32 @@ import { } from '@database/queries/NovelQueries'; import { downloadFile } from '@plugins/helpers/fetch'; import FileManager from '@specs/NativeFile'; -import { NovelStateContext } from '@screens/novel/context/NovelStateContext'; +import { + NovelStateContext, + RouteNovel, +} from '@screens/novel/context/NovelStateContext'; import { fetchNovel } from '@services/plugin/fetch'; import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; import { StorageAccessFramework } from 'expo-file-system'; import { useCallback, useContext, useEffect, useMemo } from 'react'; import useNovelPages from './useNovelPages'; +import { NovelInfo } from '@database/types'; + +type NovelState = { + path: string; + pluginId: string; + setNovel: (novel: NovelInfo) => void; + followNovel: () => void; + setCustomNovelCover: () => Promise; + saveNovelCover: () => Promise; + getNovel: () => void; +} & ( + | { novel: NovelInfo; loading: false } + | { novel: RouteNovel; loading: true } +); -const useNovelState = () => { +const useNovelState = (): NovelState => { const novelState = useContext(NovelStateContext); if (!novelState) { throw new Error( @@ -47,17 +64,17 @@ const useNovelState = () => { const followNovel = useCallback(() => { switchNovelToLibrary(path, pluginId).then(() => { - if (novel) { + if (!loading) { setNovel({ ...novel, inLibrary: !novel?.inLibrary, }); } }); - }, [novel, path, pluginId, setNovel, switchNovelToLibrary]); + }, [loading, novel, path, pluginId, setNovel, switchNovelToLibrary]); const setCustomNovelCover = useCallback(async () => { - if (!novel) { + if (loading) { return; } const newCover = await pickCustomNovelCover(novel); @@ -67,7 +84,7 @@ const useNovelState = () => { cover: newCover, }); } - }, [novel, setNovel]); + }, [loading, novel, setNovel]); const saveNovelCover = useCallback(async () => { if (!novel) { @@ -127,9 +144,7 @@ const useNovelState = () => { setLoading(false); } else { getNovel().finally(() => { - //? Sometimes loading state changes doesn't trigger rerender causing NovelScreen to be in endless loading state setLoading(false); - // getNovel(); }); } }, [getNovel, novel, setLoading]); @@ -141,23 +156,25 @@ const useNovelState = () => { path, pluginId, getNovel, + setNovel, followNovel, setCustomNovelCover, saveNovelCover, }), [ - loading, novel, + loading, path, pluginId, getNovel, + setNovel, followNovel, setCustomNovelCover, saveNovelCover, ], ); - return result; + return result as NovelState; }; export default useNovelState; diff --git a/src/navigators/ReaderStack.tsx b/src/navigators/ReaderStack.tsx index 5c7114640e..6bf687cea7 100644 --- a/src/navigators/ReaderStack.tsx +++ b/src/navigators/ReaderStack.tsx @@ -11,7 +11,7 @@ import { NovelScreenProps, ReaderStackParamList, } from './types'; -import { NovelContextProvider } from '@screens/novel/NovelProvider'; +import { NovelProvider } from '@screens/novel/NovelProvider'; const Stack = createNativeStackNavigator(); @@ -22,7 +22,7 @@ const ReaderStack = ({ route }) => { const params = useRef(route?.params); return ( - { - + ); }; diff --git a/src/navigators/types/index.ts b/src/navigators/types/index.ts index 1b7932ccd9..1654b4c75e 100644 --- a/src/navigators/types/index.ts +++ b/src/navigators/types/index.ts @@ -94,13 +94,8 @@ export type ChapterScreenProps = StackScreenProps< >; export type ReaderStackParamList = { Novel: - | { - name: string; - path: string; - pluginId: string; - cover?: string; - isLocal?: boolean; - } + | (Pick & + Partial>) | NovelInfo; Chapter: { novel: NovelInfo; diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index 12819a0e5f..be89174c24 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -1,5 +1,5 @@ import React, { Suspense, useCallback, useMemo, useRef, useState } from 'react'; -import { StyleSheet, View, StatusBar, Text, Share } from 'react-native'; +import { StyleSheet, View, StatusBar, Text } from 'react-native'; import { Drawer } from 'react-native-drawer-layout'; import Animated, { SlideInUp, @@ -12,7 +12,6 @@ import { useDownload, useTheme } from '@hooks/persisted'; import JumpToChapterModal from './components/JumpToChapterModal'; import { Actionbar } from '../../components/Actionbar/Actionbar'; import EditInfoModal from './components/EditInfoModal'; -import { pickCustomNovelCover } from '../../database/queries/NovelQueries'; import DownloadCustomChapterModal from './components/DownloadCustomChapterModal'; import { useBoolean } from '@hooks'; import NovelScreenLoading from './components/LoadingAnimation/NovelScreenLoading'; @@ -20,28 +19,25 @@ import { NovelScreenProps } from '@navigators/types'; import { ChapterInfo } from '@database/types'; import { getString } from '@strings/translations'; import NovelDrawer from './components/NovelDrawer'; -import { isNumber, noop } from 'lodash-es'; +import { noop } from 'lodash-es'; import NovelAppbar from './components/NovelAppbar'; -import { resolveUrl } from '@services/plugin/fetch'; import { updateChapterProgressByIds } from '@database/queries/ChapterQueries'; import { MaterialDesignIconName } from '@type/icon'; import NovelScreenList from './components/NovelScreenList'; import { ThemeColors } from '@theme/types'; import { SafeAreaView } from '@components'; -import { useNovelContext } from './NovelProvider'; import { FlashList } from '@shopify/flash-list'; +import useNovelState from '@hooks/persisted/novel/useNovelState'; +import useNovelChapters from '@hooks/persisted/novel/useNovelChapters'; +import useNovelPages from '@hooks/persisted/novel/useNovelPages'; const Novel = ({ route, navigation }: NovelScreenProps) => { + const { novel, loading } = useNovelState(); const { - pageIndex, - pages, - novel, chapters, fetching, batchInformation, getNextChapterBatch, - openPage, - setNovel, bookmarkChapters, markChaptersRead, markChaptersUnread, @@ -49,7 +45,8 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { markPreviousChaptersUnread, refreshChapters, deleteChapters, - } = useNovelContext(); + } = useNovelChapters(); + const { pageIndex, pages, openPage } = useNovelPages(); const theme = useTheme(); const { downloadChapters } = useDownload(); @@ -77,36 +74,6 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { // useFocusEffect(refreshChapters); - const downloadChs = useCallback( - (amount: number | 'all' | 'unread') => { - if (!novel) { - return; - } - let filtered = chapters.filter(chapter => !chapter.isDownloaded); - if (amount === 'unread') { - filtered = filtered.filter(chapter => chapter.unread); - } - if (isNumber(amount)) { - filtered = filtered.slice(0, amount); - } - if (filtered) { - downloadChapters(novel, filtered); - } - }, - [chapters, downloadChapters, novel], - ); - const deleteChs = useCallback(() => { - deleteChapters(chapters.filter(c => c.isDownloaded)); - }, [chapters, deleteChapters]); - const shareNovel = () => { - if (!novel) { - return; - } - Share.share({ - message: resolveUrl(novel.pluginId, novel.path, true), - }); - }; - const [jumpToChapterModal, showJumpToChapterModal] = useState(false); const { value: dlChapterModalVisible, @@ -117,7 +84,11 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { const actions = useMemo(() => { const list: { icon: MaterialDesignIconName; onPress: () => void }[] = []; - if (!novel?.isLocal && selected.some(obj => !obj.isDownloaded)) { + if ( + !loading && + !novel?.isLocal && + selected.some(obj => !obj.isDownloaded) + ) { list.push({ icon: 'download-outline', onPress: () => { @@ -198,6 +169,7 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { bookmarkChapters, deleteChapters, downloadChapters, + loading, markChaptersRead, markChaptersUnread, markPreviousChaptersUnread, @@ -207,19 +179,33 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { selected, ]); - const setCustomNovelCover = async () => { - if (!novel) { - return; + const styles = useMemo(() => createStyles(theme), [theme]); + + const renderDrawerContent = useCallback(() => { + if (loading) { + return ; } - const newCover = await pickCustomNovelCover(novel); - if (newCover) { - setNovel({ - ...novel, - cover: newCover, - }); + if ((novel?.totalPages ?? 0) > 1 || pages.length > 1) { + return ( + + ); } - }; - const styles = useMemo(() => createStyles(theme), [theme]); + return null; + }, [ + loading, + novel?.totalPages, + pages, + theme, + pageIndex, + openPage, + closeDrawer, + ]); return ( { hideStatusBarOnOpen={true} swipeMinVelocity={1000} drawerStyle={styles.drawer} - renderDrawerContent={() => - (novel?.totalPages ?? 0) > 1 || pages.length > 1 ? ( - - ) : ( - <> - ) - } + renderDrawerContent={renderDrawerContent} > {selected.length === 0 ? ( { showJumpToChapterModal(false)} - chapters={chapters} - novel={novel} chapterListRef={chapterListRef} navigation={navigation} /> showEditInfoModal(false)} - novel={novel} - setNovel={setNovel} theme={theme} /> diff --git a/src/screens/novel/components/DownloadCustomChapterModal.tsx b/src/screens/novel/components/DownloadCustomChapterModal.tsx index 1848952f51..05cfac9932 100644 --- a/src/screens/novel/components/DownloadCustomChapterModal.tsx +++ b/src/screens/novel/components/DownloadCustomChapterModal.tsx @@ -2,28 +2,27 @@ import React, { useState } from 'react'; import { StyleSheet, Text, View, TextInput } from 'react-native'; import { Button, IconButton, Portal } from 'react-native-paper'; -import { ThemeColors } from '@theme/types'; import { ChapterInfo, NovelInfo } from '@database/types'; import { getString } from '@strings/translations'; import { Modal } from '@components'; +import { useTheme } from '@hooks/persisted'; +import { useNovelChapters, useNovelState } from '@hooks/persisted/index'; interface DownloadCustomChapterModalProps { - theme: ThemeColors; hideModal: () => void; modalVisible: boolean; - novel: NovelInfo; - chapters: ChapterInfo[]; downloadChapters: (novel: NovelInfo, chapters: ChapterInfo[]) => void; } const DownloadCustomChapterModal = ({ - theme, hideModal, modalVisible, - novel, - chapters, downloadChapters, }: DownloadCustomChapterModalProps) => { + const theme = useTheme(); + const { novel, loading } = useNovelState(); + const { chapters } = useNovelChapters(); + const [text, setText] = useState(0); const onDismiss = () => { @@ -33,6 +32,7 @@ const DownloadCustomChapterModal = ({ const onSubmit = () => { hideModal(); + if (loading) return; downloadChapters( novel, chapters diff --git a/src/screens/novel/components/EditInfoModal.tsx b/src/screens/novel/components/EditInfoModal.tsx index 2397548949..6a0657ecae 100644 --- a/src/screens/novel/components/EditInfoModal.tsx +++ b/src/screens/novel/components/EditInfoModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FlatList, Pressable, @@ -15,16 +15,15 @@ import { updateNovelInfo } from '@database/queries/NovelQueries'; import { getString } from '@strings/translations'; import { Button, Modal } from '@components'; import { ThemeColors } from '@theme/types'; -import { NovelInfo } from '@database/types'; import { NovelStatus } from '@plugins/types'; import { translateNovelStatus } from '@utils/translateEnum'; +import useNovelState from '@hooks/persisted/novel/useNovelState'; +import { NovelInfo } from '@database/types'; interface EditInfoModalProps { theme: ThemeColors; hideModal: () => void; modalVisible: boolean; - novel: NovelInfo; - setNovel: (novel: NovelInfo | undefined) => void; } // --- Dynamic style helpers --- @@ -49,15 +48,21 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, - novel, - setNovel, }: EditInfoModalProps) => { + const { novel: _novel, setNovel, loading } = useNovelState(); + const novel = _novel as NovelInfo; const initialNovelInfo = { ...novel }; - const [novelInfo, setNovelInfo] = useState(novel); + const [novelInfo, setNovelInfo] = useState(novel); const [newGenre, setNewGenre] = useState(''); + useEffect(() => { + if (loading) return; + setNovelInfo(novel); + }, [loading, novel]); + const removeTag = (t: string) => { + if (!novelInfo || loading) return; setNovelInfo({ ...novel, genres: novelInfo.genres @@ -69,6 +74,8 @@ const EditInfoModal = ({ const status = Object.values(NovelStatus); + if (!novelInfo || loading) return null; + return ( @@ -120,7 +127,7 @@ const EditInfoModal = ({ style={styles.inputWrapper} /> void; @@ -28,10 +27,10 @@ interface EpubIconButtonProps { const EpubIconButton: React.FC = ({ theme, - novel, - chapters, anchor: Anchor, }) => { + const { novel, loading } = useNovelState(); + const { chapters } = useNovelChapters(); const { value: isVisible, setTrue: showModal, @@ -127,7 +126,7 @@ const EpubIconButton: React.FC = ({ ); const createEpub = async (uri: string) => { - if (!novel) { + if (!novel || loading) { return; } const epub = new EpubBuilder( diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 5a3fd02c90..47f66a677e 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -23,12 +23,17 @@ import NovelSummary from '../NovelSummary/NovelSummary'; import NovelScreenButtonGroup from '../NovelScreenButtonGroup/NovelScreenButtonGroup'; import { getString } from '@strings/translations'; import { filterColor } from '@theme/colors'; -import { ChapterInfo, NovelInfo as NovelData } from '@database/types'; -import { ThemeColors } from '@theme/types'; +import { ChapterInfo } from '@database/types'; import { GlobalSearchScreenProps } from '@navigators/types'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { UseBooleanReturnType } from '@hooks'; -import { useAppSettings } from '@hooks/persisted'; +import { + useAppSettings, + useTheme, + useNovelChapters, + useNovelPages, + useNovelState, +} from '@hooks/persisted'; import { NovelStatus, PluginItem } from '@plugins/types'; import { translateNovelStatus } from '@utils/translateEnum'; import { getMMKVObject } from '@utils/mmkv/mmkv'; @@ -38,25 +43,16 @@ import { NovelMetaSkeleton, VerticalBarSkeleton, } from '@components/Skeleton/Skeleton'; -import { useNovelContext } from '@screens/novel/NovelProvider'; interface NovelInfoHeaderProps { - chapters: ChapterInfo[]; deleteDownloadsSnackbar: UseBooleanReturnType; - fetching: boolean; filter: string; - isLoading: boolean; lastRead?: ChapterInfo; navigateToChapter: (chapter: ChapterInfo) => void; navigation: GlobalSearchScreenProps['navigation']; - novel: NovelData | (Omit & { id: 'NO_ID' }); novelBottomSheetRef: React.RefObject; onRefreshPage: (page: string) => void; openDrawer: () => void; - page?: string; - setCustomNovelCover: () => Promise; - saveNovelCover: () => Promise; - theme: ThemeColors; totalChapters?: number; trackerSheetRef: React.RefObject; } @@ -72,27 +68,32 @@ const getStatusIcon = (status?: string) => { }; const NovelInfoHeader = ({ - chapters, deleteDownloadsSnackbar, - fetching, + filter, - isLoading = false, + lastRead, navigateToChapter, navigation, - novel, + novelBottomSheetRef, onRefreshPage, openDrawer, - page, - setCustomNovelCover, - saveNovelCover, - theme, + totalChapters, trackerSheetRef, }: NovelInfoHeaderProps) => { const { hideBackdrop = false } = useAppSettings(); - const { followNovel } = useNovelContext(); + const { + novel, + loading: isLoading, + followNovel, + saveNovelCover, + setCustomNovelCover, + } = useNovelState(); + const { chapters, fetching } = useNovelChapters(); + const { page } = useNovelPages(); + const theme = useTheme(); const pluginName = useMemo( () => @@ -205,7 +206,7 @@ const NovelInfoHeader = ({ handleTrackerSheet={() => trackerSheetRef.current?.present()} theme={theme} /> - {isLoading && (!novel.genres || !novel.summary) ? ( + {isLoading ? ( ) : ( <> @@ -224,7 +225,7 @@ const NovelInfoHeader = ({ chapters={chapters} lastRead={lastRead} /> - {isLoading && (!novel.genres || !novel.summary) ? ( + {isLoading ? ( ) : ( void; modalVisible: boolean; - chapters: ChapterInfo[]; navigation: NovelScreenProps['navigation']; - novel: NovelInfo; chapterListRef: React.RefObject | null>; } const JumpToChapterModal = ({ hideModal, modalVisible, - chapters, navigation, - novel, chapterListRef, }: JumpToChapterModalProps) => { + const { novel, loading } = useNovelState(); + const { chapters } = useNovelChapters(); const minNumber = Math.min(...chapters.map(c => c.chapterNumber || -1)); const maxNumber = Math.max(...chapters.map(c => c.chapterNumber || -1)); const theme = useTheme(); @@ -55,6 +55,7 @@ const JumpToChapterModal = ({ }; const navigateToChapter = (chap: ChapterInfo) => { onDismiss(); + if (loading) return; navigation.navigate('Chapter', { novel: novel, chapter: chap, diff --git a/src/screens/novel/components/NovelAppbar.tsx b/src/screens/novel/components/NovelAppbar.tsx index ebb7e72f7e..f9dfc33d07 100644 --- a/src/screens/novel/components/NovelAppbar.tsx +++ b/src/screens/novel/components/NovelAppbar.tsx @@ -12,9 +12,13 @@ import Animated, { useAnimatedStyle, } from 'react-native-reanimated'; import EpubIconButton from './EpubIconButton'; -import { ChapterInfo, NovelInfo } from '@database/types'; -import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; +import { Share, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { MaterialDesignIconName } from '@type/icon'; +import useNovelChapters from '@hooks/persisted/novel/useNovelChapters'; +import useNovelState from '@hooks/persisted/novel/useNovelState'; +import { useDownload } from '@hooks/persisted'; +import { isNumber } from 'lodash-es'; +import { resolveUrl } from '@services/plugin/fetch'; const Menu = React.memo( ({ @@ -56,34 +60,58 @@ const Menu = React.memo( ); const NovelAppbar = ({ - novel, - chapters, theme, isLocal, - downloadChapters, - deleteChapters, showEditInfoModal, downloadCustomChapterModal, - setCustomNovelCover, goBack, - shareNovel, showJumpToChapterModal, headerOpacity, }: { - novel: NovelInfo | undefined; - chapters: ChapterInfo[]; theme: ThemeColors; isLocal: boolean | undefined; - downloadChapters: (amount: number | 'all' | 'unread') => void; - deleteChapters: () => void; showEditInfoModal: React.Dispatch>; downloadCustomChapterModal: () => void; - setCustomNovelCover: () => Promise; goBack: () => void; - shareNovel: () => void; showJumpToChapterModal: (arg: boolean) => void; headerOpacity: SharedValue; }) => { + const { novel, loading, setCustomNovelCover } = useNovelState(); + const { chapters, deleteChapters: _deleteChapters } = useNovelChapters(); + const { downloadChapters: _downloadChapters } = useDownload(); + + const downloadChapters = useCallback( + (amount: number | 'all' | 'unread') => { + if (!novel) { + return; + } + let filtered = chapters.filter(chapter => !chapter.isDownloaded); + if (amount === 'unread') { + filtered = filtered.filter(chapter => chapter.unread); + } + if (isNumber(amount)) { + filtered = filtered.slice(0, amount); + } + if (filtered && !loading) { + _downloadChapters(novel, filtered); + } + }, + [novel, chapters, loading, _downloadChapters], + ); + + const deleteChapters = useCallback(() => { + _deleteChapters(chapters.filter(c => c.isDownloaded)); + }, [chapters, _deleteChapters]); + + const shareNovel = () => { + if (!novel) { + return; + } + Share.share({ + message: resolveUrl(novel.pluginId, novel.path, true), + }); + }; + const headerOpacityStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( headerOpacity.value, @@ -165,12 +193,7 @@ const NovelAppbar = ({ - + ; @@ -64,23 +67,12 @@ const NovelScreenList = ({ setSelected, getNextChapterBatch, }: NovelScreenListProps) => { - const { - chapters, - deleteChapter, - fetching, - getNovel, - lastRead, - loading, - novelSettings, - pages, - setNovel, - sortAndFilterChapters, - setShowChapterTitles, - updateChapter, - novel: fetchedNovel, - batchInformation, - pageIndex, - } = useNovelContext(); + const { getNovel, novel: fetchedNovel, loading } = useNovelState(); + const { chapters, deleteChapter, fetching, batchInformation, updateChapter } = + useNovelChapters(); + const { novelSettings, sortAndFilterChapters, setShowChapterTitles } = + useNovelSettings(); + const { lastRead } = useNovelLastRead(); const { pluginId } = routeBaseNovel; @@ -352,43 +344,26 @@ const NovelScreenList = ({ // Memoize the header component props const renderHeader = useMemo(() => { const props = { - chapters, deleteDownloadsSnackbar, - fetching, filter, - isLoading: loading, lastRead, navigateToChapter, navigation, - novel, novelBottomSheetRef, onRefreshPage, openDrawer, - page: pages.length > 1 ? pages[pageIndex] : undefined, - setCustomNovelCover, - saveNovelCover, - theme, totalChapters: batchInformation.totalChapters, trackerSheetRef, }; return ; }, [ - chapters, deleteDownloadsSnackbar, - fetching, filter, - loading, lastRead, navigateToChapter, navigation, - novel, onRefreshPage, openDrawer, - pages, - pageIndex, - setCustomNovelCover, - saveNovelCover, - theme, batchInformation.totalChapters, ]); diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx index cfd9d09b41..ab33d222ff 100644 --- a/src/screens/novel/context/NovelChaptersContext.tsx +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -30,7 +30,7 @@ export const NovelChaptersContext = createContext< (ChapterState & ChapterActions) | null >(null); -export function NovelStateContextProvider({ +export function NovelChaptersContextProvider({ children, }: { children: React.JSX.Element; diff --git a/src/screens/novel/context/NovelSettingsContext.tsx b/src/screens/novel/context/NovelSettingsContext.tsx index 0a3b4c81df..c16c668174 100644 --- a/src/screens/novel/context/NovelSettingsContext.tsx +++ b/src/screens/novel/context/NovelSettingsContext.tsx @@ -1,5 +1,4 @@ import { useAppSettings } from '@hooks/persisted'; -import { NovelSettings } from '@hooks/persisted/novel/useNovel'; import { ReaderStackParamList } from '@navigators/types'; import { RouteProp } from '@react-navigation/native'; import { NOVEL_SETTINSG_PREFIX } from '@utils/constants/mmkv'; @@ -12,6 +11,12 @@ type Route = type Path = Route['path']; type PluginId = Route['pluginId']; +export interface NovelSettings { + sort?: string; + filter?: string; + showChapterTitles?: boolean; +} + interface SettingsState { novelSettings: NovelSettings; } diff --git a/src/screens/novel/context/NovelStateContext.tsx b/src/screens/novel/context/NovelStateContext.tsx index d6e67064f8..ac13e4bd70 100644 --- a/src/screens/novel/context/NovelStateContext.tsx +++ b/src/screens/novel/context/NovelStateContext.tsx @@ -3,15 +3,25 @@ import { ReaderStackParamList } from '@navigators/types'; import { RouteProp } from '@react-navigation/native'; import { createContext, useMemo, useState } from 'react'; -type Route = - | RouteProp['params'] - | RouteProp['params']['novel']; -type Path = Route['path']; -type PluginId = Route['pluginId']; - +type Route = RouteProp; +type Params = Route['params']; +type Path = Params['path']; +type PluginId = Params['pluginId']; +export type RouteNovel = Params & { + id: 'NO_ID'; + inLibrary: boolean; + isLocal: boolean; + totalPages: number; +}; +interface NovelStateLoading { + novel: RouteNovel; + loading: true; + path: Path; + pluginId: PluginId; +} interface NovelState { - novel: NovelInfo | undefined; - loading: boolean; // for novel loading only + novel: NovelInfo; + loading: false; path: Path; pluginId: PluginId; } @@ -22,31 +32,41 @@ interface NovelActions { } export const NovelStateContext = createContext< - (NovelState & NovelActions) | null + (NovelState & NovelActions) | (NovelStateLoading & NovelActions) | null >(null); export function NovelStateContextProvider({ children, - path, - pluginId, + novelParams, }: { children: React.JSX.Element; - path: Path; - pluginId: PluginId; + novelParams: Route['params']; }) { - const [novel, setNovel] = useState(undefined); + const routeNovel: RouteNovel = useMemo( + () => ({ + inLibrary: false, + isLocal: false, + totalPages: 0, + ...novelParams, + id: 'NO_ID', + }), + [novelParams], + ); + + const [novel, setNovel] = useState(routeNovel); const [loading, setLoading] = useState(true); const contextValue = useMemo( - () => ({ - novel, - loading, - path, - pluginId, - setNovel, - setLoading, - }), - [loading, novel, path, pluginId], + () => + ({ + novel, + loading, + path: novelParams.path, + pluginId: novelParams.pluginId, + setNovel, + setLoading, + } as (NovelState & NovelActions) | (NovelStateLoading & NovelActions)), + [loading, novel, novelParams.path, novelParams.pluginId], ); return ( diff --git a/src/screens/reader/components/ChapterDrawer/index.tsx b/src/screens/reader/components/ChapterDrawer/index.tsx index 0e995eb4f7..42fe76b041 100644 --- a/src/screens/reader/components/ChapterDrawer/index.tsx +++ b/src/screens/reader/components/ChapterDrawer/index.tsx @@ -7,14 +7,18 @@ import React, { } from 'react'; import { StyleSheet, View } from 'react-native'; import { Text } from 'react-native-paper'; -import { useAppSettings, useTheme } from '@hooks/persisted'; +import { + useNovelChapters, + useNovelPages, + useNovelSettings, + useTheme, +} from '@hooks/persisted'; import { Button, LoadingScreenV2 } from '@components/index'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; import { getString } from '@strings/translations'; import { ThemeColors } from '@theme/types'; import renderListChapter from './RenderListChapter'; import { useChapterContext } from '@screens/reader/ChapterContext'; -import { useNovelContext } from '@screens/novel/NovelProvider'; import { FlashList, ViewToken } from '@shopify/flash-list'; import { ChapterInfo } from '@database/types'; @@ -30,15 +34,16 @@ type ButtonsProperties = { const ChapterDrawer = () => { const { chapter, getChapter, setLoading } = useChapterContext(); - const { chapters, novelSettings, pages, setPageIndex } = useNovelContext(); + const { chapters } = useNovelChapters(); + const { pages, setPageIndex } = useNovelPages(); + const { novelSettings } = useNovelSettings(); const theme = useTheme(); const insets = useSafeAreaInsets(); - const { defaultChapterSort } = useAppSettings(); const listRef = useRef | null>(null); const styles = createStylesheet(theme, insets); - const { sort = defaultChapterSort } = novelSettings; + const { sort } = novelSettings; const listAscending = sort === 'ORDER BY position ASC'; const defaultButtonLayout: ButtonsProperties = useMemo( diff --git a/src/screens/reader/components/ReaderAppbar.tsx b/src/screens/reader/components/ReaderAppbar.tsx index e2c3a04c98..a92a438f75 100644 --- a/src/screens/reader/components/ReaderAppbar.tsx +++ b/src/screens/reader/components/ReaderAppbar.tsx @@ -12,7 +12,7 @@ import Animated, { import { ThemeColors } from '@theme/types'; import { bookmarkChapter } from '@database/queries/ChapterQueries'; import { useChapterContext } from '../ChapterContext'; -import { useNovelContext } from '@screens/novel/NovelProvider'; +import { useHeightContext } from '@screens/novel/context/HeightsContext'; interface ReaderAppbarProps { theme: ThemeColors; @@ -30,7 +30,7 @@ const ReaderAppbar = ({ setBookmarked, }: ReaderAppbarProps) => { const { chapter, novel } = useChapterContext(); - const { statusBarHeight } = useNovelContext(); + const { statusBarHeight } = useHeightContext(); const entering = () => { 'worklet'; diff --git a/src/screens/reader/components/ReaderFooter.tsx b/src/screens/reader/components/ReaderFooter.tsx index 295634baa8..b22bc3a235 100644 --- a/src/screens/reader/components/ReaderFooter.tsx +++ b/src/screens/reader/components/ReaderFooter.tsx @@ -11,8 +11,8 @@ import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/typ import { ChapterScreenProps } from '@navigators/types'; import { useChapterContext } from '../ChapterContext'; import { SCREEN_HEIGHT } from '@gorhom/bottom-sheet'; -import { useNovelContext } from '@screens/novel/NovelProvider'; import { useTheme } from '@hooks/persisted'; +import { useHeightContext } from '@screens/novel/context/HeightsContext'; interface ChapterFooterProps { readerSheetRef: React.RefObject; @@ -37,7 +37,7 @@ const ChapterFooter = ({ borderless: true, radius: 50, }; - const { navigationBarHeight } = useNovelContext(); + const { navigationBarHeight } = useHeightContext(); const entering = () => { 'worklet'; diff --git a/src/screens/reader/hooks/useChapter.ts b/src/screens/reader/hooks/useChapter.ts index 611fda9b9b..154095e7ac 100644 --- a/src/screens/reader/hooks/useChapter.ts +++ b/src/screens/reader/hooks/useChapter.ts @@ -8,6 +8,8 @@ import { ChapterInfo, NovelInfo } from '@database/types'; import { useChapterGeneralSettings, useLibrarySettings, + useNovelChapters, + useNovelLastRead, useTrackedNovel, useTracker, } from '@hooks/persisted'; @@ -32,7 +34,7 @@ import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; import NativeVolumeButtonListener from '@specs/NativeVolumeButtonListener'; import NativeFile from '@specs/NativeFile'; -import { useNovelContext } from '@screens/novel/NovelProvider'; +import { useNovelChapterCache } from '@screens/novel/context/NovelChapterCacheContext'; const emmiter = new NativeEventEmitter(NativeVolumeButtonListener); @@ -41,12 +43,9 @@ export default function useChapter( initialChapter: ChapterInfo, novel: NovelInfo, ) { - const { - setLastRead, - markChapterRead, - updateChapterProgress, - chapterTextCache, - } = useNovelContext(); + const { markChapterRead, updateChapterProgress } = useNovelChapters(); + const { setLastRead } = useNovelLastRead(); + const { chapterTextCache } = useNovelChapterCache(); const [hidden, setHidden] = useState(true); const [chapter, setChapter] = useState(initialChapter); const [loading, setLoading] = useState(true); diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx index 97adafce69..86d4f14bf6 100644 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ b/src/screens/settings/SettingsAdvancedScreen.tsx @@ -5,7 +5,6 @@ import { Portal, Text, TextInput } from 'react-native-paper'; import { useTheme, useUserAgent } from '@hooks/persisted'; import { showToast } from '@utils/showToast'; -import { deleteCachedNovels } from '@hooks/persisted/novel/useNovel'; import { getString } from '@strings/translations'; import { useBoolean } from '@hooks'; import ConfirmationDialog from '@components/ConfirmationDialog/ConfirmationDialog'; @@ -21,6 +20,7 @@ import { getUserAgentSync } from 'react-native-device-info'; import CookieManager from '@react-native-cookies/cookies'; import { store } from '@plugins/helpers/storage'; import { recreateDBIndex } from '@database/db'; +import { deleteCachedNovels } from '@utils/mmkv/deleteCachedNovels'; const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => { const theme = useTheme(); diff --git a/src/services/migrate/migrateNovel.ts b/src/services/migrate/migrateNovel.ts index 8a42a338a0..235bcbe9b7 100644 --- a/src/services/migrate/migrateNovel.ts +++ b/src/services/migrate/migrateNovel.ts @@ -9,15 +9,13 @@ import { fetchNovel } from '@services/plugin/fetch'; import { parseChapterNumber } from '@utils/parseChapterNumber'; import { getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv'; -import { - LAST_READ_PREFIX, - NOVEL_SETTINSG_PREFIX, -} from '@hooks/persisted/novel/useNovel'; + import { sleep } from '@utils/sleep'; import ServiceManager, { BackgroundTaskMetadata, } from '@services/ServiceManager'; import { db } from '@database/db'; +import { LAST_READ_PREFIX, NOVEL_SETTINSG_PREFIX } from '@utils/constants/mmkv'; export interface MigrateNovelData { pluginId: string; From 4e910e0cc319e9d573cdfac48173675698064d17 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:12:28 +0200 Subject: [PATCH 09/18] functional --- src/hooks/persisted/novel/useNovelChapters.ts | 79 +-------------- src/hooks/persisted/novel/useNovelPages.ts | 2 +- src/hooks/persisted/novel/useNovelSettings.ts | 2 +- src/hooks/persisted/novel/useNovelState.ts | 43 +------- src/screens/novel/NovelProvider.tsx | 30 +++--- .../novel/context/NovelChaptersContext.tsx | 99 ++++++++++++++++++- .../novel/context/NovelStateContext.tsx | 48 ++++++++- 7 files changed, 164 insertions(+), 139 deletions(-) diff --git a/src/hooks/persisted/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts index 1054e7cfc1..c4817f5373 100644 --- a/src/hooks/persisted/novel/useNovelChapters.ts +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { NovelChaptersContext } from '@screens/novel/context/NovelChaptersContext'; -import { useCallback, useContext, useEffect, useMemo } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { bookmarkChapter as _bookmarkChapter, markChapterRead as _markChapterRead, @@ -11,13 +11,10 @@ import { deleteChapter as _deleteChapter, deleteChapters as _deleteChapters, getPageChapters as _getPageChapters, - insertChapters, - getChapterCount, getPageChaptersBatched, updateChapterProgress as _updateChapterProgress, } from '@database/queries/ChapterQueries'; import { ChapterInfo } from '@database/types'; -import { fetchPage } from '@services/plugin/fetch'; import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; import useNovelState from './useNovelState'; @@ -36,73 +33,17 @@ const useNovelChapters = () => { fetching, batchInformation, setChapters, - _setChapters, + getChapters, updateChapter, extendChapters, mutateChapters, - setFetching, setBatchInformation, } = NovelChapters; - const { novel, path, pluginId, loading } = useNovelState(); + const { novel, loading } = useNovelState(); const { pages, pageIndex, page } = useNovelPages(); const { novelSettings } = useNovelSettings(); const currentPage = pages[pageIndex]; - const getChapters = useCallback(async () => { - if (!loading) { - let newChapters: ChapterInfo[] = []; - - const config = [ - novel.id, - novelSettings.sort, - novelSettings.filter, - currentPage, - ] as const; - - let chapterCount = getChapterCount(novel.id, currentPage); - - if (chapterCount) { - try { - newChapters = getPageChaptersBatched(...config) || []; - } catch (error) { - console.error('teaser', error); - } - } - // Fetch next page if no chapters - else if (Number(currentPage)) { - _setChapters([]); - const sourcePage = await fetchPage(pluginId, path, currentPage); - const sourceChapters = sourcePage.chapters.map(ch => { - return { - ...ch, - page: currentPage, - }; - }); - await insertChapters(novel.id, sourceChapters); - newChapters = await _getPageChapters(...config); - chapterCount = getChapterCount(novel.id, currentPage); - } - - setBatchInformation({ - batch: 0, - total: Math.floor(chapterCount / 300), - totalChapters: chapterCount, - }); - setChapters(newChapters); - } - }, [ - loading, - novel.id, - novelSettings.sort, - novelSettings.filter, - currentPage, - setBatchInformation, - setChapters, - _setChapters, - pluginId, - path, - ]); - const getNextChapterBatch = useCallback(() => { const nextBatch = batchInformation.batch + 1; if (!loading && page && nextBatch <= batchInformation.total) { @@ -335,20 +276,6 @@ const useNovelChapters = () => { ]); // #endregion - useEffect(() => { - if (novel === undefined) return; - setFetching(true); - getChapters() - .catch(e => { - if (__DEV__) console.error(e); - - showToast(e.message); - setFetching(false); - }) - .finally(() => { - setFetching(false); - }); - }, [getChapters, novel, setFetching]); const result = useMemo( () => ({ diff --git a/src/hooks/persisted/novel/useNovelPages.ts b/src/hooks/persisted/novel/useNovelPages.ts index f8c7c71415..54a4ab1597 100644 --- a/src/hooks/persisted/novel/useNovelPages.ts +++ b/src/hooks/persisted/novel/useNovelPages.ts @@ -7,7 +7,7 @@ const useNovelPages = () => { const novelPage = useContext(NovelPageContext); if (!novelPage) { throw new Error( - 'useNovelState must be used within NovelPageContextProvider', + 'useNovelPages must be used within NovelPageContextProvider', ); } const { pages, setPages, pageIndex, setPageIndex } = novelPage; diff --git a/src/hooks/persisted/novel/useNovelSettings.ts b/src/hooks/persisted/novel/useNovelSettings.ts index 9b317af2ee..8e1ed6cf78 100644 --- a/src/hooks/persisted/novel/useNovelSettings.ts +++ b/src/hooks/persisted/novel/useNovelSettings.ts @@ -5,7 +5,7 @@ const useNovelSettings = () => { const novelPage = useContext(NovelSettingsContext); if (!novelPage) { throw new Error( - 'useNovelState must be used within NovelSettingsContextProvider', + 'useNovelSettings must be used within NovelSettingsContextProvider', ); } const { novelSettings, setNovelSettings } = novelPage; diff --git a/src/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts index 7555dc35a5..2b60fe441c 100644 --- a/src/hooks/persisted/novel/useNovelState.ts +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -1,21 +1,15 @@ import { useLibraryContext } from '@components/Context/LibraryContext'; -import { - getNovelByPath, - insertNovelAndChapters, - pickCustomNovelCover, -} from '@database/queries/NovelQueries'; +import { pickCustomNovelCover } from '@database/queries/NovelQueries'; import { downloadFile } from '@plugins/helpers/fetch'; import FileManager from '@specs/NativeFile'; import { NovelStateContext, RouteNovel, } from '@screens/novel/context/NovelStateContext'; -import { fetchNovel } from '@services/plugin/fetch'; import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; import { StorageAccessFramework } from 'expo-file-system'; -import { useCallback, useContext, useEffect, useMemo } from 'react'; -import useNovelPages from './useNovelPages'; +import { useCallback, useContext, useMemo } from 'react'; import { NovelInfo } from '@database/types'; type NovelState = { @@ -39,28 +33,9 @@ const useNovelState = (): NovelState => { ); } - const { novel, setNovel, path, pluginId, loading, setLoading } = novelState; - const { calculatePages } = useNovelPages(); - const { switchNovelToLibrary } = useLibraryContext(); - - const getNovel = useCallback(async () => { - let tmpNovel = getNovelByPath(path, pluginId); - if (!tmpNovel) { - const sourceNovel = await fetchNovel(pluginId, path).catch(() => { - throw new Error(getString('updatesScreen.unableToGetNovel')); - }); - - await insertNovelAndChapters(pluginId, sourceNovel); - tmpNovel = getNovelByPath(path, pluginId); - - if (!tmpNovel) { - return; - } - } - calculatePages(tmpNovel, true); + const { novel, setNovel, path, pluginId, loading, getNovel } = novelState; - setNovel(tmpNovel); - }, [calculatePages, path, pluginId, setNovel]); + const { switchNovelToLibrary } = useLibraryContext(); const followNovel = useCallback(() => { switchNovelToLibrary(path, pluginId).then(() => { @@ -139,16 +114,6 @@ const useNovelState = (): NovelState => { } }, [novel]); - useEffect(() => { - if (novel) { - setLoading(false); - } else { - getNovel().finally(() => { - setLoading(false); - }); - } - }, [getNovel, novel, setLoading]); - const result = useMemo( () => ({ novel, diff --git a/src/screens/novel/NovelProvider.tsx b/src/screens/novel/NovelProvider.tsx index 60efe4a637..fb11aa4868 100644 --- a/src/screens/novel/NovelProvider.tsx +++ b/src/screens/novel/NovelProvider.tsx @@ -25,25 +25,25 @@ export function NovelProvider({ return ( - - - - - - + + + + + {children} - - - - - - + + + + + + ); } diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx index ab33d222ff..a2cbc53adf 100644 --- a/src/screens/novel/context/NovelChaptersContext.tsx +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -1,8 +1,23 @@ +import { + getChapterCount, + getPageChaptersBatched, + insertChapters, + getPageChapters as _getPageChapters, +} from '@database/queries/ChapterQueries'; import { ChapterInfo } from '@database/types'; +import { useNovelPages, useNovelSettings } from '@hooks/persisted'; import useNovelState from '@hooks/persisted/novel/useNovelState'; +import { fetchPage } from '@services/plugin/fetch'; import { parseChapterNumber } from '@utils/parseChapterNumber'; +import { showToast } from '@utils/showToast'; import dayjs from 'dayjs'; -import { createContext, useCallback, useMemo, useState } from 'react'; +import { + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; // ChapterStateContext.tsx interface ChapterState { @@ -17,7 +32,7 @@ interface ChapterState { interface ChapterActions { setChapters: (chapters: ChapterInfo[]) => void; - _setChapters: (chapters: ChapterInfo[]) => void; + getChapters: () => Promise; extendChapters: (chapters: ChapterInfo[]) => void; mutateChapters: (mutation: (chs: ChapterInfo[]) => ChapterInfo[]) => void; @@ -35,9 +50,11 @@ export function NovelChaptersContextProvider({ }: { children: React.JSX.Element; }) { - const { novel } = useNovelState(); + const { novel, loading, path, pluginId } = useNovelState(); + const { novelSettings } = useNovelSettings(); + const { page: currentPage } = useNovelPages(); const [chapters, _setChapters] = useState([]); - const [fetching, setFetching] = useState(true); + const [fetching, setFetching] = useState(false); const [batchInformation, setBatchInformation] = useState<{ batch: number; total: number; @@ -100,13 +117,84 @@ export function NovelChaptersContextProvider({ [transformChapters], ); + const getChapters = useCallback(async () => { + if (!loading) { + let newChapters: ChapterInfo[] = []; + + const config = [ + novel.id, + novelSettings.sort, + novelSettings.filter, + currentPage, + ] as const; + + let chapterCount = getChapterCount(novel.id, currentPage); + + if (chapterCount) { + try { + newChapters = getPageChaptersBatched(...config) || []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('teaser', error); + } + } + // Fetch next page if no chapters + else if (Number(currentPage)) { + _setChapters([]); + const sourcePage = await fetchPage(pluginId, path, currentPage); + const sourceChapters = sourcePage.chapters.map(ch => { + return { + ...ch, + page: currentPage, + }; + }); + await insertChapters(novel.id, sourceChapters); + newChapters = await _getPageChapters(...config); + chapterCount = getChapterCount(novel.id, currentPage); + } + + setBatchInformation({ + batch: 0, + total: Math.floor(chapterCount / 300), + totalChapters: chapterCount, + }); + setChapters(newChapters); + } + }, [ + loading, + novel.id, + novelSettings.sort, + novelSettings.filter, + currentPage, + setBatchInformation, + setChapters, + _setChapters, + pluginId, + path, + ]); + + useEffect(() => { + if (loading) return; + + getChapters() + .catch(e => { + // eslint-disable-next-line no-console + if (__DEV__) console.error(e); + + showToast(e.message); + }) + .finally(() => { + setFetching(false); + }); + }, [getChapters, loading]); + const contextValue = useMemo( () => ({ chapters, fetching, batchInformation, setChapters, - _setChapters, + getChapters, extendChapters, mutateChapters, updateChapter, @@ -118,6 +206,7 @@ export function NovelChaptersContextProvider({ chapters, extendChapters, fetching, + getChapters, mutateChapters, setChapters, updateChapter, diff --git a/src/screens/novel/context/NovelStateContext.tsx b/src/screens/novel/context/NovelStateContext.tsx index ac13e4bd70..ddb0f7204d 100644 --- a/src/screens/novel/context/NovelStateContext.tsx +++ b/src/screens/novel/context/NovelStateContext.tsx @@ -1,7 +1,20 @@ +import { + getNovelByPath, + insertNovelAndChapters, +} from '@database/queries/NovelQueries'; import { NovelInfo } from '@database/types'; +import { useNovelPages } from '@hooks/persisted'; import { ReaderStackParamList } from '@navigators/types'; import { RouteProp } from '@react-navigation/native'; -import { createContext, useMemo, useState } from 'react'; +import { fetchNovel } from '@services/plugin/fetch'; +import { getString } from '@strings/translations'; +import { + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; type Route = RouteProp; type Params = Route['params']; @@ -29,6 +42,7 @@ interface NovelState { interface NovelActions { setNovel: (novel: NovelInfo) => void; setLoading: (loading: boolean) => void; + getNovel: () => Promise; } export const NovelStateContext = createContext< @@ -42,6 +56,7 @@ export function NovelStateContextProvider({ children: React.JSX.Element; novelParams: Route['params']; }) { + const { calculatePages } = useNovelPages(); const routeNovel: RouteNovel = useMemo( () => ({ inLibrary: false, @@ -55,6 +70,34 @@ export function NovelStateContextProvider({ const [novel, setNovel] = useState(routeNovel); const [loading, setLoading] = useState(true); + const path = routeNovel.path; + const pluginId = routeNovel.pluginId; + + const getNovel = useCallback(async () => { + let tmpNovel = getNovelByPath(path, pluginId); + if (!tmpNovel) { + const sourceNovel = await fetchNovel(pluginId, path).catch(() => { + throw new Error(getString('updatesScreen.unableToGetNovel')); + }); + + await insertNovelAndChapters(pluginId, sourceNovel); + tmpNovel = getNovelByPath(path, pluginId); + + if (!tmpNovel) { + return; + } + } + calculatePages(tmpNovel, true); + + setNovel(tmpNovel); + }, [calculatePages, path, pluginId, setNovel]); + + useEffect(() => { + getNovel().finally(() => { + setLoading(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const contextValue = useMemo( () => @@ -65,8 +108,9 @@ export function NovelStateContextProvider({ pluginId: novelParams.pluginId, setNovel, setLoading, + getNovel, } as (NovelState & NovelActions) | (NovelStateLoading & NovelActions)), - [loading, novel, novelParams.path, novelParams.pluginId], + [getNovel, loading, novel, novelParams.path, novelParams.pluginId], ); return ( From 4b404546d380ab5031bc11862dfd132119891a49 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:45:45 +0200 Subject: [PATCH 10/18] chapter rendering performance --- package-lock.json | 83 ++--- package.json | 8 +- src/database/queries/ChapterQueries.ts | 92 +++-- src/hooks/persisted/novel/useNovelChapters.ts | 13 +- src/screens/novel/NovelScreen.tsx | 5 +- src/screens/novel/components/ChapterItem.tsx | 273 ++++++++------- .../novel/components/NovelScreenList.tsx | 20 +- .../novel/context/NovelChaptersContext.tsx | 331 ++++++++++-------- .../novel/context/NovelStateContext.tsx | 29 +- src/screens/reader/hooks/useChapter.ts | 14 +- .../updates/components/ChapterItem.tsx | 206 +++++++++++ src/utils/normalizeChapters.ts | 109 ++++++ 12 files changed, 782 insertions(+), 401 deletions(-) create mode 100644 src/screens/updates/components/ChapterItem.tsx create mode 100644 src/utils/normalizeChapters.ts diff --git a/package-lock.json b/package-lock.json index 64d2453a58..b8a8ad663b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,8 +69,8 @@ }, "devDependencies": { "@babel/core": "^7.26.0", - "@babel/preset-env": "7.25.3", - "@babel/runtime": "7.25.0", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", "@react-native-community/cli": "^18.0.0", "@react-native-community/cli-platform-android": "^18.0.0", "@react-native-community/cli-platform-ios": "^18.0.0", @@ -84,7 +84,7 @@ "@types/react": "19.1.3", "@types/sanitize-html": "^2.11.0", "babel-plugin-module-resolver": "^5.0.2", - "babel-plugin-react-compiler": "19.1.0-rc.1", + "babel-plugin-react-compiler": "19.1.0-rc.2", "eslint": "^8.19.0", "husky": "^7.0.4", "lint-staged": "^12.3.7", @@ -2121,13 +2121,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } @@ -2332,9 +2329,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3402,9 +3399,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -5959,9 +5956,9 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "19.1.0-rc.1", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.1.tgz", - "integrity": "sha512-M4fpG+Hfq5gWzsJeeMErdRokzg0fdJ8IAk+JDhfB/WLT+U3WwJWR8edphypJrk447/JEvYu6DBFwsTn10bMW4Q==", + "version": "19.1.0-rc.2", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.2.tgz", + "integrity": "sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==", "dev": true, "license": "MIT", "dependencies": { @@ -6220,9 +6217,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6797,16 +6794,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -7963,9 +7960,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -8052,9 +8049,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -9144,9 +9141,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -12370,9 +12367,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13819,12 +13816,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -15289,9 +15280,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", diff --git a/package.json b/package.json index 46eaac42f9..3f729727ea 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "android": { "javaPackageName": "com.lnreader.spec" }, - "ios": { + "ios": { "modulesProvider": { "NativeEpubUtil": "RCTNativeEpubUtil", "NativeFile": "RCTNativeFile", @@ -96,8 +96,8 @@ }, "devDependencies": { "@babel/core": "^7.26.0", - "@babel/preset-env": "7.25.3", - "@babel/runtime": "7.25.0", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", "@react-native-community/cli": "^18.0.0", "@react-native-community/cli-platform-android": "^18.0.0", "@react-native-community/cli-platform-ios": "^18.0.0", @@ -111,7 +111,7 @@ "@types/react": "19.1.3", "@types/sanitize-html": "^2.11.0", "babel-plugin-module-resolver": "^5.0.2", - "babel-plugin-react-compiler": "19.1.0-rc.1", + "babel-plugin-react-compiler": "19.1.0-rc.2", "eslint": "^8.19.0", "husky": "^7.0.4", "lint-staged": "^12.3.7", diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 024737682a..f1b1e03c27 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -11,6 +11,11 @@ import { getString } from '@strings/translations'; import { NOVEL_STORAGE } from '@utils/Storages'; import { db } from '@database/db'; import NativeFile from '@specs/NativeFile'; +import { + normaliseAsyncChapter, + normaliseAsyncChapters, + normaliseChapters, +} from '@utils/normalizeChapters'; // #region Mutations @@ -195,15 +200,20 @@ export const getCustomPages = (novelId: number) => ); export const getNovelChapters = (novelId: number) => - db.getAllAsync( - 'SELECT * FROM Chapter WHERE novelId = ?', - novelId, + normaliseAsyncChapters( + db.getAllAsync( + 'SELECT * FROM Chapter WHERE novelId = ?', + novelId, + ), ); -export const getChapter = (chapterId: number) => - db.getFirstAsync( - 'SELECT * FROM Chapter WHERE id = ?', - chapterId, +export const getChapter = (chapterId: number, novelName: string) => + normaliseAsyncChapter( + db.getFirstAsync( + 'SELECT * FROM Chapter WHERE id = ?', + chapterId, + ), + novelName, ); const getPageChaptersQuery = ( @@ -221,16 +231,20 @@ const getPageChaptersQuery = ( export const getPageChapters = ( novelId: number, + novelName: string, sort?: string, filter?: string, page?: string, offset?: number, limit?: number, ) => { - return db.getAllAsync( - getPageChaptersQuery(sort, filter, limit, offset), - novelId, - page || '1', + return normaliseAsyncChapters( + db.getAllAsync( + getPageChaptersQuery(sort, filter, limit, offset), + novelId, + page || '1', + ), + novelName, ); }; @@ -243,15 +257,19 @@ export const getChapterCount = (novelId: number, page: string = '1') => export const getPageChaptersBatched = ( novelId: number, + novelName: string, sort?: string, filter?: string, page?: string, batch: number = 0, ) => { - return db.getAllSync( - getPageChaptersQuery(sort, filter, 300, 300 * batch), - novelId, - page || '1', + return normaliseChapters( + db.getAllSync( + getPageChaptersQuery(sort, filter, 300, 300 * batch), + novelId, + page || '1', + ), + novelName, ); }; @@ -259,49 +277,60 @@ export const getPrevChapter = ( novelId: number, chapterPosition: number, page: string, + novelName: string, ) => - db.getFirstAsync( - `SELECT * FROM Chapter + normaliseAsyncChapter( + db.getFirstAsync( + `SELECT * FROM Chapter WHERE novelId = ? AND ( (position < ? AND page = ?) OR page < ? ) ORDER BY position DESC, page DESC`, - novelId, - chapterPosition, - page, - page, + novelId, + chapterPosition, + page, + page, + ), + novelName, ); export const getNextChapter = ( novelId: number, chapterPosition: number, page: string, + novelName: string, ) => - db.getFirstAsync( - `SELECT * FROM Chapter + normaliseAsyncChapter( + db.getFirstAsync( + `SELECT * FROM Chapter WHERE novelId = ? AND ( (page = ? AND position > ?) OR (position = 0 AND page > ?) ) ORDER BY position ASC, page ASC`, - novelId, - page, - chapterPosition, - page, + novelId, + page, + chapterPosition, + page, + ), + novelName, ); const getReadDownloadedChapters = () => - db.getAllAsync(` + normaliseAsyncChapters( + db.getAllAsync(` SELECT Chapter.id, Chapter.novelId, pluginId FROM Chapter JOIN Novel - ON Novel.id = Chapter.novelId AND unread = 0 AND isDownloaded = 1`); + ON Novel.id = Chapter.novelId AND unread = 0 AND isDownloaded = 1`), + ); export const getDownloadedChapters = () => - db.getAllAsync(` + normaliseAsyncChapters( + db.getAllAsync(` SELECT Chapter.*, Novel.pluginId, Novel.name as novelName, Novel.cover as novelCover, Novel.path as novelPath @@ -309,7 +338,8 @@ export const getDownloadedChapters = () => JOIN Novel ON Chapter.novelId = Novel.id WHERE Chapter.isDownloaded = 1 - `); + `), + ); export const getUpdatedOverviewFromDb = () => db.getAllAsync(`SELECT diff --git a/src/hooks/persisted/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts index c4817f5373..0fd359834e 100644 --- a/src/hooks/persisted/novel/useNovelChapters.ts +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -37,7 +37,6 @@ const useNovelChapters = () => { updateChapter, extendChapters, mutateChapters, - setBatchInformation, } = NovelChapters; const { novel, loading } = useNovelState(); const { pages, pageIndex, page } = useNovelPages(); @@ -53,6 +52,7 @@ const useNovelChapters = () => { newChapters = getPageChaptersBatched( novel.id, + novel.name, novelSettings.sort, novelSettings.filter, page, @@ -61,16 +61,15 @@ const useNovelChapters = () => { } catch (error) { console.error('teaser', error); } - setBatchInformation({ ...batchInformation, batch: nextBatch }); - extendChapters(newChapters); + extendChapters(newChapters, { ...batchInformation, batch: nextBatch }); } }, [ batchInformation, loading, page, - setBatchInformation, extendChapters, novel.id, + novel.name, novelSettings.sort, novelSettings.filter, ]); @@ -207,7 +206,7 @@ const useNovelChapters = () => { // #region refresh and delete const deleteChapter = useCallback( - (_chapter: ChapterInfo) => { + async (_chapter: ChapterInfo) => { if (!loading) { _deleteChapter(novel.pluginId, novel.id, _chapter.id).then(() => { mutateChapters(chs => @@ -258,17 +257,19 @@ const useNovelChapters = () => { if (!loading && !fetching) { _getPageChapters( novel.id, + novel.name, novelSettings.sort, novelSettings.filter, currentPage, ).then(chs => { - setChapters(chs); + setChapters(chs, { ...batchInformation }); }); } }, [ loading, fetching, novel.id, + novel.name, novelSettings.sort, novelSettings.filter, currentPage, diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index be89174c24..44d036a0e7 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -183,7 +183,7 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { const renderDrawerContent = useCallback(() => { if (loading) { - return ; + return null; } if ((novel?.totalPages ?? 0) > 1 || pages.length > 1) { return ( @@ -257,7 +257,8 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { )} - }> + {/* }> */} + }> = ({ isBookmarked ??= bookmark; + const highlight = useMemo( + () => [{ backgroundColor: theme.rippleColor }], + [theme.rippleColor], + ); + + const pressableStyle = isSelected + ? [styles.container, highlight] + : styles.container; + + const titleColour = useMemo(() => { + if (!unread) return theme.outline; + return bookmark ? theme.primary : theme.onSurface; + }, [unread, bookmark, theme]); + + const updateTitle = useMemo( + () => ({ + fontSize: 14, + color: unread ? theme.onSurface : theme.outline, + }), + [theme.onSurface, theme.outline, unread], + ); + const title = useMemo( + () => ({ + fontSize: isUpdateCard ? 12 : 14, + color: titleColour, + flex: 1, + }), + [isUpdateCard, titleColour], + ); + const meta = useMemo( + () => [ + styles.text, + { + marginTop: 4, + color: !unread + ? theme.outline + : isBookmarked + ? theme.primary + : theme.onSurfaceVariant, + }, + ], + [ + isBookmarked, + theme.onSurfaceVariant, + theme.outline, + theme.primary, + unread, + ], + ); + const progressStyle = useMemo( + () => ({ + fontSize: 12, + color: theme.outline, + marginLeft: releaseTime ? 5 : 0, + marginTop: 4, + }), + [releaseTime, theme.outline], + ); + + const handlePress = useCallback(() => { + onSelectPress ? onSelectPress(chapter) : navigateToChapter(chapter); + }, [onSelectPress, navigateToChapter, chapter]); + + const handleLong = useCallback(() => { + onSelectLongPress?.(chapter); + }, [onSelectLongPress, chapter]); + return ( - - { - if (onSelectPress) { - onSelectPress(chapter); - } else { - navigateToChapter(chapter); - } - }} - onLongPress={() => onSelectLongPress?.(chapter)} - android_ripple={{ color: theme.rippleColor }} - > - - {left} - {isBookmarked ? : null} - - {isUpdateCard ? ( - - {novelName} - - ) : null} - - {unread ? ( - - ) : null} - - - {showChapterTitles - ? name - : getString('novelScreen.chapterChapnum', { - num: chapterNumber, - })} - - - - {releaseTime && !isUpdateCard ? ( - - {releaseTime} - - ) : null} - {!isUpdateCard && progress && progress > 0 && chapter.unread ? ( - - {chapter.releaseTime ? '• ' : null} - {getString('novelScreen.progress', { progress })} - - ) : null} - - + + {left} + {isBookmarked ? : null} + + + {isUpdateCard && ( + + {novelName} + + )} + + + {unread && ( + + )} + + + {showChapterTitles + ? name + : getString('novelScreen.chapterChapnum', { + num: chapterNumber, + })} + + + + + {releaseTime && !isUpdateCard && ( + + {releaseTime} + + )} + + {!isUpdateCard && (progress ?? 0) > 0 && unread && ( + + {releaseTime ? '• ' : ''} + {getString('novelScreen.progress', { progress })} + + )} - {!isLocal ? ( - - ) : null} - - + + + {!isLocal && ( + + )} + ); }; -export default memo(ChapterItem); +function areEqual(prev: ChapterItemProps, next: ChapterItemProps) { + return ( + prev.isSelected === next.isSelected && + prev.isDownloading === next.isDownloading && + prev.isBookmarked === next.isBookmarked && + prev.chapter.unread === next.chapter.unread && + prev.chapter.bookmark === next.chapter.bookmark && + prev.chapter.isDownloaded === next.chapter.isDownloaded && + prev.chapter.progress === next.chapter.progress + ); +} + +export default memo(ChapterItem, areEqual); const styles = StyleSheet.create({ - chapterCardContainer: { + container: { alignItems: 'center', flexDirection: 'row', height: 64, @@ -202,4 +219,6 @@ const styles = StyleSheet.create({ unreadIcon: { marginRight: 4, }, + rowCenter: { flexDirection: 'row', alignItems: 'center' }, + flex1: { flex: 1 }, }); diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index 5f768ffb74..261f21660c 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ChapterItem from './ChapterItem'; import NovelInfoHeader from './Info/NovelInfoHeader'; import { useRef, useState, useCallback, useMemo } from 'react'; -import { ChapterInfo, NovelInfo } from '@database/types'; +import { ChapterInfo } from '@database/types'; import { useBoolean } from '@hooks/index'; import { useAppSettings, @@ -67,7 +67,7 @@ const NovelScreenList = ({ setSelected, getNextChapterBatch, }: NovelScreenListProps) => { - const { getNovel, novel: fetchedNovel, loading } = useNovelState(); + const { getNovel, novel, loading } = useNovelState(); const { chapters, deleteChapter, fetching, batchInformation, updateChapter } = useNovelChapters(); const { novelSettings, sortAndFilterChapters, setShowChapterTitles } = @@ -76,19 +76,6 @@ const NovelScreenList = ({ const { pluginId } = routeBaseNovel; - // Memoize route novel to prevent recreation on every render - const routeNovel: Omit & { id: 'NO_ID' } = useMemo( - () => ({ - inLibrary: false, - isLocal: false, - totalPages: 0, - ...routeBaseNovel, - id: 'NO_ID', - }), - [routeBaseNovel], - ); - - const novel = fetchedNovel ?? routeNovel; const [updating, setUpdating] = useState(false); const { @@ -116,7 +103,6 @@ const NovelScreenList = ({ const trackerSheetRef = useRef(null); const deleteDownloadsSnackbar = useBoolean(); - // Memoize selected chapter IDs for faster lookup const selectedIds = useMemo( () => new Set(selected.map(chapter => chapter.id)), @@ -382,7 +368,7 @@ const NovelScreenList = ({ onEndReached={getNextChapterBatch} onEndReachedThreshold={6} onScroll={onPageScroll} - drawDistance={1000} + drawDistance={400} ListHeaderComponent={renderHeader} /> {novel.id !== 'NO_ID' && ( diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx index a2cbc53adf..146289c7a3 100644 --- a/src/screens/novel/context/NovelChaptersContext.tsx +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -4,215 +4,254 @@ import { insertChapters, getPageChapters as _getPageChapters, } from '@database/queries/ChapterQueries'; -import { ChapterInfo } from '@database/types'; +import { ChapterInfo, NovelInfo } from '@database/types'; import { useNovelPages, useNovelSettings } from '@hooks/persisted'; -import useNovelState from '@hooks/persisted/novel/useNovelState'; import { fetchPage } from '@services/plugin/fetch'; -import { parseChapterNumber } from '@utils/parseChapterNumber'; import { showToast } from '@utils/showToast'; -import dayjs from 'dayjs'; import { createContext, useCallback, + useContext, useEffect, useMemo, - useState, + useReducer, } from 'react'; +import { NovelStateContext } from './NovelStateContext'; +import { getString } from '@strings/translations'; + +interface BatchInfo { + batch: number; + total: number; + totalChapters?: number; +} -// ChapterStateContext.tsx interface ChapterState { chapters: ChapterInfo[]; - fetching: boolean; // for chapter loading only - batchInformation: { - batch: number; - total: number; - totalChapters?: number; - }; + fetching: boolean; + batchInformation: BatchInfo; } -interface ChapterActions { - setChapters: (chapters: ChapterInfo[]) => void; - getChapters: () => Promise; +type Action = + | { type: 'SET_FETCHING'; value: boolean } + | { type: 'SET_CHAPTERS'; chapters: ChapterInfo[]; batch: BatchInfo } + | { type: 'EXTEND_CHAPTERS'; chapters: ChapterInfo[]; batch: BatchInfo } + | { type: 'UPDATE_CHAPTER'; index: number; update: Partial } + | { + type: 'MUTATE_CHAPTERS'; + mutation: (chs: ChapterInfo[]) => ChapterInfo[]; + }; + +// #region reducer +function reducer(state: ChapterState, action: Action): ChapterState { + switch (action.type) { + case 'SET_FETCHING': + return { ...state, fetching: action.value }; + + case 'SET_CHAPTERS': + return { + chapters: action.chapters, + fetching: false, + batchInformation: action.batch, + }; + + case 'EXTEND_CHAPTERS': + return { + ...state, + chapters: [...state.chapters, ...action.chapters], + batchInformation: action.batch, + }; + + case 'UPDATE_CHAPTER': { + const next = [...state.chapters]; + next[action.index] = { ...next[action.index], ...action.update }; + return { ...state, chapters: next }; + } - extendChapters: (chapters: ChapterInfo[]) => void; - mutateChapters: (mutation: (chs: ChapterInfo[]) => ChapterInfo[]) => void; - updateChapter: (index: number, update: Partial) => void; - setFetching: (fetching: boolean) => void; - setBatchInformation: (batch: ChapterState['batchInformation']) => void; + case 'MUTATE_CHAPTERS': + return { ...state, chapters: action.mutation(state.chapters) }; + + default: + return state; + } +} + +// #endregion +// #region context +interface ChapterContextValue extends ChapterState { + getChapters: (n?: NovelInfo) => Promise; + setChapters: (chs: ChapterInfo[], batchInfo: BatchInfo) => void; + extendChapters: (chs: ChapterInfo[], batchInfo: BatchInfo) => void; + mutateChapters: (m: (chs: ChapterInfo[]) => ChapterInfo[]) => void; + updateChapter: (i: number, u: Partial) => void; + setFetching: (v: boolean) => void; } -export const NovelChaptersContext = createContext< - (ChapterState & ChapterActions) | null ->(null); +export const NovelChaptersContext = createContext( + null, +); +// #endregion +// #region provider export function NovelChaptersContextProvider({ children, }: { children: React.JSX.Element; }) { - const { novel, loading, path, pluginId } = useNovelState(); + const novelState = useContext(NovelStateContext); + if (!novelState) { + throw new Error( + 'useNovelState must be used within NovelStateContextProvider', + ); + } + + const { novel, path, pluginId, loading, getNovel } = novelState; const { novelSettings } = useNovelSettings(); const { page: currentPage } = useNovelPages(); - const [chapters, _setChapters] = useState([]); - const [fetching, setFetching] = useState(false); - const [batchInformation, setBatchInformation] = useState<{ - batch: number; - total: number; - totalChapters?: number; - }>({ batch: 0, total: 0 }); - const mutateChapters = useCallback( - (mutation: (chs: ChapterInfo[]) => ChapterInfo[]) => { - if (novel) { - _setChapters(mutation); - } - }, - [novel], - ); + const [state, dispatch] = useReducer(reducer, { + chapters: [], + fetching: false, + batchInformation: { batch: 0, total: 0 }, + }); - const updateChapter = useCallback( - (index: number, update: Partial) => { - if (novel) { - _setChapters(chs => { - chs[index] = { ...chs[index], ...update }; - return chs; - }); - } - }, - [novel], - ); - const transformChapters = useCallback( - (chs: ChapterInfo[]) => { - if (!novel) return chs; - const newChapters = chs.map(chapter => { - const parsedTime = dayjs(chapter.releaseTime); - const releaseTime = parsedTime.isValid() - ? parsedTime.format('LL') - : chapter.releaseTime; - const chapterNumber = chapter.chapterNumber - ? chapter.chapterNumber - : parseChapterNumber(novel.name, chapter.name); - return { - ...chapter, - releaseTime, - chapterNumber, - }; - }); - return newChapters; - }, - [novel], + const setFetching = useCallback( + (v: boolean) => dispatch({ type: 'SET_FETCHING', value: v }), + [], ); const setChapters = useCallback( - async (chs: ChapterInfo[]) => { - _setChapters(transformChapters(chs)); - }, - [transformChapters], + (chs: ChapterInfo[], batchInfo: BatchInfo) => + dispatch({ type: 'SET_CHAPTERS', chapters: chs, batch: batchInfo }), + [], ); const extendChapters = useCallback( - async (chs: ChapterInfo[]) => { - _setChapters(prev => prev.concat(transformChapters(chs))); - }, - [transformChapters], + (chs: ChapterInfo[], batchInfo: BatchInfo) => + dispatch({ type: 'EXTEND_CHAPTERS', chapters: chs, batch: batchInfo }), + [], ); - const getChapters = useCallback(async () => { - if (!loading) { - let newChapters: ChapterInfo[] = []; - - const config = [ - novel.id, - novelSettings.sort, - novelSettings.filter, - currentPage, - ] as const; - - let chapterCount = getChapterCount(novel.id, currentPage); - - if (chapterCount) { - try { - newChapters = getPageChaptersBatched(...config) || []; - } catch (error) { - // eslint-disable-next-line no-console - console.error('teaser', error); + const mutateChapters = useCallback( + (m: (c: ChapterInfo[]) => ChapterInfo[]) => + dispatch({ type: 'MUTATE_CHAPTERS', mutation: m }), + [], + ); + + const updateChapter = useCallback( + (idx: number, up: Partial) => + dispatch({ type: 'UPDATE_CHAPTER', index: idx, update: up }), + [], + ); + + const getChapters = useCallback( + async (passedNovel?: NovelInfo) => { + if (!loading || passedNovel) { + const novelName = passedNovel?.name ?? novel.name; + const novelId = passedNovel?.id ?? (novel.id as number); + let newChapters: ChapterInfo[] = []; + + const config = [ + novelId, + novelName, + novelSettings.sort, + novelSettings.filter, + currentPage, + state.batchInformation.batch, + ] as const; + + let chapterCount = getChapterCount(novelId, currentPage); + + if (chapterCount) { + try { + newChapters = getPageChaptersBatched(...config) || []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('teaser', error); + } } - } - // Fetch next page if no chapters - else if (Number(currentPage)) { - _setChapters([]); - const sourcePage = await fetchPage(pluginId, path, currentPage); - const sourceChapters = sourcePage.chapters.map(ch => { - return { - ...ch, - page: currentPage, - }; + // Fetch next page if no chapters + else if (Number(currentPage)) { + const sourcePage = await fetchPage(pluginId, path, currentPage); + const sourceChapters = sourcePage.chapters.map(ch => { + return { + ...ch, + page: currentPage, + }; + }); + await insertChapters(novelId, sourceChapters); + newChapters = await _getPageChapters(...config); + chapterCount = getChapterCount(novelId, currentPage); + } + + const batchInformation = { + batch: 0, + total: Math.floor(chapterCount / 300), + totalChapters: chapterCount, + }; + dispatch({ + type: 'SET_CHAPTERS', + chapters: newChapters, + batch: batchInformation, }); - await insertChapters(novel.id, sourceChapters); - newChapters = await _getPageChapters(...config); - chapterCount = getChapterCount(novel.id, currentPage); } - - setBatchInformation({ - batch: 0, - total: Math.floor(chapterCount / 300), - totalChapters: chapterCount, - }); - setChapters(newChapters); - } - }, [ - loading, - novel.id, - novelSettings.sort, - novelSettings.filter, - currentPage, - setBatchInformation, - setChapters, - _setChapters, - pluginId, - path, - ]); + }, + [ + loading, + novel.name, + novel.id, + novelSettings.sort, + novelSettings.filter, + currentPage, + state.batchInformation.batch, + pluginId, + path, + ], + ); useEffect(() => { - if (loading) return; + let cancelled = false; - getChapters() - .catch(e => { + (async () => { + try { + const nov = await getNovel(); + if (!nov || cancelled) { + throw new Error(getString('updatesScreen.unableToGetNovel')); + } + await getChapters(nov); + } catch (e: any) { // eslint-disable-next-line no-console if (__DEV__) console.error(e); - showToast(e.message); - }) - .finally(() => { setFetching(false); - }); - }, [getChapters, loading]); + } + })(); + + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const contextValue = useMemo( () => ({ - chapters, - fetching, - batchInformation, + ...state, setChapters, getChapters, extendChapters, mutateChapters, updateChapter, setFetching, - setBatchInformation, }), [ - batchInformation, - chapters, extendChapters, - fetching, getChapters, mutateChapters, setChapters, + setFetching, + state, updateChapter, ], ); - return ( {children} diff --git a/src/screens/novel/context/NovelStateContext.tsx b/src/screens/novel/context/NovelStateContext.tsx index ddb0f7204d..ff1a93327f 100644 --- a/src/screens/novel/context/NovelStateContext.tsx +++ b/src/screens/novel/context/NovelStateContext.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { getNovelByPath, insertNovelAndChapters, @@ -8,13 +9,7 @@ import { ReaderStackParamList } from '@navigators/types'; import { RouteProp } from '@react-navigation/native'; import { fetchNovel } from '@services/plugin/fetch'; import { getString } from '@strings/translations'; -import { - createContext, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import { createContext, useCallback, useMemo, useState } from 'react'; type Route = RouteProp; type Params = Route['params']; @@ -42,7 +37,7 @@ interface NovelState { interface NovelActions { setNovel: (novel: NovelInfo) => void; setLoading: (loading: boolean) => void; - getNovel: () => Promise; + getNovel: () => Promise; } export const NovelStateContext = createContext< @@ -56,6 +51,8 @@ export function NovelStateContextProvider({ children: React.JSX.Element; novelParams: Route['params']; }) { + const t = Date.now() - 1754470000000; + console.time('NovelStateContextProvider ' + t); const { calculatePages } = useNovelPages(); const routeNovel: RouteNovel = useMemo( () => ({ @@ -74,6 +71,8 @@ export function NovelStateContextProvider({ const pluginId = routeNovel.pluginId; const getNovel = useCallback(async () => { + const t = Date.now() - 1754470000000; + console.time('getNovel ' + t); let tmpNovel = getNovelByPath(path, pluginId); if (!tmpNovel) { const sourceNovel = await fetchNovel(pluginId, path).catch(() => { @@ -90,14 +89,10 @@ export function NovelStateContextProvider({ calculatePages(tmpNovel, true); setNovel(tmpNovel); - }, [calculatePages, path, pluginId, setNovel]); - - useEffect(() => { - getNovel().finally(() => { - setLoading(false); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + setLoading(false); + console.timeEnd('getNovel ' + t); + return tmpNovel; + }, [calculatePages, path, pluginId]); const contextValue = useMemo( () => @@ -112,7 +107,7 @@ export function NovelStateContextProvider({ } as (NovelState & NovelActions) | (NovelStateLoading & NovelActions)), [getNovel, loading, novel, novelParams.path, novelParams.pluginId], ); - + console.timeEnd('NovelStateContextProvider ' + t); return ( {children} diff --git a/src/screens/reader/hooks/useChapter.ts b/src/screens/reader/hooks/useChapter.ts index 154095e7ac..3d3356b17b 100644 --- a/src/screens/reader/hooks/useChapter.ts +++ b/src/screens/reader/hooks/useChapter.ts @@ -120,8 +120,8 @@ export default function useChapter( const cachedText = chapterTextCache.get(chap.id); const text = cachedText ?? loadChapterText(chap.id, chap.path); const [nextChap, prevChap, awaitedText] = await Promise.all([ - getNextChapter(chap.novelId, chap.position!, chap.page), - getPrevChapter(chap.novelId, chap.position!, chap.page), + getNextChapter(chap.novelId, chap.position!, chap.page, novel.name), + getPrevChapter(chap.novelId, chap.position!, chap.page, novel.name), text, ]); if (nextChap && !chapterTextCache.get(nextChap.id)) { @@ -252,15 +252,19 @@ export default function useChapter( useEffect(() => { if (!incognitoMode) { insertHistory(chapter.id); - getDbChapter(chapter.id).then(result => result && setLastRead(result)); + getDbChapter(chapter.id, novel.name).then( + result => result && setLastRead(result), + ); } return () => { if (!incognitoMode) { - getDbChapter(chapter.id).then(result => result && setLastRead(result)); + getDbChapter(chapter.id, novel.name).then( + result => result && setLastRead(result), + ); } }; - }, [incognitoMode, setLastRead, setLoading, chapter.id]); + }, [incognitoMode, setLastRead, setLoading, chapter.id, novel.name]); useEffect(() => { if (!chapter || !chapterText) { diff --git a/src/screens/updates/components/ChapterItem.tsx b/src/screens/updates/components/ChapterItem.tsx new file mode 100644 index 0000000000..b09cf9d7e6 --- /dev/null +++ b/src/screens/updates/components/ChapterItem.tsx @@ -0,0 +1,206 @@ +/* eslint-disable react-native/no-inline-styles */ +import React, { memo, ReactNode } from 'react'; +import { View, Text, StyleSheet, Pressable } from 'react-native'; +import color from 'color'; + +import { ThemeColors } from '@theme/types'; +import { ChapterInfo } from '@database/types'; +import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; +import { getString } from '@strings/translations'; +import { + ChapterBookmarkButton, + DownloadButton, +} from '@screens/novel/components/Chapter/ChapterDownloadButtons'; + +interface ChapterItemProps { + isDownloading?: boolean; + isBookmarked?: boolean; + chapter: ChapterInfo; + theme: ThemeColors; + showChapterTitles: boolean; + isSelected?: boolean; + downloadChapter: () => void; + deleteChapter: () => void; + onSelectPress?: (chapter: ChapterInfo) => void; + onSelectLongPress?: (chapter: ChapterInfo) => void; + navigateToChapter: (chapter: ChapterInfo) => void; + setChapterDownloaded?: (value: boolean) => void; + left?: ReactNode; + isLocal: boolean; + isUpdateCard?: boolean; + novelName: string; +} + +const ChapterItem: React.FC = ({ + isDownloading, + isBookmarked, + chapter, + theme, + showChapterTitles, + downloadChapter, + deleteChapter, + isSelected, + onSelectPress, + onSelectLongPress, + navigateToChapter, + setChapterDownloaded, + isLocal, + left, + isUpdateCard, + novelName, +}) => { + const { id, name, unread, releaseTime, bookmark, chapterNumber, progress } = + chapter; + + isBookmarked ??= bookmark; + + return ( + + { + if (onSelectPress) { + onSelectPress(chapter); + } else { + navigateToChapter(chapter); + } + }} + onLongPress={() => onSelectLongPress?.(chapter)} + android_ripple={{ color: theme.rippleColor }} + > + + {left} + {isBookmarked ? : null} + + {isUpdateCard ? ( + + {novelName} + + ) : null} + + {unread ? ( + + ) : null} + + + {showChapterTitles + ? name + : getString('novelScreen.chapterChapnum', { + num: chapterNumber, + })} + + + + {releaseTime && !isUpdateCard ? ( + + {releaseTime} + + ) : null} + {!isUpdateCard && progress && progress > 0 && chapter.unread ? ( + + {chapter.releaseTime ? '• ' : null} + {getString('novelScreen.progress', { progress })} + + ) : null} + + + + {!isLocal ? ( + + ) : null} + + + ); +}; + +export default memo(ChapterItem); + +const styles = StyleSheet.create({ + chapterCardContainer: { + alignItems: 'center', + flexDirection: 'row', + height: 64, + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 8, + }, + row: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + }, + text: { + fontSize: 12, + }, + unreadIcon: { + marginRight: 4, + }, +}); diff --git a/src/utils/normalizeChapters.ts b/src/utils/normalizeChapters.ts new file mode 100644 index 0000000000..06f8d704fc --- /dev/null +++ b/src/utils/normalizeChapters.ts @@ -0,0 +1,109 @@ +import { ChapterInfo, DownloadedChapter } from '@database/types'; +import dayjs from 'dayjs'; +import { parseChapterNumber } from './parseChapterNumber'; + +function normaliseChapters( + raw: T, + novelName: string, +): T; +function normaliseChapters( + raw: T, + novelName?: string, +): T; +function normaliseChapters< + T extends Array | null, +>(raw: T, novelName?: string): T; +function normaliseChapters< + T extends Array | null, +>( + raw: T, + novelName: string = '', +): Array | null { + if (!raw) return null; + return raw.map(c => ({ + ...c, + releaseTime: dayjs(c.releaseTime).isValid() + ? dayjs(c.releaseTime).format('LL') + : c.releaseTime, + chapterNumber: + // @ts-ignore + c.chapterNumber ?? parseChapterNumber(novelName ?? c.novelName, c.name), + })); +} + +async function normaliseAsyncChapters( + raw: Promise, + novelName: string, +): Promise; +async function normaliseAsyncChapters( + raw: Promise, + novelName?: string, +): Promise; +async function normaliseAsyncChapters< + T extends Array | null, +>(raw: Promise, novelName?: string): Promise; +async function normaliseAsyncChapters< + T extends Array | null, +>( + raw: Promise, + novelName?: string, +): Promise | null> { + return raw.then(c => normaliseChapters(c, novelName)); +} + +function normaliseChapter( + raw: T, + novelName: string, +): T; +function normaliseChapter( + raw: T, + novelName?: string, +): T; +function normaliseChapter( + raw: T, + novelName?: string, +): T; +function normaliseChapter( + raw: T, + novelName: string = '', +): ChapterInfo | DownloadedChapter | null { + if (!raw) return raw; + return { + ...raw, + releaseTime: dayjs(raw.releaseTime).isValid() + ? dayjs(raw.releaseTime).format('LL') + : raw.releaseTime, + chapterNumber: + raw.chapterNumber ?? + // @ts-ignore + parseChapterNumber(novelName ?? raw.novelName, raw.name), + }; +} +async function normaliseAsyncChapter( + raw: Promise, + novelName: string, +): Promise; +async function normaliseAsyncChapter( + raw: Promise, + novelName?: string, +): Promise; +async function normaliseAsyncChapter< + T extends ChapterInfo | DownloadedChapter | null, +>(raw: Promise, novelName?: string): Promise; +async function normaliseAsyncChapter< + T extends ChapterInfo | DownloadedChapter | null, +>( + raw: Promise, + novelName?: string, +): Promise { + const c = await raw; + if (!c) return c; + return normaliseChapter(c, novelName); +} + +export { + normaliseChapters, + normaliseChapter, + normaliseAsyncChapters, + normaliseAsyncChapter, +}; From 2bfe2bbc4d16bbd176f6bc78fc186be4b376e796 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:16:04 +0200 Subject: [PATCH 11/18] useTheme Context --- App.tsx | 23 +++--- babel.config.js | 1 + src/components/Actionbar/Actionbar.tsx | 2 +- .../AppErrorBoundary/AppErrorBoundary.tsx | 2 +- src/components/Button/Button.tsx | 2 +- src/components/EmptyView.tsx | 2 +- .../ErrorScreenV2/ErrorScreenV2.tsx | 2 +- src/components/Modal/Modal.tsx | 2 +- src/components/NewUpdateDialog.tsx | 2 +- src/components/Skeleton/Skeleton.tsx | 3 +- src/components/Switch/Switch.tsx | 2 +- src/hooks/common/useFullscreenMode.ts | 2 +- src/hooks/persisted/index.ts | 1 - src/navigators/BottomNavigator.tsx | 3 +- src/navigators/Main.tsx | 3 +- .../ThemeProvider.tsx} | 18 ++++- .../BrowseSourceScreen/BrowseSourceScreen.tsx | 2 +- .../components/FilterBottomSheet.tsx | 2 +- src/screens/Categories/CategoriesScreen.tsx | 2 +- .../components/AddCategoryModal.tsx | 2 +- .../Categories/components/CategoryCard.tsx | 2 +- .../components/DeleteCategoryModal.tsx | 2 +- .../GlobalSearchScreen/GlobalSearchScreen.tsx | 2 +- .../components/GlobalSearchResultsList.tsx | 2 +- src/screens/StatsScreen/StatsScreen.tsx | 2 +- src/screens/WebviewScreen/WebviewScreen.tsx | 2 +- src/screens/browse/BrowseScreen.tsx | 3 +- src/screens/browse/SourceNovels.tsx | 2 +- .../components/Modals/SourceSettings.tsx | 2 +- .../browse/discover/AniListTopNovels.tsx | 3 +- src/screens/browse/discover/MalTopNovels.tsx | 2 +- src/screens/browse/migration/Migration.tsx | 3 +- .../browse/migration/MigrationNovels.tsx | 3 +- .../browse/settings/BrowseSettings.tsx | 7 +- src/screens/history/HistoryScreen.tsx | 3 +- .../components/HistoryCard/HistoryCard.tsx | 2 +- src/screens/library/LibraryScreen.tsx | 3 +- .../LibraryBottomSheet/LibraryBottomSheet.tsx | 3 +- .../library/components/LibraryListView.tsx | 2 +- src/screens/more/About.tsx | 2 +- src/screens/more/DownloadsScreen.tsx | 2 +- src/screens/more/MoreScreen.tsx | 3 +- src/screens/more/TaskQueueScreen.tsx | 2 +- src/screens/novel/NovelScreen.tsx | 3 +- .../components/ChooseEpubLocationModal.tsx | 3 +- .../components/DownloadCustomChapterModal.tsx | 2 +- .../novel/components/Info/NovelInfoHeader.tsx | 2 +- .../novel/components/JumpToChapterModal.tsx | 2 +- .../novel/components/NovelScreenList.tsx | 2 +- .../novel/components/SetCategoriesModal.tsx | 2 +- src/screens/onboarding/OnboardingScreen.tsx | 2 +- src/screens/onboarding/PickThemeStep.tsx | 2 +- src/screens/reader/ReaderScreen.tsx | 3 +- .../reader/components/ChapterDrawer/index.tsx | 2 +- .../ReaderBottomSheet/ReaderBottomSheet.tsx | 3 +- .../ReaderBottomSheet/ReaderFontPicker.tsx | 3 +- .../ReaderTextAlignSelector.tsx | 3 +- .../ReaderBottomSheet/ReaderThemeSelector.tsx | 3 +- .../ReaderBottomSheet/ReaderValueChange.tsx | 3 +- .../ReaderBottomSheet/TextSizeSlider.tsx | 3 +- .../reader/components/ReaderFooter.tsx | 2 +- .../reader/components/WebViewReader.tsx | 2 +- .../settings/SettingsAdvancedScreen.tsx | 3 +- .../settings/SettingsAppearanceScreen.tsx | 3 +- .../settings/SettingsBackupScreen/index.tsx | 2 +- .../SettingsGeneralScreen.tsx | 2 +- .../DefaultCategoryDialog.tsx | 2 +- .../SettingsLibraryScreen.tsx | 3 +- .../Modals/CustomFileModal.tsx | 2 +- .../Modals/FontPickerModal.tsx | 3 +- .../Modals/VoicePickerModal.tsx | 3 +- .../SettingsReaderScreen/ReaderTextSize.tsx | 3 +- .../Settings/CustomCSSSettings.tsx | 3 +- .../Settings/CustomJSSettings.tsx | 3 +- .../Settings/DisplaySettings.tsx | 3 +- .../Settings/GeneralSettings.tsx | 3 +- .../Settings/ReaderThemeSettings.tsx | 3 +- .../Settings/TextToSpeechSettings.tsx | 2 +- .../SettingsReaderScreen.tsx | 2 +- .../SettingsRepositoryScreen.tsx | 3 +- .../components/AddRepositoryModal.tsx | 2 +- .../components/DeleteRepositoryModal.tsx | 2 +- .../components/RepositoryCard.tsx | 2 +- src/screens/settings/SettingsScreen.tsx | 2 +- .../settings/SettingsTrackerScreen.tsx | 3 +- src/screens/updates/UpdatesScreen.tsx | 2 +- .../updates/components/UpdateNovelCard.tsx | 3 +- tsconfig.json | 73 +++++-------------- 88 files changed, 166 insertions(+), 155 deletions(-) rename src/{hooks/persisted/useTheme.ts => providers/ThemeProvider.tsx} (79%) diff --git a/App.tsx b/App.tsx index 90d1aa6427..9436e2bba6 100644 --- a/App.tsx +++ b/App.tsx @@ -16,6 +16,7 @@ import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; import Main from './src/navigators/Main'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; +import { ThemeContextProvider } from './src/providers/ThemeProvider'; Notifications.setNotificationHandler({ handleNotification: async () => { @@ -33,16 +34,18 @@ LottieSplashScreen.hide(); const App = () => { return ( - - - - - -
- - - - + + + + + + +
+ + + + + ); }; diff --git a/babel.config.js b/babel.config.js index 21c9d0ba65..877121b8ad 100644 --- a/babel.config.js +++ b/babel.config.js @@ -20,6 +20,7 @@ module.exports = function (api) { '@strings': './strings', '@services': './src/services', '@plugins': './src/plugins', + '@providers': './src/providers', '@utils': './src/utils', '@theme': './src/theme', '@navigators': './src/navigators', diff --git a/src/components/Actionbar/Actionbar.tsx b/src/components/Actionbar/Actionbar.tsx index 5086004e23..c168822553 100644 --- a/src/components/Actionbar/Actionbar.tsx +++ b/src/components/Actionbar/Actionbar.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import React from 'react'; import { Dimensions, diff --git a/src/components/AppErrorBoundary/AppErrorBoundary.tsx b/src/components/AppErrorBoundary/AppErrorBoundary.tsx index 95e3f40722..f055a5ee64 100644 --- a/src/components/AppErrorBoundary/AppErrorBoundary.tsx +++ b/src/components/AppErrorBoundary/AppErrorBoundary.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View, Text, StatusBar } from 'react-native'; import ErrorBoundary from 'react-native-error-boundary'; import { Button, List } from '@components'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { SafeAreaView } from 'react-native-safe-area-context'; interface ErrorFallbackProps { diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index c531c1cb46..487975ead5 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -4,7 +4,7 @@ import { ButtonProps as PaperButtonProps, } from 'react-native-paper'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { ThemeProp } from 'react-native-paper/lib/typescript/types'; interface ButtonProps extends Partial { diff --git a/src/components/EmptyView.tsx b/src/components/EmptyView.tsx index 99af6b1276..d69d9b3bf7 100644 --- a/src/components/EmptyView.tsx +++ b/src/components/EmptyView.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; diff --git a/src/components/ErrorScreenV2/ErrorScreenV2.tsx b/src/components/ErrorScreenV2/ErrorScreenV2.tsx index b4e89e5bff..03b6d5499c 100644 --- a/src/components/ErrorScreenV2/ErrorScreenV2.tsx +++ b/src/components/ErrorScreenV2/ErrorScreenV2.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getErrorMessage } from '@utils/error'; import { MaterialDesignIconName } from '@type/icon'; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 2bbce938e1..17ed723667 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,5 +1,5 @@ import SafeAreaView from '@components/SafeAreaView/SafeAreaView'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import React from 'react'; import { StyleSheet } from 'react-native'; import { ModalProps, overlay, Modal as PaperModal } from 'react-native-paper'; diff --git a/src/components/NewUpdateDialog.tsx b/src/components/NewUpdateDialog.tsx index a865510900..ca83cceca0 100644 --- a/src/components/NewUpdateDialog.tsx +++ b/src/components/NewUpdateDialog.tsx @@ -6,7 +6,7 @@ import { ScrollView } from 'react-native-gesture-handler'; import Button from './Button/Button'; import { getString } from '@strings/translations'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Modal } from '@components'; interface NewUpdateDialogProps { diff --git a/src/components/Skeleton/Skeleton.tsx b/src/components/Skeleton/Skeleton.tsx index acc9194941..9d70eb7f25 100644 --- a/src/components/Skeleton/Skeleton.tsx +++ b/src/components/Skeleton/Skeleton.tsx @@ -1,4 +1,5 @@ -import { useAppSettings, useTheme } from '@hooks/persisted'; +import { useAppSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import * as React from 'react'; import { StyleProp, ViewStyle, StyleSheet, View } from 'react-native'; import Animated, { diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx index 25b742d8bc..ec967d3684 100644 --- a/src/components/Switch/Switch.tsx +++ b/src/components/Switch/Switch.tsx @@ -8,7 +8,7 @@ import Animated, { withTiming, useDerivedValue, } from 'react-native-reanimated'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; interface SwitchProps { value: boolean; diff --git a/src/hooks/common/useFullscreenMode.ts b/src/hooks/common/useFullscreenMode.ts index ed3f45c48d..0ade5820a7 100644 --- a/src/hooks/common/useFullscreenMode.ts +++ b/src/hooks/common/useFullscreenMode.ts @@ -4,8 +4,8 @@ import { useNavigation } from '@react-navigation/native'; import { useChapterGeneralSettings, useChapterReaderSettings, - useTheme, } from '../persisted'; +import { useTheme } from '@providers/ThemeProvider'; import Color from 'color'; import * as NavigationBar from 'expo-navigation-bar'; import { diff --git a/src/hooks/persisted/index.ts b/src/hooks/persisted/index.ts index dcbe2ad728..f9dbe2fcfb 100644 --- a/src/hooks/persisted/index.ts +++ b/src/hooks/persisted/index.ts @@ -1,4 +1,3 @@ -export { useTheme } from './useTheme'; export { useUpdates, useLastUpdate } from './useUpdates'; export { default as useCategories } from './useCategories'; export { default as useHistory } from './useHistory'; diff --git a/src/navigators/BottomNavigator.tsx b/src/navigators/BottomNavigator.tsx index eee21c02c7..9137fa104b 100644 --- a/src/navigators/BottomNavigator.tsx +++ b/src/navigators/BottomNavigator.tsx @@ -11,7 +11,8 @@ import Browse from '../screens/browse/BrowseScreen'; import More from '../screens/more/MoreScreen'; import { getString } from '@strings/translations'; -import { useAppSettings, usePlugins, useTheme } from '@hooks/persisted'; +import { useAppSettings, usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { BottomNavigatorParamList } from './types'; import Icon from '@react-native-vector-icons/material-design-icons'; import { MaterialDesignIconName } from '@type/icon'; diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx index ed08f5b2f6..5f1b2059e4 100644 --- a/src/navigators/Main.tsx +++ b/src/navigators/Main.tsx @@ -7,7 +7,8 @@ import { changeNavigationBarColor, setStatusBarColor, } from '@theme/utils/setBarColor'; -import { useAppSettings, usePlugins, useTheme } from '@hooks/persisted'; +import { useAppSettings, usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useGithubUpdateChecker } from '@hooks/common/githubUpdateChecker'; /** diff --git a/src/hooks/persisted/useTheme.ts b/src/providers/ThemeProvider.tsx similarity index 79% rename from src/hooks/persisted/useTheme.ts rename to src/providers/ThemeProvider.tsx index 684de33934..3b4f0695f0 100644 --- a/src/hooks/persisted/useTheme.ts +++ b/src/providers/ThemeProvider.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import React, { createContext, useContext, useMemo } from 'react'; import { Appearance } from 'react-native'; import { useMMKVBoolean, @@ -18,7 +18,13 @@ const getElevationColor = (colors: ThemeColors, elevation: number) => { .string(); }; -export const useTheme = (): ThemeColors => { +const ThemeContext = createContext(null); + +export function ThemeContextProvider({ + children, +}: { + children: React.JSX.Element; +}) { const [appTheme] = useMMKVObject('APP_THEME'); const [isAmoledBlack] = useMMKVBoolean('AMOLED_BLACK'); const [customAccent] = useMMKVString('CUSTOM_ACCENT_COLOR'); @@ -57,5 +63,11 @@ export const useTheme = (): ThemeColors => { return colors; }, [appTheme, isAmoledBlack, customAccent]); - return theme; + return ( + {children} + ); +} + +export const useTheme = () => { + return useContext(ThemeContext)!; }; diff --git a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx index cd36504fb9..657b3af98f 100644 --- a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx +++ b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx @@ -8,7 +8,7 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import FilterBottomSheet from './components/FilterBottomSheet'; import { useSearch } from '@hooks'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useBrowseSource, useSearchSource } from './useBrowseSource'; import { NovelItem } from '@plugins/types'; diff --git a/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx b/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx index 6720e1aff0..42d795669f 100644 --- a/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx +++ b/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx @@ -14,7 +14,7 @@ import { BottomSheetView, } from '@gorhom/bottom-sheet'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { FilterTypes, FilterToValues, diff --git a/src/screens/Categories/CategoriesScreen.tsx b/src/screens/Categories/CategoriesScreen.tsx index b6d0411eb3..e224f51f94 100644 --- a/src/screens/Categories/CategoriesScreen.tsx +++ b/src/screens/Categories/CategoriesScreen.tsx @@ -8,7 +8,7 @@ import AddCategoryModal from './components/AddCategoryModal'; import { updateCategoryOrderInDb } from '@database/queries/CategoryQueries'; import { useBoolean } from '@hooks'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import CategoryCard from './components/CategoryCard'; diff --git a/src/screens/Categories/components/AddCategoryModal.tsx b/src/screens/Categories/components/AddCategoryModal.tsx index 05d3f7a952..4474b141a1 100644 --- a/src/screens/Categories/components/AddCategoryModal.tsx +++ b/src/screens/Categories/components/AddCategoryModal.tsx @@ -10,7 +10,7 @@ import { isCategoryNameDuplicate, updateCategory, } from '../../../database/queries/CategoryQueries'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; diff --git a/src/screens/Categories/components/CategoryCard.tsx b/src/screens/Categories/components/CategoryCard.tsx index bf8bb017cc..fc2bd96a01 100644 --- a/src/screens/Categories/components/CategoryCard.tsx +++ b/src/screens/Categories/components/CategoryCard.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Text, View } from 'react-native'; import React from 'react'; import { Category } from '@database/types'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import AddCategoryModal from './AddCategoryModal'; import { useBoolean } from '@hooks'; import { overlay, Portal } from 'react-native-paper'; diff --git a/src/screens/Categories/components/DeleteCategoryModal.tsx b/src/screens/Categories/components/DeleteCategoryModal.tsx index d6a61ca713..42b476f13c 100644 --- a/src/screens/Categories/components/DeleteCategoryModal.tsx +++ b/src/screens/Categories/components/DeleteCategoryModal.tsx @@ -6,7 +6,7 @@ import { Button, Modal } from '@components/index'; import { Category } from '@database/types'; import { deleteCategoryById } from '@database/queries/CategoryQueries'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; diff --git a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx index 042b3b32b0..18404a1933 100644 --- a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx +++ b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx @@ -6,7 +6,7 @@ import { EmptyView, SafeAreaView, SearchbarV2 } from '@components/index'; import GlobalSearchResultsList from './components/GlobalSearchResultsList'; import { useSearch } from '@hooks'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { useGlobalSearch } from './hooks/useGlobalSearch'; diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx index 2fb96afb24..5039d2a280 100644 --- a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx @@ -7,7 +7,7 @@ import { useNavigation } from '@react-navigation/native'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import { getString } from '@strings/translations'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { GlobalSearchResult } from '../hooks/useGlobalSearch'; import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading'; diff --git a/src/screens/StatsScreen/StatsScreen.tsx b/src/screens/StatsScreen/StatsScreen.tsx index ec3eab0bf6..f1e297feae 100644 --- a/src/screens/StatsScreen/StatsScreen.tsx +++ b/src/screens/StatsScreen/StatsScreen.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { diff --git a/src/screens/WebviewScreen/WebviewScreen.tsx b/src/screens/WebviewScreen/WebviewScreen.tsx index e2e887b533..1b8aaebd07 100644 --- a/src/screens/WebviewScreen/WebviewScreen.tsx +++ b/src/screens/WebviewScreen/WebviewScreen.tsx @@ -5,7 +5,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { getPlugin } from '@plugins/pluginManager'; import { useBackHandler } from '@hooks'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { WebviewScreenProps } from '@navigators/types'; import { getUserAgent } from '@hooks/persisted/useUserAgent'; import { resolveUrl } from '@services/plugin/fetch'; diff --git a/src/screens/browse/BrowseScreen.tsx b/src/screens/browse/BrowseScreen.tsx index 4f80881fe4..bab53b9e84 100644 --- a/src/screens/browse/BrowseScreen.tsx +++ b/src/screens/browse/BrowseScreen.tsx @@ -3,7 +3,8 @@ import React, { useEffect, useMemo } from 'react'; import { TabView, TabBar } from 'react-native-tab-view'; import { useSearch } from '@hooks'; -import { usePlugins, useTheme } from '@hooks/persisted'; +import { usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { EmptyView, SafeAreaView, SearchbarV2 } from '@components'; diff --git a/src/screens/browse/SourceNovels.tsx b/src/screens/browse/SourceNovels.tsx index eb3036edb8..b9d3fc2f9a 100644 --- a/src/screens/browse/SourceNovels.tsx +++ b/src/screens/browse/SourceNovels.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { StyleSheet, View, FlatList, Text, FlatListProps } from 'react-native'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import ListView from '../../components/ListView'; import { Appbar } from '@components'; diff --git a/src/screens/browse/components/Modals/SourceSettings.tsx b/src/screens/browse/components/Modals/SourceSettings.tsx index 1b7a2db186..727e791715 100644 --- a/src/screens/browse/components/Modals/SourceSettings.tsx +++ b/src/screens/browse/components/Modals/SourceSettings.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { TextInput } from 'react-native-paper'; import { Button, Modal, SwitchItem } from '@components/index'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { Storage } from '@plugins/helpers/storage'; diff --git a/src/screens/browse/discover/AniListTopNovels.tsx b/src/screens/browse/discover/AniListTopNovels.tsx index 71d629f3b0..b6f1fd0d85 100644 --- a/src/screens/browse/discover/AniListTopNovels.tsx +++ b/src/screens/browse/discover/AniListTopNovels.tsx @@ -14,7 +14,8 @@ import { SafeAreaView, SearchbarV2 } from '@components'; import { showToast } from '@utils/showToast'; import TrackerNovelCard from './TrackerNovelCard'; -import { useTheme, useTracker } from '@hooks/persisted'; +import { useTracker } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import TrackerLoading from '../loadingAnimation/TrackerLoading'; import { queryAniList } from '@services/Trackers/aniList'; import localeData from 'dayjs/plugin/localeData'; diff --git a/src/screens/browse/discover/MalTopNovels.tsx b/src/screens/browse/discover/MalTopNovels.tsx index 6fdc19aa8c..12040a087b 100644 --- a/src/screens/browse/discover/MalTopNovels.tsx +++ b/src/screens/browse/discover/MalTopNovels.tsx @@ -16,7 +16,7 @@ import { SafeAreaView, SearchbarV2 } from '@components'; import { showToast } from '@utils/showToast'; import { scrapeSearchResults, scrapeTopNovels } from './MyAnimeListScraper'; import MalNovelCard from './TrackerNovelCard'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import MalLoading from '../loadingAnimation/MalLoading'; import { BrowseMalScreenProps } from '@navigators/types'; diff --git a/src/screens/browse/migration/Migration.tsx b/src/screens/browse/migration/Migration.tsx index 80f66edce4..c8b6fffe00 100644 --- a/src/screens/browse/migration/Migration.tsx +++ b/src/screens/browse/migration/Migration.tsx @@ -3,7 +3,8 @@ import { StyleSheet, View, FlatList, Text, FlatListProps } from 'react-native'; import MigrationSourceItem from './MigrationSourceItem'; -import { usePlugins, useTheme } from '@hooks/persisted'; +import { usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useLibraryNovels } from '@screens/library/hooks/useLibrary'; import { Appbar } from '@components'; import { MigrationScreenProps } from '@navigators/types'; diff --git a/src/screens/browse/migration/MigrationNovels.tsx b/src/screens/browse/migration/MigrationNovels.tsx index 75d06d9642..7c9b7f9890 100644 --- a/src/screens/browse/migration/MigrationNovels.tsx +++ b/src/screens/browse/migration/MigrationNovels.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { View, Text, FlatList, StyleSheet, FlatListProps } from 'react-native'; import { ProgressBar } from 'react-native-paper'; -import { usePlugins, useTheme } from '@hooks/persisted'; +import { usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import EmptyView from '@components/EmptyView'; import MigrationNovelList from './MigrationNovelList'; diff --git a/src/screens/browse/settings/BrowseSettings.tsx b/src/screens/browse/settings/BrowseSettings.tsx index d0aecd79a0..929b2c951f 100644 --- a/src/screens/browse/settings/BrowseSettings.tsx +++ b/src/screens/browse/settings/BrowseSettings.tsx @@ -2,11 +2,8 @@ import { FlatList, StyleSheet } from 'react-native'; import React from 'react'; import { Appbar, List, SwitchItem } from '@components'; -import { - useBrowseSettings, - usePlugins, - useTheme, -} from '@hooks/persisted/index'; +import { useBrowseSettings, usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { getLocaleLanguageName, languages } from '@utils/constants/languages'; import { BrowseSettingsScreenProp } from '@navigators/types/index'; diff --git a/src/screens/history/HistoryScreen.tsx b/src/screens/history/HistoryScreen.tsx index dfd569e688..d87543e7db 100644 --- a/src/screens/history/HistoryScreen.tsx +++ b/src/screens/history/HistoryScreen.tsx @@ -12,7 +12,8 @@ import { import HistoryCard from './components/HistoryCard/HistoryCard'; import { useSearch, useBoolean } from '@hooks'; -import { useTheme, useHistory } from '@hooks/persisted'; +import { useHistory } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { convertDateToISOString } from '@database/utils/convertDateToISOString'; diff --git a/src/screens/history/components/HistoryCard/HistoryCard.tsx b/src/screens/history/components/HistoryCard/HistoryCard.tsx index 048be3e3bf..d9ce613072 100644 --- a/src/screens/history/components/HistoryCard/HistoryCard.tsx +++ b/src/screens/history/components/HistoryCard/HistoryCard.tsx @@ -8,7 +8,7 @@ import { IconButtonV2 } from '@components'; import { defaultCover } from '@plugins/helpers/constants'; import { getString } from '@strings/translations'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { History, NovelInfo } from '@database/types'; import { HistoryScreenProps } from '@navigators/types'; diff --git a/src/screens/library/LibraryScreen.tsx b/src/screens/library/LibraryScreen.tsx index 14ca73cf70..de4c66713e 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -28,7 +28,8 @@ import LibraryBottomSheet from './components/LibraryBottomSheet/LibraryBottomShe import { Banner } from './components/Banner'; import { Actionbar } from '@components/Actionbar/Actionbar'; -import { useAppSettings, useHistory, useTheme } from '@hooks/persisted'; +import { useAppSettings, useHistory } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useSearch, useBackHandler, useBoolean } from '@hooks'; import { getString } from '@strings/translations'; import { FAB, Portal } from 'react-native-paper'; diff --git a/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx b/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx index c624771395..3d1b8e7096 100644 --- a/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx +++ b/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx @@ -15,7 +15,8 @@ import { } from 'react-native-tab-view'; import color from 'color'; -import { useLibrarySettings, useTheme } from '@hooks/persisted'; +import { useLibrarySettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { Checkbox, SortItem } from '@components/Checkbox/Checkbox'; import { diff --git a/src/screens/library/components/LibraryListView.tsx b/src/screens/library/components/LibraryListView.tsx index 0d80caaa2c..f84883e77b 100644 --- a/src/screens/library/components/LibraryListView.tsx +++ b/src/screens/library/components/LibraryListView.tsx @@ -9,7 +9,7 @@ import NovelList, { NovelListRenderItem } from '@components/NovelList'; import { NovelInfo } from '@database/types'; import { getString } from '@strings/translations'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { LibraryScreenProps } from '@navigators/types'; import ServiceManager from '@services/ServiceManager'; diff --git a/src/screens/more/About.tsx b/src/screens/more/About.tsx index 6cdc263c56..84e30a164a 100644 --- a/src/screens/more/About.tsx +++ b/src/screens/more/About.tsx @@ -5,7 +5,7 @@ import * as Linking from 'expo-linking'; import { getString } from '@strings/translations'; import { MoreHeader } from './components/MoreHeader'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { List, SafeAreaView } from '@components'; import { AboutScreenProps } from '@navigators/types'; import { GIT_HASH, RELEASE_DATE, BUILD_TYPE } from '@env'; diff --git a/src/screens/more/DownloadsScreen.tsx b/src/screens/more/DownloadsScreen.tsx index a5dc597fcb..ba87a019bc 100644 --- a/src/screens/more/DownloadsScreen.tsx +++ b/src/screens/more/DownloadsScreen.tsx @@ -11,7 +11,7 @@ import { getDownloadedChapters, } from '@database/queries/ChapterQueries'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import RemoveDownloadsDialog from './components/RemoveDownloadsDialog'; import UpdatesSkeletonLoading from '@screens/updates/components/UpdatesSkeletonLoading'; diff --git a/src/screens/more/MoreScreen.tsx b/src/screens/more/MoreScreen.tsx index c429b73244..991066aada 100644 --- a/src/screens/more/MoreScreen.tsx +++ b/src/screens/more/MoreScreen.tsx @@ -5,7 +5,8 @@ import { getString } from '@strings/translations'; import { List, SafeAreaView } from '@components'; import { MoreHeader } from './components/MoreHeader'; -import { useLibrarySettings, useTheme } from '@hooks/persisted'; +import { useLibrarySettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { MoreStackScreenProps } from '@navigators/types'; import Switch from '@components/Switch/Switch'; import { useMMKVObject } from 'react-native-mmkv'; diff --git a/src/screens/more/TaskQueueScreen.tsx b/src/screens/more/TaskQueueScreen.tsx index 030dbff945..8b856bba11 100644 --- a/src/screens/more/TaskQueueScreen.tsx +++ b/src/screens/more/TaskQueueScreen.tsx @@ -8,7 +8,7 @@ import { overlay, } from 'react-native-paper'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { showToast } from '../../utils/showToast'; import { getString } from '@strings/translations'; diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index 44d036a0e7..bae51233fd 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -8,7 +8,8 @@ import Animated, { } from 'react-native-reanimated'; import { Portal, Appbar, Snackbar } from 'react-native-paper'; -import { useDownload, useTheme } from '@hooks/persisted'; +import { useDownload } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import JumpToChapterModal from './components/JumpToChapterModal'; import { Actionbar } from '../../components/Actionbar/Actionbar'; import EditInfoModal from './components/EditInfoModal'; diff --git a/src/screens/novel/components/ChooseEpubLocationModal.tsx b/src/screens/novel/components/ChooseEpubLocationModal.tsx index 1dca5cf4e9..be5a06ef45 100644 --- a/src/screens/novel/components/ChooseEpubLocationModal.tsx +++ b/src/screens/novel/components/ChooseEpubLocationModal.tsx @@ -7,7 +7,8 @@ import { Button, List, Modal, SwitchItem } from '@components'; import { useBoolean } from '@hooks'; import { getString } from '@strings/translations'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { showToast } from '@utils/showToast'; interface ChooseEpubLocationModalProps { diff --git a/src/screens/novel/components/DownloadCustomChapterModal.tsx b/src/screens/novel/components/DownloadCustomChapterModal.tsx index 05cfac9932..e2ba939495 100644 --- a/src/screens/novel/components/DownloadCustomChapterModal.tsx +++ b/src/screens/novel/components/DownloadCustomChapterModal.tsx @@ -5,7 +5,7 @@ import { Button, IconButton, Portal } from 'react-native-paper'; import { ChapterInfo, NovelInfo } from '@database/types'; import { getString } from '@strings/translations'; import { Modal } from '@components'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useNovelChapters, useNovelState } from '@hooks/persisted/index'; interface DownloadCustomChapterModalProps { diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 47f66a677e..4bc5540d93 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -29,11 +29,11 @@ import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/typ import { UseBooleanReturnType } from '@hooks'; import { useAppSettings, - useTheme, useNovelChapters, useNovelPages, useNovelState, } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { NovelStatus, PluginItem } from '@plugins/types'; import { translateNovelStatus } from '@utils/translateEnum'; import { getMMKVObject } from '@utils/mmkv/mmkv'; diff --git a/src/screens/novel/components/JumpToChapterModal.tsx b/src/screens/novel/components/JumpToChapterModal.tsx index 954f34d8aa..ed4f029814 100644 --- a/src/screens/novel/components/JumpToChapterModal.tsx +++ b/src/screens/novel/components/JumpToChapterModal.tsx @@ -9,7 +9,7 @@ import { getString } from '@strings/translations'; import { Button, Modal, SwitchItem } from '@components'; import { Portal, Text } from 'react-native-paper'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { ChapterInfo } from '@database/types'; import { NovelScreenProps } from '@navigators/types'; import { FlashList, ListRenderItem } from '@shopify/flash-list'; diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index 261f21660c..4e5a17dc84 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -7,7 +7,6 @@ import { useBoolean } from '@hooks/index'; import { useAppSettings, useDownload, - useTheme, useNovelChapters, useNovelSettings, useNovelState, @@ -34,6 +33,7 @@ import { ChapterListSkeleton } from '@components/Skeleton/Skeleton'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { FlashList } from '@shopify/flash-list'; import useNovelLastRead from '@hooks/persisted/novel/useNovelLastRead'; +import { useTheme } from '@providers/ThemeProvider'; type NovelScreenListProps = { headerOpacity: SharedValue; diff --git a/src/screens/novel/components/SetCategoriesModal.tsx b/src/screens/novel/components/SetCategoriesModal.tsx index 9bdafb8fe0..2c5bd7fd21 100644 --- a/src/screens/novel/components/SetCategoriesModal.tsx +++ b/src/screens/novel/components/SetCategoriesModal.tsx @@ -5,7 +5,7 @@ import { NavigationProp, useNavigation } from '@react-navigation/native'; import { Button, Modal } from '@components/index'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { getCategoriesWithCount } from '@database/queries/CategoryQueries'; diff --git a/src/screens/onboarding/OnboardingScreen.tsx b/src/screens/onboarding/OnboardingScreen.tsx index 94447b4fee..3e7386579c 100644 --- a/src/screens/onboarding/OnboardingScreen.tsx +++ b/src/screens/onboarding/OnboardingScreen.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Text } from 'react-native-paper'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Image, StyleSheet, View } from 'react-native'; diff --git a/src/screens/onboarding/PickThemeStep.tsx b/src/screens/onboarding/PickThemeStep.tsx index 7202f44f73..4a0cbd1cef 100644 --- a/src/screens/onboarding/PickThemeStep.tsx +++ b/src/screens/onboarding/PickThemeStep.tsx @@ -3,7 +3,7 @@ import { View, Text, Pressable, StyleSheet, FlatList } from 'react-native'; import { ThemePicker } from '@components/ThemePicker/ThemePicker'; import { ThemeColors } from '@theme/types'; import { useMMKVObject } from 'react-native-mmkv'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { darkThemes, lightThemes } from '@theme/md3'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx index 1677e1ece9..068f7cf3d4 100644 --- a/src/screens/reader/ReaderScreen.tsx +++ b/src/screens/reader/ReaderScreen.tsx @@ -1,5 +1,6 @@ import React, { useRef, useCallback, useState, useEffect } from 'react'; -import { useChapterGeneralSettings, useTheme } from '@hooks/persisted'; +import { useChapterGeneralSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import ReaderAppbar from './components/ReaderAppbar'; import ReaderFooter from './components/ReaderFooter'; diff --git a/src/screens/reader/components/ChapterDrawer/index.tsx b/src/screens/reader/components/ChapterDrawer/index.tsx index 42fe76b041..05be2ba96f 100644 --- a/src/screens/reader/components/ChapterDrawer/index.tsx +++ b/src/screens/reader/components/ChapterDrawer/index.tsx @@ -11,8 +11,8 @@ import { useNovelChapters, useNovelPages, useNovelSettings, - useTheme, } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Button, LoadingScreenV2 } from '@components/index'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx index dd6e876212..b4d3b250e6 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx @@ -17,7 +17,8 @@ import Color from 'color'; import { BottomSheetFlashList, BottomSheetView } from '@gorhom/bottom-sheet'; import BottomSheet from '@components/BottomSheet/BottomSheet'; -import { useChapterGeneralSettings, useTheme } from '@hooks/persisted'; +import { useChapterGeneralSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { SceneMap, TabBar, TabView } from 'react-native-tab-view'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx index cc72df00b9..92aa70f7f7 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx @@ -4,7 +4,8 @@ import color from 'color'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import { getString } from '@strings/translations'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Font, readerFonts } from '@utils/constants/readerConstants'; import { FlatList } from 'react-native-gesture-handler'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx index cce2301ed5..68c74f8baf 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx @@ -1,7 +1,8 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { textAlignments } from '@utils/constants/readerConstants'; import { ToggleButton } from '@components/Common/ToggleButton'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx index 15cb1c0fab..4a68697824 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { ToggleColorButton } from '@components/Common/ToggleButton'; import { getString } from '@strings/translations'; import { presetReaderThemes } from '@utils/constants/readerConstants'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { FlatList } from 'react-native-gesture-handler'; import { ReaderTheme } from '@hooks/persisted/useSettings'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx index d3b4b9b925..5c58034b9c 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx @@ -1,7 +1,8 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { IconButtonV2 } from '@components'; import { ChapterReaderSettings } from '@hooks/persisted/useSettings'; diff --git a/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx b/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx index 73225c7b09..67d080dcc6 100644 --- a/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx @@ -1,7 +1,8 @@ import { StyleSheet, Text, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import Slider from '@react-native-community/slider'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderFooter.tsx b/src/screens/reader/components/ReaderFooter.tsx index b22bc3a235..47993066fe 100644 --- a/src/screens/reader/components/ReaderFooter.tsx +++ b/src/screens/reader/components/ReaderFooter.tsx @@ -11,7 +11,7 @@ import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/typ import { ChapterScreenProps } from '@navigators/types'; import { useChapterContext } from '../ChapterContext'; import { SCREEN_HEIGHT } from '@gorhom/bottom-sheet'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useHeightContext } from '@screens/novel/context/HeightsContext'; interface ChapterFooterProps { diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index 82d07d9ba9..361b734db6 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -3,7 +3,7 @@ import { NativeEventEmitter, NativeModules, StatusBar } from 'react-native'; import WebView from 'react-native-webview'; import color from 'color'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { getPlugin } from '@plugins/pluginManager'; diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx index 86d4f14bf6..e899bab25e 100644 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ b/src/screens/settings/SettingsAdvancedScreen.tsx @@ -2,7 +2,8 @@ import React, { useState } from 'react'; import { Portal, Text, TextInput } from 'react-native-paper'; -import { useTheme, useUserAgent } from '@hooks/persisted'; +import { useUserAgent } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsAppearanceScreen.tsx b/src/screens/settings/SettingsAppearanceScreen.tsx index 8d7202c4b8..6c534879fc 100644 --- a/src/screens/settings/SettingsAppearanceScreen.tsx +++ b/src/screens/settings/SettingsAppearanceScreen.tsx @@ -5,7 +5,8 @@ import { ThemePicker } from '@components/ThemePicker/ThemePicker'; import SettingSwitch from './components/SettingSwitch'; import ColorPickerModal from '@components/ColorPickerModal/ColorPickerModal'; -import { useAppSettings, useTheme } from '@hooks/persisted'; +import { useAppSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useMMKVBoolean, useMMKVObject, diff --git a/src/screens/settings/SettingsBackupScreen/index.tsx b/src/screens/settings/SettingsBackupScreen/index.tsx index ee882d1755..6a29f012fe 100644 --- a/src/screens/settings/SettingsBackupScreen/index.tsx +++ b/src/screens/settings/SettingsBackupScreen/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Appbar, List, SafeAreaView } from '@components'; import { useBoolean } from '@hooks'; import { BackupSettingsScreenProps } from '@navigators/types'; diff --git a/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx b/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx index 694a16993e..d41c222189 100644 --- a/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx +++ b/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx @@ -8,8 +8,8 @@ import { useAppSettings, useLastUpdate, useLibrarySettings, - useTheme, } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import DefaultChapterSortModal from '../components/DefaultChapterSortModal'; import { DisplayModes, diff --git a/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx b/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx index c239aa99fc..dbed69fa77 100644 --- a/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx +++ b/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx @@ -5,7 +5,7 @@ import { Button, Dialog } from 'react-native-paper'; import { RadioButton } from '@components'; import { getString } from '@strings/translations'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Category } from '@database/types'; diff --git a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx index 3f33d7b30b..beb01257b3 100644 --- a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx +++ b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { Appbar, List } from '@components'; import { getString } from '@strings/translations'; import { useBoolean } from '@hooks'; -import { useCategories, useTheme } from '@hooks/persisted'; +import { useCategories } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { useNavigation } from '@react-navigation/native'; import { Portal } from 'react-native-paper'; import DefaultCategoryDialog from './DefaultCategoryDialog'; diff --git a/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx index 40e9651ded..1f6dc0fa52 100644 --- a/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx +++ b/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx @@ -7,7 +7,7 @@ import * as DocumentPicker from 'expo-document-picker'; import { Button, Modal } from '@components/index'; import { showToast } from '@utils/showToast'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; interface CustomFileModal { diff --git a/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx index 65cf32ee4e..c9ba814bb0 100644 --- a/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx +++ b/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { Portal } from 'react-native-paper'; import { RadioButton } from '@components/RadioButton/RadioButton'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { readerFonts } from '@utils/constants/readerConstants'; import { Modal } from '@components'; diff --git a/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx index 5289c99cbb..a101044a53 100644 --- a/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx +++ b/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx @@ -3,7 +3,8 @@ import React, { useState } from 'react'; import { Portal, TextInput, ActivityIndicator } from 'react-native-paper'; import { RadioButton } from '@components/RadioButton/RadioButton'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Voice } from 'expo-speech'; import { FlashList } from '@shopify/flash-list'; import { Modal } from '@components'; diff --git a/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx b/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx index 9837fd6c8f..18873a7490 100644 --- a/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx +++ b/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx @@ -1,7 +1,8 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { IconButtonV2 } from '@components/index'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx index 33bd44b2b3..502ea8a3c8 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx @@ -5,7 +5,8 @@ import { Portal } from 'react-native-paper'; import { Button, List, ConfirmationDialog } from '@components'; import { useBoolean } from '@hooks'; -import { useTheme, useChapterReaderSettings } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import CustomFileModal from '../Modals/CustomFileModal'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx index 50a272f0e1..c05bd5bae8 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx @@ -5,7 +5,8 @@ import { Portal } from 'react-native-paper'; import { Button, List, ConfirmationDialog } from '@components/index'; import { useBoolean } from '@hooks'; -import { useTheme, useChapterReaderSettings } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import CustomFileModal from '../Modals/CustomFileModal'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx index ed83eb0cba..154415a7d2 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { List } from '@components/index'; -import { useChapterGeneralSettings, useTheme } from '@hooks/persisted'; +import { useChapterGeneralSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import SettingSwitch from '../../components/SettingSwitch'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx index e61d7a3686..7f3ff40a9c 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx @@ -11,7 +11,8 @@ import { defaultTo } from 'lodash-es'; import { Button, List } from '@components/index'; -import { useTheme, useChapterGeneralSettings } from '@hooks/persisted'; +import { useChapterGeneralSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import SettingSwitch from '../../components/SettingSwitch'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx index 5f65f85445..f810f41569 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { Button, ColorPreferenceItem, List } from '@components/index'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useChapterReaderSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import ReaderTextAlignSelector from '@screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector'; import ReaderTextSize from '../ReaderTextSize'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx index e9452fc71d..89665d444e 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx @@ -2,8 +2,8 @@ import { IconButtonV2, List } from '@components'; import { useChapterGeneralSettings, useChapterReaderSettings, - useTheme, } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import React, { useEffect, useState } from 'react'; import VoicePickerModal from '../Modals/VoicePickerModal'; import { useBoolean } from '@hooks'; diff --git a/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx b/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx index aa54ed6059..508a6de110 100644 --- a/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx +++ b/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx @@ -10,8 +10,8 @@ import { Appbar, List, SafeAreaView } from '@components/index'; import { useChapterGeneralSettings, useChapterReaderSettings, - useTheme, } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import GeneralSettings from './Settings/GeneralSettings'; diff --git a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx index daae136d7a..3a0a4bd54d 100644 --- a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx @@ -12,7 +12,8 @@ import { } from '@database/queries/RepositoryQueries'; import { Repository } from '@database/types'; import { useBackHandler, useBoolean } from '@hooks/index'; -import { usePlugins, useTheme } from '@hooks/persisted'; +import { usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import AddRepositoryModal from './components/AddRepositoryModal'; diff --git a/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx b/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx index cdb31a4300..a03ef992a1 100644 --- a/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx @@ -5,7 +5,7 @@ import { Portal, TextInput } from 'react-native-paper'; import { Button, Modal } from '@components/index'; import { Repository } from '@database/types'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx b/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx index f667dae8c0..9d70c35bb2 100644 --- a/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx @@ -6,7 +6,7 @@ import { Button, Modal } from '@components/index'; import { Repository } from '@database/types'; import { deleteRepositoryById } from '@database/queries/RepositoryQueries'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx b/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx index b87dbc05d2..8a539707dc 100644 --- a/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx @@ -7,7 +7,7 @@ import { IconButtonV2 } from '@components'; import { Repository } from '@database/types'; import { useBoolean } from '@hooks/index'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; import { Portal } from 'react-native-paper'; diff --git a/src/screens/settings/SettingsScreen.tsx b/src/screens/settings/SettingsScreen.tsx index 32cc50ff08..17c81fddd9 100644 --- a/src/screens/settings/SettingsScreen.tsx +++ b/src/screens/settings/SettingsScreen.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ScrollView, StyleSheet } from 'react-native'; import { Appbar, List, SafeAreaView } from '@components'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { SettingsScreenProps } from '@navigators/types'; diff --git a/src/screens/settings/SettingsTrackerScreen.tsx b/src/screens/settings/SettingsTrackerScreen.tsx index 7590f4b95c..b74c4560d9 100644 --- a/src/screens/settings/SettingsTrackerScreen.tsx +++ b/src/screens/settings/SettingsTrackerScreen.tsx @@ -2,7 +2,8 @@ import React, { useState } from 'react'; import { View, StyleSheet } from 'react-native'; import { Portal, Text, Button, Provider } from 'react-native-paper'; -import { getTracker, useTheme, useTracker } from '@hooks/persisted'; +import { getTracker, useTracker } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { Appbar, List, Modal, SafeAreaView } from '@components'; import { TrackerSettingsScreenProps } from '@navigators/types'; import { getString } from '@strings/translations'; diff --git a/src/screens/updates/UpdatesScreen.tsx b/src/screens/updates/UpdatesScreen.tsx index 3987ee20a8..a6c0b4b923 100644 --- a/src/screens/updates/UpdatesScreen.tsx +++ b/src/screens/updates/UpdatesScreen.tsx @@ -10,7 +10,7 @@ import { } from '@components'; import { useSearch } from '@hooks'; -import { useTheme } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { getString } from '@strings/translations'; import { ThemeColors } from '@theme/types'; import UpdatesSkeletonLoading from './components/UpdatesSkeletonLoading'; diff --git a/src/screens/updates/components/UpdateNovelCard.tsx b/src/screens/updates/components/UpdateNovelCard.tsx index 9ca926f49d..a9c1bd803a 100644 --- a/src/screens/updates/components/UpdateNovelCard.tsx +++ b/src/screens/updates/components/UpdateNovelCard.tsx @@ -11,7 +11,8 @@ import { import { List } from 'react-native-paper'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import ChapterItem from '@screens/novel/components/ChapterItem'; -import { useDownload, useTheme, useUpdates } from '@hooks/persisted'; +import { useDownload, useUpdates } from '@hooks/persisted'; +import { useTheme } from '@providers/ThemeProvider'; import { RootStackParamList } from '@navigators/types'; import { FlatList } from 'react-native-gesture-handler'; import { defaultCover } from '@plugins/helpers/constants'; diff --git a/tsconfig.json b/tsconfig.json index 67cd87287f..de42f2e180 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,61 +4,26 @@ "noUnusedLocals": true, "module": "ES2022", "paths": { - "@components": [ - "src/components/index" - ], - "@components/*": [ - "src/components/*" - ], - "@database/*": [ - "src/database/*" - ], - "@hooks/*": [ - "src/hooks/*" - ], - "@hooks": [ - "src/hooks/index" - ], - "@screens/*": [ - "src/screens/*" - ], - "@strings/*": [ - "strings/*" - ], - "@theme/*": [ - "src/theme/*" - ], - "@utils/*": [ - "src/utils/*" - ], - "@plugins/*": [ - "src/plugins/*" - ], - "@services/*": [ - "src/services/*" - ], - "@navigators/*": [ - "src/navigators/*" - ], - "@native/*": [ - "src/native/*" - ], - "@api/*": [ - "src/api/*" - ], - "@type/*": [ - "src/type/*" - ], - "@specs/*": [ - "specs/*" - ], + "@components": ["src/components/index"], + "@components/*": ["src/components/*"], + "@database/*": ["src/database/*"], + "@hooks/*": ["src/hooks/*"], + "@hooks": ["src/hooks/index"], + "@screens/*": ["src/screens/*"], + "@strings/*": ["strings/*"], + "@theme/*": ["src/theme/*"], + "@utils/*": ["src/utils/*"], + "@plugins/*": ["src/plugins/*"], + "@providers/*": ["src/providers/*"], + "@services/*": ["src/services/*"], + "@navigators/*": ["src/navigators/*"], + "@native/*": ["src/native/*"], + "@api/*": ["src/api/*"], + "@type/*": ["src/type/*"], + "@specs/*": ["specs/*"] }, "types": ["react-native"] }, - "exclude": [ - "node_modules", - "babel.config.js", - "metro.config.js" - ], + "exclude": ["node_modules", "babel.config.js", "metro.config.js"], "extends": "@react-native/typescript-config/tsconfig.json" -} \ No newline at end of file +} From f917752d3f02fca78d6af9628e7376a8b5f5eaa2 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Mon, 11 Aug 2025 21:42:16 +0200 Subject: [PATCH 12/18] improve ChapterItem --- package-lock.json | 11 - src/database/queries/ChapterQueries.ts | 2 +- src/hooks/persisted/novel/useNovelState.ts | 2 +- src/hooks/persisted/useDownload.ts | 62 ++--- src/hooks/persisted/useImport.ts | 24 +- .../Chapter/ChapterDownloadButtons.tsx | 238 ++++++++---------- src/screens/novel/components/ChapterItem.tsx | 141 ++++++++--- .../novel/components/NovelScreenList.tsx | 4 - 8 files changed, 260 insertions(+), 224 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8a8ad663b..0970e8a1b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8599,17 +8599,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/babel-plugin-react-compiler": { - "version": "19.0.0-beta-ebf51a3-20250411", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-ebf51a3-20250411.tgz", - "integrity": "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.26.0" - } - }, "node_modules/expo/node_modules/babel-preset-expo": { "version": "13.1.11", "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.1.11.tgz", diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index f1b1e03c27..7189152848 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -207,7 +207,7 @@ export const getNovelChapters = (novelId: number) => ), ); -export const getChapter = (chapterId: number, novelName: string) => +export const getChapter = (chapterId: number, novelName?: string) => normaliseAsyncChapter( db.getFirstAsync( 'SELECT * FROM Chapter WHERE id = ?', diff --git a/src/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts index 2b60fe441c..14814686c9 100644 --- a/src/hooks/persisted/novel/useNovelState.ts +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -19,7 +19,7 @@ type NovelState = { followNovel: () => void; setCustomNovelCover: () => Promise; saveNovelCover: () => Promise; - getNovel: () => void; + getNovel: () => Promise; } & ( | { novel: NovelInfo; loading: false } | { novel: RouteNovel; loading: true } diff --git a/src/hooks/persisted/useDownload.ts b/src/hooks/persisted/useDownload.ts index e4622b3a0c..0a3f529af3 100644 --- a/src/hooks/persisted/useDownload.ts +++ b/src/hooks/persisted/useDownload.ts @@ -4,7 +4,7 @@ import ServiceManager, { DownloadChapterTask, QueuedBackgroundTask, } from '@services/ServiceManager'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useMMKVObject } from 'react-native-mmkv'; export const DOWNLOAD_QUEUE = 'DOWNLOAD'; @@ -20,39 +20,45 @@ export default function useDownload() { [queue], ) as { task: DownloadChapterTask; meta: BackgroundTaskMetadata }[]; - const downloadChapter = (novel: NovelInfo, chapter: ChapterInfo) => - ServiceManager.manager.addTask({ - name: 'DOWNLOAD_CHAPTER', - data: { - chapterId: chapter.id, - novelName: novel.name, - chapterName: chapter.name, - }, - }); - const downloadChapters = (novel: NovelInfo, chapters: ChapterInfo[]) => - ServiceManager.manager.addTask( - chapters.map(chapter => ({ + const downloadChapter = useCallback( + (novel: NovelInfo, chapter: ChapterInfo) => + ServiceManager.manager.addTask({ name: 'DOWNLOAD_CHAPTER', data: { chapterId: chapter.id, novelName: novel.name, chapterName: chapter.name, }, - })), - ); - const resumeDowndload = () => ServiceManager.manager.resume(); - - const pauseDownload = () => ServiceManager.manager.pause(); + }), + [], + ); + const downloadChapters = useCallback( + (novel: NovelInfo, chapters: ChapterInfo[]) => + ServiceManager.manager.addTask( + chapters.map(chapter => ({ + name: 'DOWNLOAD_CHAPTER', + data: { + chapterId: chapter.id, + novelName: novel.name, + chapterName: chapter.name, + }, + })), + ), + [], + ); - const cancelDownload = () => - ServiceManager.manager.removeTasksByName('DOWNLOAD_CHAPTER'); + const hookContent = useMemo( + () => ({ + downloadQueue, + downloadChapter, + downloadChapters, + resumeDowndload: () => ServiceManager.manager.resume(), + pauseDownload: () => ServiceManager.manager.pause(), + cancelDownload: () => + ServiceManager.manager.removeTasksByName('DOWNLOAD_CHAPTER'), + }), + [downloadChapter, downloadChapters, downloadQueue], + ); - return { - downloadQueue, - resumeDowndload, - downloadChapter, - downloadChapters, - pauseDownload, - cancelDownload, - }; + return hookContent; } diff --git a/src/hooks/persisted/useImport.ts b/src/hooks/persisted/useImport.ts index 7f104d8e03..1aaec59693 100644 --- a/src/hooks/persisted/useImport.ts +++ b/src/hooks/persisted/useImport.ts @@ -30,18 +30,18 @@ export default function useImport() { })), ); }, []); - const resumeImport = () => ServiceManager.manager.resume(); - const pauseImport = () => ServiceManager.manager.pause(); - - const cancelImport = () => - ServiceManager.manager.removeTasksByName('IMPORT_EPUB'); + const hookContent = useMemo( + () => ({ + importQueue, + importNovel, + resumeImport: () => ServiceManager.manager.resume(), + pauseImport: () => ServiceManager.manager.pause(), + cancelImport: () => + ServiceManager.manager.removeTasksByName('IMPORT_EPUB'), + }), + [importNovel, importQueue], + ); - return { - importQueue, - importNovel, - resumeImport, - pauseImport, - cancelImport, - }; + return hookContent; } diff --git a/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx b/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx index bed6f9f59b..fea45fa6c6 100644 --- a/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx +++ b/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx @@ -1,18 +1,16 @@ -import React, { useEffect, useRef } from 'react'; -import { MD3ThemeType } from '@theme/types'; +import React, { memo, useMemo, useCallback, useState } from 'react'; import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'; - import { Menu, overlay } from 'react-native-paper'; -import { getString } from '@strings/translations'; -import { isChapterDownloaded } from '@database/queries/ChapterQueries'; -import { useBoolean } from '@hooks/index'; -import { IconButtonV2 } from '@components'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import Color from 'color'; +import { getString } from '@strings/translations'; +import type { MD3ThemeType } from '@theme/types'; +import { useTheme } from '@providers/ThemeProvider'; +import { IconButtonV2 } from '@components'; -interface DownloadButtonProps { +interface Props { chapterId: number; - isDownloaded: boolean; + isDownloaded: boolean | undefined; isDownloading?: boolean; theme: MD3ThemeType; deleteChapter: () => void; @@ -20,8 +18,7 @@ interface DownloadButtonProps { setChapterDownloaded?: (value: boolean) => void; } -export const DownloadButton: React.FC = ({ - chapterId, +const DownloadButtonControlled: React.FC = ({ isDownloaded, isDownloading, theme, @@ -29,133 +26,121 @@ export const DownloadButton: React.FC = ({ downloadChapter, setChapterDownloaded, }) => { - const [downloaded, setDownloaded] = React.useState( - isDownloaded, + // local menu state only + const [menuVisible, setMenuVisible] = useState(false); + + const rippleStyle = useMemo( + () => ({ color: Color(theme.primary).alpha(0.12).string() }), + [theme.primary], ); - const { - value: deleteChapterMenuVisible, - setTrue: showDeleteChapterMenu, - setFalse: hideDeleteChapterMenu, - } = useBoolean(); + const menuContentStyle = useMemo( + () => ({ backgroundColor: overlay(2, theme.surface) }), + [theme.surface], + ); + const menuTitleStyle = useMemo( + () => ({ color: theme.onSurface }), + [theme.onSurface], + ); + + const onDelete = useCallback(() => { + deleteChapter(); + setMenuVisible(false); + setChapterDownloaded?.(false); + }, [deleteChapter, setChapterDownloaded]); - const isFirstRender = useRef(true); + const onDownload = useCallback(() => { + downloadChapter(); + }, [downloadChapter]); - useEffect(() => { - if (isFirstRender.current) { - isFirstRender.current = false; - return; // Skip the first render as it leads to 'Maximum update depth exceeded.' error - } - if (!isDownloading) { - const isDownloadedValue = isChapterDownloaded(chapterId); - setDownloaded(isDownloadedValue); - setChapterDownloaded?.(isDownloadedValue); - } - }, [chapterId, isDownloading, setChapterDownloaded]); - if (isDownloading || downloaded === undefined) { - return ; + if (isDownloading || isDownloaded === undefined) { + return ( + + + + ); } - return downloaded ? ( - - } - contentStyle={{ backgroundColor: overlay(2, theme.surface) }} - > - { - deleteChapter(); - hideDeleteChapterMenu(); - setDownloaded(false); - }} - title={getString('common.delete')} - titleStyle={{ color: theme.onSurface }} - /> - - ) : ( - { - downloadChapter(); - setDownloaded(undefined); - }} - /> + + if (isDownloaded) { + return ( + setMenuVisible(false)} + anchor={ + + setMenuVisible(true)} + android_ripple={rippleStyle} + > + + + + } + contentStyle={menuContentStyle} + > + + + ); + } + + return ( + + + + + ); }; -interface theme { - theme: MD3ThemeType; +function areEqual(a: Props, b: Props) { + return ( + a.isDownloaded === b.isDownloaded && + a.isDownloading === b.isDownloading && + a.deleteChapter === b.deleteChapter && + a.downloadChapter === b.downloadChapter && + a.theme.primary === b.theme.primary && + a.theme.surface === b.theme.surface && + a.theme.outline === b.theme.outline + ); } -type buttonPropType = theme & { - onPress: () => void; -}; -export const ChapterDownloadingButton: React.FC = ({ theme }) => ( - - - -); - -const DownloadIcon: React.FC = ({ theme }) => ( - -); -export const DownloadChapterButton: React.FC = ({ - theme, - onPress, -}) => ( - - - - - -); +export const DownloadButton = memo(DownloadButtonControlled, areEqual); -const DeleteIcon: React.FC = ({ theme }) => ( - -); +const ChapterBookmarkButtonI: React.FC = () => { + const theme = useTheme(); -export const DeleteChapterButton: React.FC = ({ - theme, - onPress, -}) => ( - - - - - -); - -export const ChapterBookmarkButton: React.FC = ({ theme }) => ( - -); + return ( + + ); +}; +export const ChapterBookmarkButton = memo(ChapterBookmarkButtonI); const styles = StyleSheet.create({ activityIndicator: { margin: 3.5, padding: 5 }, @@ -173,6 +158,5 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - iconButton: { margin: 2 }, iconButtonLeft: { marginLeft: 2 }, }); diff --git a/src/screens/novel/components/ChapterItem.tsx b/src/screens/novel/components/ChapterItem.tsx index 99ac597ccd..55e176bf3c 100644 --- a/src/screens/novel/components/ChapterItem.tsx +++ b/src/screens/novel/components/ChapterItem.tsx @@ -4,16 +4,15 @@ import { ChapterBookmarkButton, DownloadButton, } from './Chapter/ChapterDownloadButtons'; -import { ThemeColors } from '@theme/types'; import { ChapterInfo } from '@database/types'; -import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import { getString } from '@strings/translations'; +import { useTheme } from '@providers/ThemeProvider'; +import { isChapterDownloaded } from '@database/queries/ChapterQueries'; interface ChapterItemProps { isDownloading?: boolean; isBookmarked?: boolean; chapter: ChapterInfo; - theme: ThemeColors; showChapterTitles: boolean; isSelected?: boolean; downloadChapter: () => void; @@ -29,10 +28,9 @@ interface ChapterItemProps { } const ChapterItem: React.FC = ({ - isDownloading, + isDownloading: isDownloadingProp, isBookmarked, chapter, - theme, showChapterTitles, downloadChapter, deleteChapter, @@ -46,13 +44,33 @@ const ChapterItem: React.FC = ({ isUpdateCard, novelName, }) => { - const { id, name, unread, releaseTime, bookmark, chapterNumber, progress } = - chapter; + const { + id, + name, + unread, + releaseTime, + bookmark, + chapterNumber, + progress, + isDownloaded: isDownloadedProp, + } = chapter; + chapter; - isBookmarked ??= bookmark; + const isBookmarkedLocal = isBookmarked ?? bookmark; + + const theme = useTheme(); + + const isDownloaded = useMemo(() => { + console.log(isDownloadedProp, isDownloadingProp); + + if (!isDownloadingProp) { + return isChapterDownloaded(id) ?? false; + } + return isDownloadedProp; + }, [id, isDownloadedProp, isDownloadingProp]); const highlight = useMemo( - () => [{ backgroundColor: theme.rippleColor }], + () => ({ backgroundColor: theme.rippleColor }), [theme.rippleColor], ); @@ -62,17 +80,24 @@ const ChapterItem: React.FC = ({ const titleColour = useMemo(() => { if (!unread) return theme.outline; - return bookmark ? theme.primary : theme.onSurface; - }, [unread, bookmark, theme]); + return isBookmarkedLocal ? theme.primary : theme.onSurface; + }, [ + unread, + isBookmarkedLocal, + theme.outline, + theme.primary, + theme.onSurface, + ]); - const updateTitle = useMemo( + const updateTitleStyle = useMemo( () => ({ fontSize: 14, color: unread ? theme.onSurface : theme.outline, }), - [theme.onSurface, theme.outline, unread], + [unread, theme.onSurface, theme.outline], ); - const title = useMemo( + + const titleStyle = useMemo( () => ({ fontSize: isUpdateCard ? 12 : 14, color: titleColour, @@ -80,26 +105,28 @@ const ChapterItem: React.FC = ({ }), [isUpdateCard, titleColour], ); - const meta = useMemo( + + const metaStyle = useMemo( () => [ styles.text, { marginTop: 4, color: !unread ? theme.outline - : isBookmarked + : isBookmarkedLocal ? theme.primary : theme.onSurfaceVariant, }, ], [ - isBookmarked, - theme.onSurfaceVariant, + unread, + isBookmarkedLocal, theme.outline, theme.primary, - unread, + theme.onSurfaceVariant, ], ); + const progressStyle = useMemo( () => ({ fontSize: 12, @@ -110,8 +137,26 @@ const ChapterItem: React.FC = ({ [releaseTime, theme.outline], ); + // memoize strings so translation lookup / interpolation isn't done every render + const titleText = useMemo( + () => + showChapterTitles + ? name + : getString('novelScreen.chapterChapnum', { num: chapterNumber }), + [showChapterTitles, name, chapterNumber], + ); + + const progressText = useMemo( + () => getString('novelScreen.progress', { progress }), + [progress], + ); + const handlePress = useCallback(() => { - onSelectPress ? onSelectPress(chapter) : navigateToChapter(chapter); + if (onSelectPress) { + onSelectPress(chapter); + } else { + navigateToChapter(chapter); + } }, [onSelectPress, navigateToChapter, chapter]); const handleLong = useCallback(() => { @@ -126,37 +171,30 @@ const ChapterItem: React.FC = ({ android_ripple={{ color: theme.rippleColor }} > {left} - {isBookmarked ? : null} + {isBookmarkedLocal ? : null} {isUpdateCard && ( - + {novelName} )} {unread && ( - )} - - {showChapterTitles - ? name - : getString('novelScreen.chapterChapnum', { - num: chapterNumber, - })} + + {titleText} {releaseTime && !isUpdateCard && ( - + {releaseTime} )} @@ -164,7 +202,7 @@ const ChapterItem: React.FC = ({ {!isUpdateCard && (progress ?? 0) > 0 && unread && ( {releaseTime ? '• ' : ''} - {getString('novelScreen.progress', { progress })} + {progressText} )} @@ -172,8 +210,8 @@ const ChapterItem: React.FC = ({ {!isLocal && ( = ({ ); }; +// comparator: must include any prop that affects rendering or handlers. +// compare shallowly and include theme color primitives used. function areEqual(prev: ChapterItemProps, next: ChapterItemProps) { + const prevBook = prev.isBookmarked ?? prev.chapter.bookmark; + const nextBook = next.isBookmarked ?? next.chapter.bookmark; + return ( prev.isSelected === next.isSelected && prev.isDownloading === next.isDownloading && - prev.isBookmarked === next.isBookmarked && + prevBook === nextBook && + prev.chapter.id === next.chapter.id && + prev.chapter.name === next.chapter.name && prev.chapter.unread === next.chapter.unread && prev.chapter.bookmark === next.chapter.bookmark && prev.chapter.isDownloaded === next.chapter.isDownloaded && - prev.chapter.progress === next.chapter.progress + prev.chapter.progress === next.chapter.progress && + prev.chapter.releaseTime === next.chapter.releaseTime && + prev.chapter.chapterNumber === next.chapter.chapterNumber && + prev.showChapterTitles === next.showChapterTitles && + prev.isLocal === next.isLocal && + prev.isUpdateCard === next.isUpdateCard && + prev.novelName === next.novelName && + prev.left === next.left && + prev.downloadChapter === next.downloadChapter && + prev.deleteChapter === next.deleteChapter && + prev.setChapterDownloaded === next.setChapterDownloaded && + prev.onSelectPress === next.onSelectPress && + prev.onSelectLongPress === next.onSelectLongPress && + prev.navigateToChapter === next.navigateToChapter ); } @@ -216,7 +274,10 @@ const styles = StyleSheet.create({ text: { fontSize: 12, }, - unreadIcon: { + unreadDot: { + width: 8, + height: 8, + borderRadius: 4, marginRight: 4, }, rowCenter: { flexDirection: 'row', alignItems: 'center' }, diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index 4e5a17dc84..24a7ff7f86 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -260,7 +260,6 @@ const NovelScreenList = ({ [selected, chapters, disableHapticFeedback, setSelected], ); - // Memoize the renderItem function const renderItem = useCallback( ({ item, index }: { item: ChapterInfo; index: number }) => { if (novel.id === 'NO_ID') { @@ -271,7 +270,6 @@ const NovelScreenList = ({ isDownloading={downloadingIds.has(item.id)} isBookmarked={!!item.bookmark} isLocal={novel.isLocal} - theme={theme} chapter={item} showChapterTitles={showChapterTitles} deleteChapter={() => deleteChapter(item)} @@ -290,7 +288,6 @@ const NovelScreenList = ({ [ novel, downloadingIds, - theme, showChapterTitles, isSelected, onSelectPress, @@ -302,7 +299,6 @@ const NovelScreenList = ({ ], ); - // Optimize extraData to only include what actually affects rendering const extraData = useMemo( () => ({ chaptersLength: chapters.length, From 85003d3fcb56966b214b4b52fdbca6c5a43c80c3 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:58:55 +0200 Subject: [PATCH 13/18] added Queue Context --- App.tsx | 6 +- src/components/Actionbar/Actionbar.tsx | 2 +- .../AppErrorBoundary/AppErrorBoundary.tsx | 2 +- src/components/Button/Button.tsx | 2 +- src/components/EmptyView.tsx | 2 +- .../ErrorScreenV2/ErrorScreenV2.tsx | 2 +- src/components/Modal/Modal.tsx | 2 +- src/components/NewUpdateDialog.tsx | 2 +- src/components/Skeleton/Skeleton.tsx | 2 +- src/components/Switch/Switch.tsx | 2 +- src/hooks/common/useFullscreenMode.ts | 2 +- src/hooks/persisted/useDownload.ts | 17 +----- src/hooks/persisted/useImport.ts | 14 ++--- src/navigators/BottomNavigator.tsx | 2 +- src/navigators/Main.tsx | 2 +- src/providers/Providers.tsx | 14 +++++ src/providers/context/QueueContext.tsx | 55 +++++++++++++++++++ .../ThemeContext.tsx} | 0 .../BrowseSourceScreen/BrowseSourceScreen.tsx | 2 +- .../components/FilterBottomSheet.tsx | 2 +- src/screens/Categories/CategoriesScreen.tsx | 2 +- .../components/AddCategoryModal.tsx | 2 +- .../Categories/components/CategoryCard.tsx | 2 +- .../components/DeleteCategoryModal.tsx | 2 +- .../GlobalSearchScreen/GlobalSearchScreen.tsx | 2 +- .../components/GlobalSearchResultsList.tsx | 2 +- src/screens/StatsScreen/StatsScreen.tsx | 2 +- src/screens/WebviewScreen/WebviewScreen.tsx | 2 +- src/screens/browse/BrowseScreen.tsx | 2 +- src/screens/browse/SourceNovels.tsx | 2 +- .../components/Modals/SourceSettings.tsx | 2 +- .../browse/discover/AniListTopNovels.tsx | 2 +- src/screens/browse/discover/MalTopNovels.tsx | 2 +- src/screens/browse/migration/Migration.tsx | 2 +- .../browse/migration/MigrationNovels.tsx | 2 +- .../browse/settings/BrowseSettings.tsx | 2 +- src/screens/history/HistoryScreen.tsx | 2 +- .../components/HistoryCard/HistoryCard.tsx | 2 +- src/screens/library/LibraryScreen.tsx | 2 +- .../LibraryBottomSheet/LibraryBottomSheet.tsx | 2 +- .../library/components/LibraryListView.tsx | 2 +- src/screens/more/About.tsx | 2 +- src/screens/more/DownloadsScreen.tsx | 2 +- src/screens/more/MoreScreen.tsx | 8 +-- src/screens/more/TaskQueueScreen.tsx | 9 +-- src/screens/novel/NovelScreen.tsx | 2 +- .../Chapter/ChapterDownloadButtons.tsx | 10 ++-- src/screens/novel/components/ChapterItem.tsx | 7 ++- .../components/ChooseEpubLocationModal.tsx | 2 +- .../components/DownloadCustomChapterModal.tsx | 2 +- .../novel/components/Info/NovelInfoHeader.tsx | 2 +- .../novel/components/JumpToChapterModal.tsx | 2 +- .../novel/components/NovelScreenList.tsx | 6 +- .../novel/components/SetCategoriesModal.tsx | 2 +- src/screens/onboarding/OnboardingScreen.tsx | 2 +- src/screens/onboarding/PickThemeStep.tsx | 2 +- src/screens/reader/ReaderScreen.tsx | 2 +- .../reader/components/ChapterDrawer/index.tsx | 2 +- .../ReaderBottomSheet/ReaderBottomSheet.tsx | 2 +- .../ReaderBottomSheet/ReaderFontPicker.tsx | 2 +- .../ReaderTextAlignSelector.tsx | 2 +- .../ReaderBottomSheet/ReaderThemeSelector.tsx | 2 +- .../ReaderBottomSheet/ReaderValueChange.tsx | 2 +- .../ReaderBottomSheet/TextSizeSlider.tsx | 2 +- .../reader/components/ReaderFooter.tsx | 2 +- .../reader/components/WebViewReader.tsx | 2 +- .../settings/SettingsAdvancedScreen.tsx | 2 +- .../settings/SettingsAppearanceScreen.tsx | 2 +- .../settings/SettingsBackupScreen/index.tsx | 2 +- .../SettingsGeneralScreen.tsx | 2 +- .../DefaultCategoryDialog.tsx | 2 +- .../SettingsLibraryScreen.tsx | 2 +- .../Modals/CustomFileModal.tsx | 2 +- .../Modals/FontPickerModal.tsx | 2 +- .../Modals/VoicePickerModal.tsx | 2 +- .../SettingsReaderScreen/ReaderTextSize.tsx | 2 +- .../Settings/CustomCSSSettings.tsx | 2 +- .../Settings/CustomJSSettings.tsx | 2 +- .../Settings/DisplaySettings.tsx | 2 +- .../Settings/GeneralSettings.tsx | 2 +- .../Settings/ReaderThemeSettings.tsx | 2 +- .../Settings/TextToSpeechSettings.tsx | 2 +- .../SettingsReaderScreen.tsx | 2 +- .../SettingsRepositoryScreen.tsx | 2 +- .../components/AddRepositoryModal.tsx | 2 +- .../components/DeleteRepositoryModal.tsx | 2 +- .../components/RepositoryCard.tsx | 2 +- src/screens/settings/SettingsScreen.tsx | 2 +- .../settings/SettingsTrackerScreen.tsx | 2 +- src/screens/updates/UpdatesScreen.tsx | 2 +- .../updates/components/UpdateNovelCard.tsx | 2 +- src/services/ServiceManager.ts | 27 +++++---- 92 files changed, 193 insertions(+), 140 deletions(-) create mode 100644 src/providers/Providers.tsx create mode 100644 src/providers/context/QueueContext.tsx rename src/providers/{ThemeProvider.tsx => context/ThemeContext.tsx} (100%) diff --git a/App.tsx b/App.tsx index 9436e2bba6..ab802ff665 100644 --- a/App.tsx +++ b/App.tsx @@ -16,7 +16,7 @@ import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; import Main from './src/navigators/Main'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; -import { ThemeContextProvider } from './src/providers/ThemeProvider'; +import { Providers } from './src/providers/Providers'; Notifications.setNotificationHandler({ handleNotification: async () => { @@ -34,7 +34,7 @@ LottieSplashScreen.hide(); const App = () => { return ( - + @@ -45,7 +45,7 @@ const App = () => { - + ); }; diff --git a/src/components/Actionbar/Actionbar.tsx b/src/components/Actionbar/Actionbar.tsx index c168822553..df64b45a8e 100644 --- a/src/components/Actionbar/Actionbar.tsx +++ b/src/components/Actionbar/Actionbar.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import React from 'react'; import { Dimensions, diff --git a/src/components/AppErrorBoundary/AppErrorBoundary.tsx b/src/components/AppErrorBoundary/AppErrorBoundary.tsx index f055a5ee64..362d853d49 100644 --- a/src/components/AppErrorBoundary/AppErrorBoundary.tsx +++ b/src/components/AppErrorBoundary/AppErrorBoundary.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View, Text, StatusBar } from 'react-native'; import ErrorBoundary from 'react-native-error-boundary'; import { Button, List } from '@components'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { SafeAreaView } from 'react-native-safe-area-context'; interface ErrorFallbackProps { diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 487975ead5..574846e799 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -4,7 +4,7 @@ import { ButtonProps as PaperButtonProps, } from 'react-native-paper'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { ThemeProp } from 'react-native-paper/lib/typescript/types'; interface ButtonProps extends Partial { diff --git a/src/components/EmptyView.tsx b/src/components/EmptyView.tsx index d69d9b3bf7..c4db84c016 100644 --- a/src/components/EmptyView.tsx +++ b/src/components/EmptyView.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; diff --git a/src/components/ErrorScreenV2/ErrorScreenV2.tsx b/src/components/ErrorScreenV2/ErrorScreenV2.tsx index 03b6d5499c..87132306ea 100644 --- a/src/components/ErrorScreenV2/ErrorScreenV2.tsx +++ b/src/components/ErrorScreenV2/ErrorScreenV2.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getErrorMessage } from '@utils/error'; import { MaterialDesignIconName } from '@type/icon'; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 17ed723667..5ad1c830b2 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,5 +1,5 @@ import SafeAreaView from '@components/SafeAreaView/SafeAreaView'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import React from 'react'; import { StyleSheet } from 'react-native'; import { ModalProps, overlay, Modal as PaperModal } from 'react-native-paper'; diff --git a/src/components/NewUpdateDialog.tsx b/src/components/NewUpdateDialog.tsx index ca83cceca0..d41eefbc96 100644 --- a/src/components/NewUpdateDialog.tsx +++ b/src/components/NewUpdateDialog.tsx @@ -6,7 +6,7 @@ import { ScrollView } from 'react-native-gesture-handler'; import Button from './Button/Button'; import { getString } from '@strings/translations'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Modal } from '@components'; interface NewUpdateDialogProps { diff --git a/src/components/Skeleton/Skeleton.tsx b/src/components/Skeleton/Skeleton.tsx index 9d70eb7f25..eed7b097e4 100644 --- a/src/components/Skeleton/Skeleton.tsx +++ b/src/components/Skeleton/Skeleton.tsx @@ -1,5 +1,5 @@ import { useAppSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import * as React from 'react'; import { StyleProp, ViewStyle, StyleSheet, View } from 'react-native'; import Animated, { diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx index ec967d3684..163c54b590 100644 --- a/src/components/Switch/Switch.tsx +++ b/src/components/Switch/Switch.tsx @@ -8,7 +8,7 @@ import Animated, { withTiming, useDerivedValue, } from 'react-native-reanimated'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; interface SwitchProps { value: boolean; diff --git a/src/hooks/common/useFullscreenMode.ts b/src/hooks/common/useFullscreenMode.ts index 0ade5820a7..299083797b 100644 --- a/src/hooks/common/useFullscreenMode.ts +++ b/src/hooks/common/useFullscreenMode.ts @@ -5,7 +5,7 @@ import { useChapterGeneralSettings, useChapterReaderSettings, } from '../persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import Color from 'color'; import * as NavigationBar from 'expo-navigation-bar'; import { diff --git a/src/hooks/persisted/useDownload.ts b/src/hooks/persisted/useDownload.ts index 0a3f529af3..2f39cff8ee 100644 --- a/src/hooks/persisted/useDownload.ts +++ b/src/hooks/persisted/useDownload.ts @@ -1,24 +1,13 @@ import { ChapterInfo, NovelInfo } from '@database/types'; -import ServiceManager, { - BackgroundTaskMetadata, - DownloadChapterTask, - QueuedBackgroundTask, -} from '@services/ServiceManager'; +import { useQueue } from '@providers/Providers'; +import ServiceManager from '@services/ServiceManager'; import { useCallback, useMemo } from 'react'; -import { useMMKVObject } from 'react-native-mmkv'; export const DOWNLOAD_QUEUE = 'DOWNLOAD'; export const CHAPTER_DOWNLOADING = 'CHAPTER_DOWNLOADING'; export default function useDownload() { - const [queue] = useMMKVObject( - ServiceManager.manager.STORE_KEY, - ); - - const downloadQueue = useMemo( - () => queue?.filter(t => t.task?.name === 'DOWNLOAD_CHAPTER') || [], - [queue], - ) as { task: DownloadChapterTask; meta: BackgroundTaskMetadata }[]; + const { downloadQueue } = useQueue(); const downloadChapter = useCallback( (novel: NovelInfo, chapter: ChapterInfo) => diff --git a/src/hooks/persisted/useImport.ts b/src/hooks/persisted/useImport.ts index 1aaec59693..0b0811277e 100644 --- a/src/hooks/persisted/useImport.ts +++ b/src/hooks/persisted/useImport.ts @@ -1,22 +1,16 @@ import { useLibraryContext } from '@components/Context/LibraryContext'; -import ServiceManager, { BackgroundTask } from '@services/ServiceManager'; +import { useQueue } from '@providers/Providers'; +import ServiceManager from '@services/ServiceManager'; import { DocumentPickerResult } from 'expo-document-picker'; import { useCallback, useEffect, useMemo } from 'react'; -import { useMMKVObject } from 'react-native-mmkv'; export default function useImport() { const { refetchLibrary } = useLibraryContext(); - const [queue] = useMMKVObject( - ServiceManager.manager.STORE_KEY, - ); - const importQueue = useMemo( - () => queue?.filter(t => t.name === 'IMPORT_EPUB') || [], - [queue], - ); + const { importQueue } = useQueue(); useEffect(() => { refetchLibrary(); - }, [importQueue, refetchLibrary]); + }, [importQueue.length, refetchLibrary]); const importNovel = useCallback((pickedNovel: DocumentPickerResult) => { if (pickedNovel.canceled) return; diff --git a/src/navigators/BottomNavigator.tsx b/src/navigators/BottomNavigator.tsx index 9137fa104b..26cc4e95e7 100644 --- a/src/navigators/BottomNavigator.tsx +++ b/src/navigators/BottomNavigator.tsx @@ -12,7 +12,7 @@ import More from '../screens/more/MoreScreen'; import { getString } from '@strings/translations'; import { useAppSettings, usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { BottomNavigatorParamList } from './types'; import Icon from '@react-native-vector-icons/material-design-icons'; import { MaterialDesignIconName } from '@type/icon'; diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx index 5f1b2059e4..5e70c3b151 100644 --- a/src/navigators/Main.tsx +++ b/src/navigators/Main.tsx @@ -8,7 +8,7 @@ import { setStatusBarColor, } from '@theme/utils/setBarColor'; import { useAppSettings, usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useGithubUpdateChecker } from '@hooks/common/githubUpdateChecker'; /** diff --git a/src/providers/Providers.tsx b/src/providers/Providers.tsx new file mode 100644 index 0000000000..25493e0041 --- /dev/null +++ b/src/providers/Providers.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { ThemeContextProvider } from './context/ThemeContext'; +import { QueueContextProvider } from './context/QueueContext'; + +export { useTheme } from './context/ThemeContext'; +export { useQueue } from './context/QueueContext'; + +export function Providers({ children }: { children: React.JSX.Element }) { + return ( + + {children} + + ); +} diff --git a/src/providers/context/QueueContext.tsx b/src/providers/context/QueueContext.tsx new file mode 100644 index 0000000000..d60c0101c3 --- /dev/null +++ b/src/providers/context/QueueContext.tsx @@ -0,0 +1,55 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import { useMMKVObject } from 'react-native-mmkv'; + +import ServiceManager, { QueuedBackgroundTask } from '@services/ServiceManager'; + +type QueueContextType = { + taskQueue: QueuedBackgroundTask[]; + importQueue: QueuedBackgroundTask<'IMPORT_EPUB'>[]; + downloadQueue: QueuedBackgroundTask<'DOWNLOAD_CHAPTER'>[]; +}; + +const QueueContext = createContext(null); + +export function QueueContextProvider({ + children, +}: { + children: React.JSX.Element; +}) { + const [taskQueue = []] = useMMKVObject( + ServiceManager.manager.STORE_KEY, + ); + + const contextValue = useMemo(() => { + const importQueue: QueuedBackgroundTask<'IMPORT_EPUB'>[] = []; + const downloadQueue: QueuedBackgroundTask<'DOWNLOAD_CHAPTER'>[] = []; + + taskQueue.forEach(task => { + switch (task.task.name) { + case 'IMPORT_EPUB': + importQueue.push(task as QueuedBackgroundTask<'IMPORT_EPUB'>); + break; + case 'DOWNLOAD_CHAPTER': + downloadQueue.push(task as QueuedBackgroundTask<'DOWNLOAD_CHAPTER'>); + break; + } + }); + + return { + taskQueue, + importQueue, + downloadQueue, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [taskQueue.length]); + + return ( + + {children} + + ); +} + +export const useQueue = () => { + return useContext(QueueContext)!; +}; diff --git a/src/providers/ThemeProvider.tsx b/src/providers/context/ThemeContext.tsx similarity index 100% rename from src/providers/ThemeProvider.tsx rename to src/providers/context/ThemeContext.tsx diff --git a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx index 657b3af98f..5fe00c0b49 100644 --- a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx +++ b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx @@ -8,7 +8,7 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import FilterBottomSheet from './components/FilterBottomSheet'; import { useSearch } from '@hooks'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useBrowseSource, useSearchSource } from './useBrowseSource'; import { NovelItem } from '@plugins/types'; diff --git a/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx b/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx index 42d795669f..e72971b233 100644 --- a/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx +++ b/src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx @@ -14,7 +14,7 @@ import { BottomSheetView, } from '@gorhom/bottom-sheet'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { FilterTypes, FilterToValues, diff --git a/src/screens/Categories/CategoriesScreen.tsx b/src/screens/Categories/CategoriesScreen.tsx index e224f51f94..d467eb61ae 100644 --- a/src/screens/Categories/CategoriesScreen.tsx +++ b/src/screens/Categories/CategoriesScreen.tsx @@ -8,7 +8,7 @@ import AddCategoryModal from './components/AddCategoryModal'; import { updateCategoryOrderInDb } from '@database/queries/CategoryQueries'; import { useBoolean } from '@hooks'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import CategoryCard from './components/CategoryCard'; diff --git a/src/screens/Categories/components/AddCategoryModal.tsx b/src/screens/Categories/components/AddCategoryModal.tsx index 4474b141a1..14a90f189d 100644 --- a/src/screens/Categories/components/AddCategoryModal.tsx +++ b/src/screens/Categories/components/AddCategoryModal.tsx @@ -10,7 +10,7 @@ import { isCategoryNameDuplicate, updateCategory, } from '../../../database/queries/CategoryQueries'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { showToast } from '@utils/showToast'; diff --git a/src/screens/Categories/components/CategoryCard.tsx b/src/screens/Categories/components/CategoryCard.tsx index fc2bd96a01..67ec8ecb35 100644 --- a/src/screens/Categories/components/CategoryCard.tsx +++ b/src/screens/Categories/components/CategoryCard.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Text, View } from 'react-native'; import React from 'react'; import { Category } from '@database/types'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import AddCategoryModal from './AddCategoryModal'; import { useBoolean } from '@hooks'; import { overlay, Portal } from 'react-native-paper'; diff --git a/src/screens/Categories/components/DeleteCategoryModal.tsx b/src/screens/Categories/components/DeleteCategoryModal.tsx index 42b476f13c..914c9b0ace 100644 --- a/src/screens/Categories/components/DeleteCategoryModal.tsx +++ b/src/screens/Categories/components/DeleteCategoryModal.tsx @@ -6,7 +6,7 @@ import { Button, Modal } from '@components/index'; import { Category } from '@database/types'; import { deleteCategoryById } from '@database/queries/CategoryQueries'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; diff --git a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx index 18404a1933..b4dd26e836 100644 --- a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx +++ b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx @@ -6,7 +6,7 @@ import { EmptyView, SafeAreaView, SearchbarV2 } from '@components/index'; import GlobalSearchResultsList from './components/GlobalSearchResultsList'; import { useSearch } from '@hooks'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { useGlobalSearch } from './hooks/useGlobalSearch'; diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx index 5039d2a280..d52da7ac82 100644 --- a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx @@ -7,7 +7,7 @@ import { useNavigation } from '@react-navigation/native'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import { getString } from '@strings/translations'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { GlobalSearchResult } from '../hooks/useGlobalSearch'; import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading'; diff --git a/src/screens/StatsScreen/StatsScreen.tsx b/src/screens/StatsScreen/StatsScreen.tsx index f1e297feae..c47069256f 100644 --- a/src/screens/StatsScreen/StatsScreen.tsx +++ b/src/screens/StatsScreen/StatsScreen.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { diff --git a/src/screens/WebviewScreen/WebviewScreen.tsx b/src/screens/WebviewScreen/WebviewScreen.tsx index 1b8aaebd07..f8b60377a3 100644 --- a/src/screens/WebviewScreen/WebviewScreen.tsx +++ b/src/screens/WebviewScreen/WebviewScreen.tsx @@ -5,7 +5,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { getPlugin } from '@plugins/pluginManager'; import { useBackHandler } from '@hooks'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { WebviewScreenProps } from '@navigators/types'; import { getUserAgent } from '@hooks/persisted/useUserAgent'; import { resolveUrl } from '@services/plugin/fetch'; diff --git a/src/screens/browse/BrowseScreen.tsx b/src/screens/browse/BrowseScreen.tsx index bab53b9e84..37521bfab8 100644 --- a/src/screens/browse/BrowseScreen.tsx +++ b/src/screens/browse/BrowseScreen.tsx @@ -4,7 +4,7 @@ import { TabView, TabBar } from 'react-native-tab-view'; import { useSearch } from '@hooks'; import { usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { EmptyView, SafeAreaView, SearchbarV2 } from '@components'; diff --git a/src/screens/browse/SourceNovels.tsx b/src/screens/browse/SourceNovels.tsx index b9d3fc2f9a..e974a0752c 100644 --- a/src/screens/browse/SourceNovels.tsx +++ b/src/screens/browse/SourceNovels.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { StyleSheet, View, FlatList, Text, FlatListProps } from 'react-native'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import ListView from '../../components/ListView'; import { Appbar } from '@components'; diff --git a/src/screens/browse/components/Modals/SourceSettings.tsx b/src/screens/browse/components/Modals/SourceSettings.tsx index 727e791715..719e36b828 100644 --- a/src/screens/browse/components/Modals/SourceSettings.tsx +++ b/src/screens/browse/components/Modals/SourceSettings.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { TextInput } from 'react-native-paper'; import { Button, Modal, SwitchItem } from '@components/index'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { Storage } from '@plugins/helpers/storage'; diff --git a/src/screens/browse/discover/AniListTopNovels.tsx b/src/screens/browse/discover/AniListTopNovels.tsx index b6f1fd0d85..e867f05c5f 100644 --- a/src/screens/browse/discover/AniListTopNovels.tsx +++ b/src/screens/browse/discover/AniListTopNovels.tsx @@ -15,7 +15,7 @@ import { SafeAreaView, SearchbarV2 } from '@components'; import { showToast } from '@utils/showToast'; import TrackerNovelCard from './TrackerNovelCard'; import { useTracker } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import TrackerLoading from '../loadingAnimation/TrackerLoading'; import { queryAniList } from '@services/Trackers/aniList'; import localeData from 'dayjs/plugin/localeData'; diff --git a/src/screens/browse/discover/MalTopNovels.tsx b/src/screens/browse/discover/MalTopNovels.tsx index 12040a087b..a6e2b2783e 100644 --- a/src/screens/browse/discover/MalTopNovels.tsx +++ b/src/screens/browse/discover/MalTopNovels.tsx @@ -16,7 +16,7 @@ import { SafeAreaView, SearchbarV2 } from '@components'; import { showToast } from '@utils/showToast'; import { scrapeSearchResults, scrapeTopNovels } from './MyAnimeListScraper'; import MalNovelCard from './TrackerNovelCard'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import MalLoading from '../loadingAnimation/MalLoading'; import { BrowseMalScreenProps } from '@navigators/types'; diff --git a/src/screens/browse/migration/Migration.tsx b/src/screens/browse/migration/Migration.tsx index c8b6fffe00..4a7f1d0750 100644 --- a/src/screens/browse/migration/Migration.tsx +++ b/src/screens/browse/migration/Migration.tsx @@ -4,7 +4,7 @@ import { StyleSheet, View, FlatList, Text, FlatListProps } from 'react-native'; import MigrationSourceItem from './MigrationSourceItem'; import { usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useLibraryNovels } from '@screens/library/hooks/useLibrary'; import { Appbar } from '@components'; import { MigrationScreenProps } from '@navigators/types'; diff --git a/src/screens/browse/migration/MigrationNovels.tsx b/src/screens/browse/migration/MigrationNovels.tsx index 7c9b7f9890..f59a842806 100644 --- a/src/screens/browse/migration/MigrationNovels.tsx +++ b/src/screens/browse/migration/MigrationNovels.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { View, Text, FlatList, StyleSheet, FlatListProps } from 'react-native'; import { ProgressBar } from 'react-native-paper'; import { usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import EmptyView from '@components/EmptyView'; import MigrationNovelList from './MigrationNovelList'; diff --git a/src/screens/browse/settings/BrowseSettings.tsx b/src/screens/browse/settings/BrowseSettings.tsx index 929b2c951f..a47acce5a2 100644 --- a/src/screens/browse/settings/BrowseSettings.tsx +++ b/src/screens/browse/settings/BrowseSettings.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Appbar, List, SwitchItem } from '@components'; import { useBrowseSettings, usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { getLocaleLanguageName, languages } from '@utils/constants/languages'; import { BrowseSettingsScreenProp } from '@navigators/types/index'; diff --git a/src/screens/history/HistoryScreen.tsx b/src/screens/history/HistoryScreen.tsx index d87543e7db..7557f135d6 100644 --- a/src/screens/history/HistoryScreen.tsx +++ b/src/screens/history/HistoryScreen.tsx @@ -13,7 +13,7 @@ import HistoryCard from './components/HistoryCard/HistoryCard'; import { useSearch, useBoolean } from '@hooks'; import { useHistory } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { convertDateToISOString } from '@database/utils/convertDateToISOString'; diff --git a/src/screens/history/components/HistoryCard/HistoryCard.tsx b/src/screens/history/components/HistoryCard/HistoryCard.tsx index d9ce613072..2208933633 100644 --- a/src/screens/history/components/HistoryCard/HistoryCard.tsx +++ b/src/screens/history/components/HistoryCard/HistoryCard.tsx @@ -8,7 +8,7 @@ import { IconButtonV2 } from '@components'; import { defaultCover } from '@plugins/helpers/constants'; import { getString } from '@strings/translations'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { History, NovelInfo } from '@database/types'; import { HistoryScreenProps } from '@navigators/types'; diff --git a/src/screens/library/LibraryScreen.tsx b/src/screens/library/LibraryScreen.tsx index de4c66713e..ac4f8d02ce 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -29,7 +29,7 @@ import { Banner } from './components/Banner'; import { Actionbar } from '@components/Actionbar/Actionbar'; import { useAppSettings, useHistory } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useSearch, useBackHandler, useBoolean } from '@hooks'; import { getString } from '@strings/translations'; import { FAB, Portal } from 'react-native-paper'; diff --git a/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx b/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx index 3d1b8e7096..cdbc86862e 100644 --- a/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx +++ b/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx @@ -16,7 +16,7 @@ import { import color from 'color'; import { useLibrarySettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { Checkbox, SortItem } from '@components/Checkbox/Checkbox'; import { diff --git a/src/screens/library/components/LibraryListView.tsx b/src/screens/library/components/LibraryListView.tsx index f84883e77b..3109588cc9 100644 --- a/src/screens/library/components/LibraryListView.tsx +++ b/src/screens/library/components/LibraryListView.tsx @@ -9,7 +9,7 @@ import NovelList, { NovelListRenderItem } from '@components/NovelList'; import { NovelInfo } from '@database/types'; import { getString } from '@strings/translations'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { LibraryScreenProps } from '@navigators/types'; import ServiceManager from '@services/ServiceManager'; diff --git a/src/screens/more/About.tsx b/src/screens/more/About.tsx index 84e30a164a..20a04f4c01 100644 --- a/src/screens/more/About.tsx +++ b/src/screens/more/About.tsx @@ -5,7 +5,7 @@ import * as Linking from 'expo-linking'; import { getString } from '@strings/translations'; import { MoreHeader } from './components/MoreHeader'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { List, SafeAreaView } from '@components'; import { AboutScreenProps } from '@navigators/types'; import { GIT_HASH, RELEASE_DATE, BUILD_TYPE } from '@env'; diff --git a/src/screens/more/DownloadsScreen.tsx b/src/screens/more/DownloadsScreen.tsx index ba87a019bc..46ac21ad4e 100644 --- a/src/screens/more/DownloadsScreen.tsx +++ b/src/screens/more/DownloadsScreen.tsx @@ -11,7 +11,7 @@ import { getDownloadedChapters, } from '@database/queries/ChapterQueries'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import RemoveDownloadsDialog from './components/RemoveDownloadsDialog'; import UpdatesSkeletonLoading from '@screens/updates/components/UpdatesSkeletonLoading'; diff --git a/src/screens/more/MoreScreen.tsx b/src/screens/more/MoreScreen.tsx index 991066aada..797ea57cee 100644 --- a/src/screens/more/MoreScreen.tsx +++ b/src/screens/more/MoreScreen.tsx @@ -6,17 +6,13 @@ import { List, SafeAreaView } from '@components'; import { MoreHeader } from './components/MoreHeader'; import { useLibrarySettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useQueue, useTheme } from '@providers/Providers'; import { MoreStackScreenProps } from '@navigators/types'; import Switch from '@components/Switch/Switch'; -import { useMMKVObject } from 'react-native-mmkv'; -import ServiceManager, { BackgroundTask } from '@services/ServiceManager'; const MoreScreen = ({ navigation }: MoreStackScreenProps) => { const theme = useTheme(); - const [taskQueue] = useMMKVObject( - ServiceManager.manager.STORE_KEY, - ); + const { taskQueue } = useQueue(); const { incognitoMode = false, downloadedOnlyMode = false, diff --git a/src/screens/more/TaskQueueScreen.tsx b/src/screens/more/TaskQueueScreen.tsx index 8b856bba11..e5c528d4d0 100644 --- a/src/screens/more/TaskQueueScreen.tsx +++ b/src/screens/more/TaskQueueScreen.tsx @@ -8,22 +8,19 @@ import { overlay, } from 'react-native-paper'; -import { useTheme } from '@providers/ThemeProvider'; +import { useQueue, useTheme } from '@providers/Providers'; import { showToast } from '../../utils/showToast'; import { getString } from '@strings/translations'; import { Appbar, EmptyView, SafeAreaView } from '@components'; import { TaskQueueScreenProps } from '@navigators/types'; -import ServiceManager, { QueuedBackgroundTask } from '@services/ServiceManager'; +import ServiceManager from '@services/ServiceManager'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useMMKVObject } from 'react-native-mmkv'; const DownloadQueue = ({ navigation }: TaskQueueScreenProps) => { const theme = useTheme(); const { bottom, right } = useSafeAreaInsets(); - const [taskQueue] = useMMKVObject( - ServiceManager.manager.STORE_KEY, - ); + const { taskQueue } = useQueue(); const [isRunning, setIsRunning] = useState(ServiceManager.manager.isRunning); const [visible, setVisible] = useState(false); const openMenu = () => setVisible(true); diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index bae51233fd..fb132ba900 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -9,7 +9,7 @@ import Animated, { import { Portal, Appbar, Snackbar } from 'react-native-paper'; import { useDownload } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import JumpToChapterModal from './components/JumpToChapterModal'; import { Actionbar } from '../../components/Actionbar/Actionbar'; import EditInfoModal from './components/EditInfoModal'; diff --git a/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx b/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx index fea45fa6c6..b2d92386b9 100644 --- a/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx +++ b/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx @@ -5,7 +5,7 @@ import MaterialCommunityIcons from '@react-native-vector-icons/material-design-i import Color from 'color'; import { getString } from '@strings/translations'; import type { MD3ThemeType } from '@theme/types'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { IconButtonV2 } from '@components'; interface Props { @@ -19,7 +19,7 @@ interface Props { } const DownloadButtonControlled: React.FC = ({ - isDownloaded, + isDownloaded: isDownloadedProp, isDownloading, theme, deleteChapter, @@ -28,6 +28,7 @@ const DownloadButtonControlled: React.FC = ({ }) => { // local menu state only const [menuVisible, setMenuVisible] = useState(false); + const [isDownloaded, setIsDownloaded] = useState(isDownloadedProp); const rippleStyle = useMemo( () => ({ color: Color(theme.primary).alpha(0.12).string() }), @@ -46,11 +47,12 @@ const DownloadButtonControlled: React.FC = ({ const onDelete = useCallback(() => { deleteChapter(); setMenuVisible(false); - setChapterDownloaded?.(false); - }, [deleteChapter, setChapterDownloaded]); + setIsDownloaded(false); + }, [deleteChapter]); const onDownload = useCallback(() => { downloadChapter(); + setIsDownloaded(true); }, [downloadChapter]); if (isDownloading || isDownloaded === undefined) { diff --git a/src/screens/novel/components/ChapterItem.tsx b/src/screens/novel/components/ChapterItem.tsx index 55e176bf3c..dc29bc4c3e 100644 --- a/src/screens/novel/components/ChapterItem.tsx +++ b/src/screens/novel/components/ChapterItem.tsx @@ -6,7 +6,7 @@ import { } from './Chapter/ChapterDownloadButtons'; import { ChapterInfo } from '@database/types'; import { getString } from '@strings/translations'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { isChapterDownloaded } from '@database/queries/ChapterQueries'; interface ChapterItemProps { @@ -55,14 +55,15 @@ const ChapterItem: React.FC = ({ isDownloaded: isDownloadedProp, } = chapter; chapter; + if (name === 'Chapter 2: Rice Farming 101') { + console.log(name, isDownloadedProp); + } const isBookmarkedLocal = isBookmarked ?? bookmark; const theme = useTheme(); const isDownloaded = useMemo(() => { - console.log(isDownloadedProp, isDownloadingProp); - if (!isDownloadingProp) { return isChapterDownloaded(id) ?? false; } diff --git a/src/screens/novel/components/ChooseEpubLocationModal.tsx b/src/screens/novel/components/ChooseEpubLocationModal.tsx index be5a06ef45..b89559bf6b 100644 --- a/src/screens/novel/components/ChooseEpubLocationModal.tsx +++ b/src/screens/novel/components/ChooseEpubLocationModal.tsx @@ -8,7 +8,7 @@ import { Button, List, Modal, SwitchItem } from '@components'; import { useBoolean } from '@hooks'; import { getString } from '@strings/translations'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { showToast } from '@utils/showToast'; interface ChooseEpubLocationModalProps { diff --git a/src/screens/novel/components/DownloadCustomChapterModal.tsx b/src/screens/novel/components/DownloadCustomChapterModal.tsx index e2ba939495..2a81934374 100644 --- a/src/screens/novel/components/DownloadCustomChapterModal.tsx +++ b/src/screens/novel/components/DownloadCustomChapterModal.tsx @@ -5,7 +5,7 @@ import { Button, IconButton, Portal } from 'react-native-paper'; import { ChapterInfo, NovelInfo } from '@database/types'; import { getString } from '@strings/translations'; import { Modal } from '@components'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useNovelChapters, useNovelState } from '@hooks/persisted/index'; interface DownloadCustomChapterModalProps { diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 4bc5540d93..0bb1fa398a 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -33,7 +33,7 @@ import { useNovelPages, useNovelState, } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { NovelStatus, PluginItem } from '@plugins/types'; import { translateNovelStatus } from '@utils/translateEnum'; import { getMMKVObject } from '@utils/mmkv/mmkv'; diff --git a/src/screens/novel/components/JumpToChapterModal.tsx b/src/screens/novel/components/JumpToChapterModal.tsx index ed4f029814..8bcb6c5b4a 100644 --- a/src/screens/novel/components/JumpToChapterModal.tsx +++ b/src/screens/novel/components/JumpToChapterModal.tsx @@ -9,7 +9,7 @@ import { getString } from '@strings/translations'; import { Button, Modal, SwitchItem } from '@components'; import { Portal, Text } from 'react-native-paper'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { ChapterInfo } from '@database/types'; import { NovelScreenProps } from '@navigators/types'; import { FlashList, ListRenderItem } from '@shopify/flash-list'; diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index 24a7ff7f86..73ea2cecdf 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -33,7 +33,7 @@ import { ChapterListSkeleton } from '@components/Skeleton/Skeleton'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { FlashList } from '@shopify/flash-list'; import useNovelLastRead from '@hooks/persisted/novel/useNovelLastRead'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; type NovelScreenListProps = { headerOpacity: SharedValue; @@ -301,13 +301,13 @@ const NovelScreenList = ({ const extraData = useMemo( () => ({ - chaptersLength: chapters.length, + chapters, selectedLength: selected.length, novelId: novel.id, loading, downloadingIds: Array.from(downloadingIds).sort().join(','), // Convert to string for stable comparison }), - [chapters.length, selected.length, novel.id, loading, downloadingIds], + [chapters, selected.length, novel.id, loading, downloadingIds], ); const keyExtractor = useCallback((item: ChapterInfo) => 'c' + item.id, []); diff --git a/src/screens/novel/components/SetCategoriesModal.tsx b/src/screens/novel/components/SetCategoriesModal.tsx index 2c5bd7fd21..0992fe3f7b 100644 --- a/src/screens/novel/components/SetCategoriesModal.tsx +++ b/src/screens/novel/components/SetCategoriesModal.tsx @@ -5,7 +5,7 @@ import { NavigationProp, useNavigation } from '@react-navigation/native'; import { Button, Modal } from '@components/index'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { getCategoriesWithCount } from '@database/queries/CategoryQueries'; diff --git a/src/screens/onboarding/OnboardingScreen.tsx b/src/screens/onboarding/OnboardingScreen.tsx index 3e7386579c..d18332a24f 100644 --- a/src/screens/onboarding/OnboardingScreen.tsx +++ b/src/screens/onboarding/OnboardingScreen.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Text } from 'react-native-paper'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Image, StyleSheet, View } from 'react-native'; diff --git a/src/screens/onboarding/PickThemeStep.tsx b/src/screens/onboarding/PickThemeStep.tsx index 4a0cbd1cef..d69a46f8f3 100644 --- a/src/screens/onboarding/PickThemeStep.tsx +++ b/src/screens/onboarding/PickThemeStep.tsx @@ -3,7 +3,7 @@ import { View, Text, Pressable, StyleSheet, FlatList } from 'react-native'; import { ThemePicker } from '@components/ThemePicker/ThemePicker'; import { ThemeColors } from '@theme/types'; import { useMMKVObject } from 'react-native-mmkv'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { darkThemes, lightThemes } from '@theme/md3'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx index 068f7cf3d4..ab0ef7a602 100644 --- a/src/screens/reader/ReaderScreen.tsx +++ b/src/screens/reader/ReaderScreen.tsx @@ -1,6 +1,6 @@ import React, { useRef, useCallback, useState, useEffect } from 'react'; import { useChapterGeneralSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import ReaderAppbar from './components/ReaderAppbar'; import ReaderFooter from './components/ReaderFooter'; diff --git a/src/screens/reader/components/ChapterDrawer/index.tsx b/src/screens/reader/components/ChapterDrawer/index.tsx index 05be2ba96f..9e81889bec 100644 --- a/src/screens/reader/components/ChapterDrawer/index.tsx +++ b/src/screens/reader/components/ChapterDrawer/index.tsx @@ -12,7 +12,7 @@ import { useNovelPages, useNovelSettings, } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Button, LoadingScreenV2 } from '@components/index'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx index b4d3b250e6..0421ed59de 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx @@ -18,7 +18,7 @@ import Color from 'color'; import { BottomSheetFlashList, BottomSheetView } from '@gorhom/bottom-sheet'; import BottomSheet from '@components/BottomSheet/BottomSheet'; import { useChapterGeneralSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { SceneMap, TabBar, TabView } from 'react-native-tab-view'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx index 92aa70f7f7..1cd9bddf8b 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx @@ -5,7 +5,7 @@ import color from 'color'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import { getString } from '@strings/translations'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Font, readerFonts } from '@utils/constants/readerConstants'; import { FlatList } from 'react-native-gesture-handler'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx index 68c74f8baf..85863a6b71 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { textAlignments } from '@utils/constants/readerConstants'; import { ToggleButton } from '@components/Common/ToggleButton'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx index 4a68697824..924f007127 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx @@ -4,7 +4,7 @@ import { ToggleColorButton } from '@components/Common/ToggleButton'; import { getString } from '@strings/translations'; import { presetReaderThemes } from '@utils/constants/readerConstants'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { FlatList } from 'react-native-gesture-handler'; import { ReaderTheme } from '@hooks/persisted/useSettings'; diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx index 5c58034b9c..6812e4b456 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { IconButtonV2 } from '@components'; import { ChapterReaderSettings } from '@hooks/persisted/useSettings'; diff --git a/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx b/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx index 67d080dcc6..d211bf532e 100644 --- a/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Text, View } from 'react-native'; import React from 'react'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import Slider from '@react-native-community/slider'; import { getString } from '@strings/translations'; diff --git a/src/screens/reader/components/ReaderFooter.tsx b/src/screens/reader/components/ReaderFooter.tsx index 47993066fe..2339287d64 100644 --- a/src/screens/reader/components/ReaderFooter.tsx +++ b/src/screens/reader/components/ReaderFooter.tsx @@ -11,7 +11,7 @@ import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/typ import { ChapterScreenProps } from '@navigators/types'; import { useChapterContext } from '../ChapterContext'; import { SCREEN_HEIGHT } from '@gorhom/bottom-sheet'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useHeightContext } from '@screens/novel/context/HeightsContext'; interface ChapterFooterProps { diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index 361b734db6..f760cdb2fd 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -3,7 +3,7 @@ import { NativeEventEmitter, NativeModules, StatusBar } from 'react-native'; import WebView from 'react-native-webview'; import color from 'color'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { getPlugin } from '@plugins/pluginManager'; diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx index e899bab25e..3a967987ae 100644 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ b/src/screens/settings/SettingsAdvancedScreen.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { Portal, Text, TextInput } from 'react-native-paper'; import { useUserAgent } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsAppearanceScreen.tsx b/src/screens/settings/SettingsAppearanceScreen.tsx index 6c534879fc..23d1cca9e1 100644 --- a/src/screens/settings/SettingsAppearanceScreen.tsx +++ b/src/screens/settings/SettingsAppearanceScreen.tsx @@ -6,7 +6,7 @@ import SettingSwitch from './components/SettingSwitch'; import ColorPickerModal from '@components/ColorPickerModal/ColorPickerModal'; import { useAppSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useMMKVBoolean, useMMKVObject, diff --git a/src/screens/settings/SettingsBackupScreen/index.tsx b/src/screens/settings/SettingsBackupScreen/index.tsx index 6a29f012fe..1dd4e71179 100644 --- a/src/screens/settings/SettingsBackupScreen/index.tsx +++ b/src/screens/settings/SettingsBackupScreen/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Appbar, List, SafeAreaView } from '@components'; import { useBoolean } from '@hooks'; import { BackupSettingsScreenProps } from '@navigators/types'; diff --git a/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx b/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx index d41c222189..0acefed48e 100644 --- a/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx +++ b/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx @@ -9,7 +9,7 @@ import { useLastUpdate, useLibrarySettings, } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import DefaultChapterSortModal from '../components/DefaultChapterSortModal'; import { DisplayModes, diff --git a/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx b/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx index dbed69fa77..8c8d427798 100644 --- a/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx +++ b/src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx @@ -5,7 +5,7 @@ import { Button, Dialog } from 'react-native-paper'; import { RadioButton } from '@components'; import { getString } from '@strings/translations'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Category } from '@database/types'; diff --git a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx index beb01257b3..1595faa43b 100644 --- a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx +++ b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx @@ -3,7 +3,7 @@ import { Appbar, List } from '@components'; import { getString } from '@strings/translations'; import { useBoolean } from '@hooks'; import { useCategories } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { useNavigation } from '@react-navigation/native'; import { Portal } from 'react-native-paper'; import DefaultCategoryDialog from './DefaultCategoryDialog'; diff --git a/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx index 1f6dc0fa52..6472584edb 100644 --- a/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx +++ b/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx @@ -7,7 +7,7 @@ import * as DocumentPicker from 'expo-document-picker'; import { Button, Modal } from '@components/index'; import { showToast } from '@utils/showToast'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; interface CustomFileModal { diff --git a/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx index c9ba814bb0..3b7c1826c9 100644 --- a/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx +++ b/src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx @@ -4,7 +4,7 @@ import { Portal } from 'react-native-paper'; import { RadioButton } from '@components/RadioButton/RadioButton'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { readerFonts } from '@utils/constants/readerConstants'; import { Modal } from '@components'; diff --git a/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx index a101044a53..73640db7d8 100644 --- a/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx +++ b/src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx @@ -4,7 +4,7 @@ import { Portal, TextInput, ActivityIndicator } from 'react-native-paper'; import { RadioButton } from '@components/RadioButton/RadioButton'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Voice } from 'expo-speech'; import { FlashList } from '@shopify/flash-list'; import { Modal } from '@components'; diff --git a/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx b/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx index 18873a7490..ae625c2099 100644 --- a/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx +++ b/src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx @@ -2,7 +2,7 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { IconButtonV2 } from '@components/index'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx index 502ea8a3c8..0bc2db8645 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/CustomCSSSettings.tsx @@ -6,7 +6,7 @@ import { Button, List, ConfirmationDialog } from '@components'; import { useBoolean } from '@hooks'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import CustomFileModal from '../Modals/CustomFileModal'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx index c05bd5bae8..fb28c53516 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/CustomJSSettings.tsx @@ -6,7 +6,7 @@ import { Button, List, ConfirmationDialog } from '@components/index'; import { useBoolean } from '@hooks'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import CustomFileModal from '../Modals/CustomFileModal'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx index 154415a7d2..38d29e200c 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/DisplaySettings.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { List } from '@components/index'; import { useChapterGeneralSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import SettingSwitch from '../../components/SettingSwitch'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx index 7f3ff40a9c..e5eb7750aa 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/GeneralSettings.tsx @@ -12,7 +12,7 @@ import { defaultTo } from 'lodash-es'; import { Button, List } from '@components/index'; import { useChapterGeneralSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import SettingSwitch from '../../components/SettingSwitch'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx index f810f41569..c40cad58cd 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/ReaderThemeSettings.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Button, ColorPreferenceItem, List } from '@components/index'; import { useChapterReaderSettings } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import ReaderTextAlignSelector from '@screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector'; import ReaderTextSize from '../ReaderTextSize'; diff --git a/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx b/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx index 89665d444e..6f687289b2 100644 --- a/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx +++ b/src/screens/settings/SettingsReaderScreen/Settings/TextToSpeechSettings.tsx @@ -3,7 +3,7 @@ import { useChapterGeneralSettings, useChapterReaderSettings, } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import React, { useEffect, useState } from 'react'; import VoicePickerModal from '../Modals/VoicePickerModal'; import { useBoolean } from '@hooks'; diff --git a/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx b/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx index 508a6de110..3ff76072b9 100644 --- a/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx +++ b/src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx @@ -11,7 +11,7 @@ import { useChapterGeneralSettings, useChapterReaderSettings, } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import GeneralSettings from './Settings/GeneralSettings'; diff --git a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx index 3a0a4bd54d..8b5229e7dd 100644 --- a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx @@ -13,7 +13,7 @@ import { import { Repository } from '@database/types'; import { useBackHandler, useBoolean } from '@hooks/index'; import { usePlugins } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import AddRepositoryModal from './components/AddRepositoryModal'; diff --git a/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx b/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx index a03ef992a1..80f3fed5ac 100644 --- a/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx @@ -5,7 +5,7 @@ import { Portal, TextInput } from 'react-native-paper'; import { Button, Modal } from '@components/index'; import { Repository } from '@database/types'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx b/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx index 9d70c35bb2..04ad133e79 100644 --- a/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx @@ -6,7 +6,7 @@ import { Button, Modal } from '@components/index'; import { Repository } from '@database/types'; import { deleteRepositoryById } from '@database/queries/RepositoryQueries'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; diff --git a/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx b/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx index 8a539707dc..1b4ca738d2 100644 --- a/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx @@ -7,7 +7,7 @@ import { IconButtonV2 } from '@components'; import { Repository } from '@database/types'; import { useBoolean } from '@hooks/index'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; import { Portal } from 'react-native-paper'; diff --git a/src/screens/settings/SettingsScreen.tsx b/src/screens/settings/SettingsScreen.tsx index 17c81fddd9..7ddb34eb48 100644 --- a/src/screens/settings/SettingsScreen.tsx +++ b/src/screens/settings/SettingsScreen.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ScrollView, StyleSheet } from 'react-native'; import { Appbar, List, SafeAreaView } from '@components'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { SettingsScreenProps } from '@navigators/types'; diff --git a/src/screens/settings/SettingsTrackerScreen.tsx b/src/screens/settings/SettingsTrackerScreen.tsx index b74c4560d9..68a867ff73 100644 --- a/src/screens/settings/SettingsTrackerScreen.tsx +++ b/src/screens/settings/SettingsTrackerScreen.tsx @@ -3,7 +3,7 @@ import { View, StyleSheet } from 'react-native'; import { Portal, Text, Button, Provider } from 'react-native-paper'; import { getTracker, useTracker } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { Appbar, List, Modal, SafeAreaView } from '@components'; import { TrackerSettingsScreenProps } from '@navigators/types'; import { getString } from '@strings/translations'; diff --git a/src/screens/updates/UpdatesScreen.tsx b/src/screens/updates/UpdatesScreen.tsx index a6c0b4b923..4446851ae2 100644 --- a/src/screens/updates/UpdatesScreen.tsx +++ b/src/screens/updates/UpdatesScreen.tsx @@ -10,7 +10,7 @@ import { } from '@components'; import { useSearch } from '@hooks'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { getString } from '@strings/translations'; import { ThemeColors } from '@theme/types'; import UpdatesSkeletonLoading from './components/UpdatesSkeletonLoading'; diff --git a/src/screens/updates/components/UpdateNovelCard.tsx b/src/screens/updates/components/UpdateNovelCard.tsx index a9c1bd803a..e9a1827193 100644 --- a/src/screens/updates/components/UpdateNovelCard.tsx +++ b/src/screens/updates/components/UpdateNovelCard.tsx @@ -12,7 +12,7 @@ import { List } from 'react-native-paper'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import ChapterItem from '@screens/novel/components/ChapterItem'; import { useDownload, useUpdates } from '@hooks/persisted'; -import { useTheme } from '@providers/ThemeProvider'; +import { useTheme } from '@providers/Providers'; import { RootStackParamList } from '@navigators/types'; import { FlatList } from 'react-native-gesture-handler'; import { defaultCover } from '@plugins/helpers/constants'; diff --git a/src/services/ServiceManager.ts b/src/services/ServiceManager.ts index dc5b3b1a08..e78388beec 100644 --- a/src/services/ServiceManager.ts +++ b/src/services/ServiceManager.ts @@ -26,7 +26,7 @@ type taskNames = | 'MIGRATE_NOVEL' | 'DOWNLOAD_CHAPTER'; -export type BackgroundTask = +export type GeneralBackgroundTask = | { name: 'IMPORT_EPUB'; data: { @@ -52,6 +52,11 @@ export type DownloadChapterTask = { data: { chapterId: number; novelName: string; chapterName: string }; }; +export type BackgroundTask = Extract< + GeneralBackgroundTask, + { name: T } +>; + export type BackgroundTaskMetadata = { name: string; isRunning: boolean; @@ -59,8 +64,8 @@ export type BackgroundTaskMetadata = { progressText: string | undefined; }; -export type QueuedBackgroundTask = { - task: BackgroundTask; +export type QueuedBackgroundTask = { + task: BackgroundTask; meta: BackgroundTaskMetadata; id: string; }; @@ -91,10 +96,10 @@ export default class ServiceManager { return BackgroundService.isRunning(); } - isMultiplicableTask(task: BackgroundTask) { + isMultiplicableTask(task: GeneralBackgroundTask) { return ( ['DOWNLOAD_CHAPTER', 'IMPORT_EPUB', 'MIGRATE_NOVEL'] as Array< - BackgroundTask['name'] + GeneralBackgroundTask['name'] > ).includes(task.name); } @@ -243,7 +248,7 @@ export default class ServiceManager { static async launch() { // retrieve class instance because this is running in different context const manager = ServiceManager.manager; - const doneTasks: Record = { + const doneTasks: Record = { 'IMPORT_EPUB': 0, 'UPDATE_LIBRARY': 0, 'DRIVE_BACKUP': 0, @@ -288,11 +293,11 @@ export default class ServiceManager { content: { title: 'Background tasks done', body: Object.keys(doneTasks) - .filter(key => doneTasks[key as BackgroundTask['name']] > 0) + .filter(key => doneTasks[key as GeneralBackgroundTask['name']] > 0) .map( key => `${getString(`notifications.${key as taskNames}`)}: ${ - doneTasks[key as BackgroundTask['name']] + doneTasks[key as GeneralBackgroundTask['name']] }`, ) .join('\n'), @@ -302,7 +307,7 @@ export default class ServiceManager { } } - getTaskName(task: BackgroundTask) { + getTaskName(task: GeneralBackgroundTask) { switch (task.name) { case 'DOWNLOAD_CHAPTER': return 'Download ' + task.data.novelName; @@ -332,7 +337,7 @@ export default class ServiceManager { return getMMKVObject>(this.STORE_KEY) || []; } - addTask(tasks: BackgroundTask | BackgroundTask[]) { + addTask(tasks: GeneralBackgroundTask | GeneralBackgroundTask[]) { let currentTasks = this.getTaskList(); // @ts-expect-error Older version can still have tasks with old format currentTasks = currentTasks.filter(task => !task?.name); @@ -362,7 +367,7 @@ export default class ServiceManager { } } - removeTasksByName(name: BackgroundTask['name']) { + removeTasksByName(name: GeneralBackgroundTask['name']) { const taskList = this.getTaskList(); if (taskList[0]?.task?.name === name) { this.pause(); From acc818faa76258b8744492e8d953df33dc8b17da Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:49:02 +0200 Subject: [PATCH 14/18] better download handling for chapterItems --- src/hooks/persisted/novel/useNovelChapters.ts | 1 + src/providers/context/QueueContext.tsx | 3 +- .../components/Chapter/ChapterBookmark.tsx | 23 ++ .../Chapter/ChapterDownloadButtons.tsx | 103 ++++----- src/screens/novel/components/ChapterItem.tsx | 44 ++-- .../novel/components/NovelScreenList.tsx | 8 +- .../novel/context/NovelChaptersContext.tsx | 29 +++ .../updates/components/ChapterItem.tsx | 206 ------------------ .../updates/components/UpdateNovelCard.tsx | 4 +- src/services/ServiceManager.ts | 134 ++++++++---- 10 files changed, 198 insertions(+), 357 deletions(-) create mode 100644 src/screens/novel/components/Chapter/ChapterBookmark.tsx delete mode 100644 src/screens/updates/components/ChapterItem.tsx diff --git a/src/hooks/persisted/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts index 0fd359834e..9d78976a4a 100644 --- a/src/hooks/persisted/novel/useNovelChapters.ts +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -274,6 +274,7 @@ const useNovelChapters = () => { novelSettings.filter, currentPage, setChapters, + batchInformation, ]); // #endregion diff --git a/src/providers/context/QueueContext.tsx b/src/providers/context/QueueContext.tsx index d60c0101c3..fd51bb6308 100644 --- a/src/providers/context/QueueContext.tsx +++ b/src/providers/context/QueueContext.tsx @@ -40,8 +40,7 @@ export function QueueContextProvider({ importQueue, downloadQueue, }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [taskQueue.length]); + }, [taskQueue]); return ( diff --git a/src/screens/novel/components/Chapter/ChapterBookmark.tsx b/src/screens/novel/components/Chapter/ChapterBookmark.tsx new file mode 100644 index 0000000000..f750a2b7a1 --- /dev/null +++ b/src/screens/novel/components/Chapter/ChapterBookmark.tsx @@ -0,0 +1,23 @@ +import { IconButtonV2 } from '@components'; +import { useTheme } from '@providers/Providers'; +import { memo } from 'react'; +import { StyleSheet } from 'react-native'; + +const _ChapterBookmarkButton: React.FC = () => { + const theme = useTheme(); + + return ( + + ); +}; +export const ChapterBookmarkButton = memo(_ChapterBookmarkButton); + +const styles = StyleSheet.create({ + iconButtonLeft: { marginLeft: 2 }, +}); diff --git a/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx b/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx index b2d92386b9..8a401e5ac4 100644 --- a/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx +++ b/src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx @@ -1,37 +1,31 @@ -import React, { memo, useMemo, useCallback, useState } from 'react'; +import React, { memo, useMemo, useState } from 'react'; import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'; import { Menu, overlay } from 'react-native-paper'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import Color from 'color'; import { getString } from '@strings/translations'; import type { MD3ThemeType } from '@theme/types'; -import { useTheme } from '@providers/Providers'; -import { IconButtonV2 } from '@components'; -interface Props { - chapterId: number; - isDownloaded: boolean | undefined; - isDownloading?: boolean; +export interface DownloadButtonProps { + status: 'idle' | 'downloading' | 'downloaded'; theme: MD3ThemeType; - deleteChapter: () => void; - downloadChapter: () => void; - setChapterDownloaded?: (value: boolean) => void; + onDelete: () => void; + onDownload: () => void; + // Optional: Add progress for a progress bar if desired + // progress?: number; // 0-100 } -const DownloadButtonControlled: React.FC = ({ - isDownloaded: isDownloadedProp, - isDownloading, +const _DownloadButton: React.FC = ({ + status, theme, - deleteChapter, - downloadChapter, - setChapterDownloaded, + onDelete, + onDownload, + // progress, }) => { - // local menu state only const [menuVisible, setMenuVisible] = useState(false); - const [isDownloaded, setIsDownloaded] = useState(isDownloadedProp); - const rippleStyle = useMemo( - () => ({ color: Color(theme.primary).alpha(0.12).string() }), + const rippleColor = useMemo( + () => Color(theme.primary).alpha(0.12).string(), [theme.primary], ); @@ -39,23 +33,8 @@ const DownloadButtonControlled: React.FC = ({ () => ({ backgroundColor: overlay(2, theme.surface) }), [theme.surface], ); - const menuTitleStyle = useMemo( - () => ({ color: theme.onSurface }), - [theme.onSurface], - ); - - const onDelete = useCallback(() => { - deleteChapter(); - setMenuVisible(false); - setIsDownloaded(false); - }, [deleteChapter]); - const onDownload = useCallback(() => { - downloadChapter(); - setIsDownloaded(true); - }, [downloadChapter]); - - if (isDownloading || isDownloaded === undefined) { + if (status === 'downloading') { return ( = ({ ); } - if (isDownloaded) { + if (status === 'downloaded') { return ( = ({ setMenuVisible(true)} - android_ripple={rippleStyle} + android_ripple={{ color: rippleColor }} > = ({ contentStyle={menuContentStyle} > { + onDelete(); + setMenuVisible(false); + }} title={getString('common.delete')} - titleStyle={menuTitleStyle} + titleStyle={{ color: theme.onSurface }} /> ); } + // status === 'idle' or 'error' return ( = ({ ); }; -function areEqual(a: Props, b: Props) { +function areEqualDownloadButton( + prev: DownloadButtonProps, + next: DownloadButtonProps, +) { return ( - a.isDownloaded === b.isDownloaded && - a.isDownloading === b.isDownloading && - a.deleteChapter === b.deleteChapter && - a.downloadChapter === b.downloadChapter && - a.theme.primary === b.theme.primary && - a.theme.surface === b.theme.surface && - a.theme.outline === b.theme.outline + prev.status === next.status && + // prev.progress === next.progress && // if you add progress prop + prev.theme.primary === next.theme.primary && + prev.theme.onSurface === next.theme.onSurface && + prev.theme.outline === next.theme.outline && + prev.theme.surface === next.theme.surface && // for menu overlay + prev.onDelete === next.onDelete && + prev.onDownload === next.onDownload ); } -export const DownloadButton = memo(DownloadButtonControlled, areEqual); - -const ChapterBookmarkButtonI: React.FC = () => { - const theme = useTheme(); - - return ( - - ); -}; -export const ChapterBookmarkButton = memo(ChapterBookmarkButtonI); +export const DownloadButton = memo(_DownloadButton, areEqualDownloadButton); const styles = StyleSheet.create({ activityIndicator: { margin: 3.5, padding: 5 }, @@ -160,5 +132,4 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - iconButtonLeft: { marginLeft: 2 }, }); diff --git a/src/screens/novel/components/ChapterItem.tsx b/src/screens/novel/components/ChapterItem.tsx index dc29bc4c3e..55106c892e 100644 --- a/src/screens/novel/components/ChapterItem.tsx +++ b/src/screens/novel/components/ChapterItem.tsx @@ -1,13 +1,10 @@ import React, { memo, ReactNode, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, Pressable } from 'react-native'; -import { - ChapterBookmarkButton, - DownloadButton, -} from './Chapter/ChapterDownloadButtons'; +import { DownloadButton } from './Chapter/ChapterDownloadButtons'; import { ChapterInfo } from '@database/types'; import { getString } from '@strings/translations'; import { useTheme } from '@providers/Providers'; -import { isChapterDownloaded } from '@database/queries/ChapterQueries'; +import { ChapterBookmarkButton } from './Chapter/ChapterBookmark'; interface ChapterItemProps { isDownloading?: boolean; @@ -20,7 +17,6 @@ interface ChapterItemProps { onSelectPress?: (chapter: ChapterInfo) => void; onSelectLongPress?: (chapter: ChapterInfo) => void; navigateToChapter: (chapter: ChapterInfo) => void; - setChapterDownloaded?: (value: boolean) => void; left?: ReactNode; isLocal: boolean; isUpdateCard?: boolean; @@ -28,7 +24,7 @@ interface ChapterItemProps { } const ChapterItem: React.FC = ({ - isDownloading: isDownloadingProp, + isDownloading, isBookmarked, chapter, showChapterTitles, @@ -38,37 +34,29 @@ const ChapterItem: React.FC = ({ onSelectPress, onSelectLongPress, navigateToChapter, - setChapterDownloaded, isLocal, left, isUpdateCard, novelName, }) => { + const theme = useTheme(); const { - id, name, unread, releaseTime, bookmark, chapterNumber, progress, - isDownloaded: isDownloadedProp, + isDownloaded, } = chapter; - chapter; - if (name === 'Chapter 2: Rice Farming 101') { - console.log(name, isDownloadedProp); - } - - const isBookmarkedLocal = isBookmarked ?? bookmark; - const theme = useTheme(); + const downloadStatus = useMemo(() => { + if (isDownloading) return 'downloading'; + if (isDownloaded) return 'downloaded'; + return 'idle'; + }, [isDownloaded, isDownloading]); - const isDownloaded = useMemo(() => { - if (!isDownloadingProp) { - return isChapterDownloaded(id) ?? false; - } - return isDownloadedProp; - }, [id, isDownloadedProp, isDownloadingProp]); + const isBookmarkedLocal = isBookmarked ?? bookmark; const highlight = useMemo( () => ({ backgroundColor: theme.rippleColor }), @@ -211,13 +199,10 @@ const ChapterItem: React.FC = ({ {!isLocal && ( )} @@ -249,7 +234,6 @@ function areEqual(prev: ChapterItemProps, next: ChapterItemProps) { prev.left === next.left && prev.downloadChapter === next.downloadChapter && prev.deleteChapter === next.deleteChapter && - prev.setChapterDownloaded === next.setChapterDownloaded && prev.onSelectPress === next.onSelectPress && prev.onSelectLongPress === next.onSelectLongPress && prev.navigateToChapter === next.navigateToChapter diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index 73ea2cecdf..46d11f7b83 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -68,7 +68,7 @@ const NovelScreenList = ({ getNextChapterBatch, }: NovelScreenListProps) => { const { getNovel, novel, loading } = useNovelState(); - const { chapters, deleteChapter, fetching, batchInformation, updateChapter } = + const { chapters, deleteChapter, fetching, batchInformation } = useNovelChapters(); const { novelSettings, sortAndFilterChapters, setShowChapterTitles } = useNovelSettings(); @@ -261,7 +261,7 @@ const NovelScreenList = ({ ); const renderItem = useCallback( - ({ item, index }: { item: ChapterInfo; index: number }) => { + ({ item }: { item: ChapterInfo }) => { if (novel.id === 'NO_ID') { return null; } @@ -279,9 +279,6 @@ const NovelScreenList = ({ onSelectLongPress={onSelectLongPress} navigateToChapter={navigateToChapter} novelName={novel.name} - setChapterDownloaded={(value: boolean) => - updateChapter?.(index, { isDownloaded: value }) - } /> ); }, @@ -295,7 +292,6 @@ const NovelScreenList = ({ navigateToChapter, deleteChapter, downloadChapter, - updateChapter, ], ); diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx index 146289c7a3..5728c03ce7 100644 --- a/src/screens/novel/context/NovelChaptersContext.tsx +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -18,6 +18,10 @@ import { } from 'react'; import { NovelStateContext } from './NovelStateContext'; import { getString } from '@strings/translations'; +import ServiceManager, { + DownloadChapterTask, + QueuedBackgroundTask, +} from '@services/ServiceManager'; interface BatchInfo { batch: number; @@ -90,6 +94,8 @@ export const NovelChaptersContext = createContext( null, ); +const serviceManager = ServiceManager.manager; + // #endregion // #region provider export function NovelChaptersContextProvider({ @@ -208,6 +214,29 @@ export function NovelChaptersContextProvider({ ], ); + useEffect(() => { + const handleTaskCompletion = (task: QueuedBackgroundTask) => { + if (task.task.name === 'DOWNLOAD_CHAPTER') { + const chapterId = (task.task as DownloadChapterTask).data.chapterId; + if (task.meta.finalStatus === 'completed') { + mutateChapters(chs => { + const chIndex = chs.findIndex(ch => ch.id === chapterId); + if (chIndex !== -1) { + chs[chIndex].isDownloaded = true; + } + return chs; + }); + } + } + }; + + serviceManager.addCompletionListener(handleTaskCompletion); + + return () => { + serviceManager.removeCompletionListener(handleTaskCompletion); + }; + }, [mutateChapters]); + useEffect(() => { let cancelled = false; diff --git a/src/screens/updates/components/ChapterItem.tsx b/src/screens/updates/components/ChapterItem.tsx deleted file mode 100644 index b09cf9d7e6..0000000000 --- a/src/screens/updates/components/ChapterItem.tsx +++ /dev/null @@ -1,206 +0,0 @@ -/* eslint-disable react-native/no-inline-styles */ -import React, { memo, ReactNode } from 'react'; -import { View, Text, StyleSheet, Pressable } from 'react-native'; -import color from 'color'; - -import { ThemeColors } from '@theme/types'; -import { ChapterInfo } from '@database/types'; -import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; -import { getString } from '@strings/translations'; -import { - ChapterBookmarkButton, - DownloadButton, -} from '@screens/novel/components/Chapter/ChapterDownloadButtons'; - -interface ChapterItemProps { - isDownloading?: boolean; - isBookmarked?: boolean; - chapter: ChapterInfo; - theme: ThemeColors; - showChapterTitles: boolean; - isSelected?: boolean; - downloadChapter: () => void; - deleteChapter: () => void; - onSelectPress?: (chapter: ChapterInfo) => void; - onSelectLongPress?: (chapter: ChapterInfo) => void; - navigateToChapter: (chapter: ChapterInfo) => void; - setChapterDownloaded?: (value: boolean) => void; - left?: ReactNode; - isLocal: boolean; - isUpdateCard?: boolean; - novelName: string; -} - -const ChapterItem: React.FC = ({ - isDownloading, - isBookmarked, - chapter, - theme, - showChapterTitles, - downloadChapter, - deleteChapter, - isSelected, - onSelectPress, - onSelectLongPress, - navigateToChapter, - setChapterDownloaded, - isLocal, - left, - isUpdateCard, - novelName, -}) => { - const { id, name, unread, releaseTime, bookmark, chapterNumber, progress } = - chapter; - - isBookmarked ??= bookmark; - - return ( - - { - if (onSelectPress) { - onSelectPress(chapter); - } else { - navigateToChapter(chapter); - } - }} - onLongPress={() => onSelectLongPress?.(chapter)} - android_ripple={{ color: theme.rippleColor }} - > - - {left} - {isBookmarked ? : null} - - {isUpdateCard ? ( - - {novelName} - - ) : null} - - {unread ? ( - - ) : null} - - - {showChapterTitles - ? name - : getString('novelScreen.chapterChapnum', { - num: chapterNumber, - })} - - - - {releaseTime && !isUpdateCard ? ( - - {releaseTime} - - ) : null} - {!isUpdateCard && progress && progress > 0 && chapter.unread ? ( - - {chapter.releaseTime ? '• ' : null} - {getString('novelScreen.progress', { progress })} - - ) : null} - - - - {!isLocal ? ( - - ) : null} - - - ); -}; - -export default memo(ChapterItem); - -const styles = StyleSheet.create({ - chapterCardContainer: { - alignItems: 'center', - flexDirection: 'row', - height: 64, - justifyContent: 'space-between', - paddingHorizontal: 16, - paddingVertical: 8, - }, - row: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - }, - text: { - fontSize: 12, - }, - unreadIcon: { - marginRight: 4, - }, -}); diff --git a/src/screens/updates/components/UpdateNovelCard.tsx b/src/screens/updates/components/UpdateNovelCard.tsx index e9a1827193..1f2372b4a2 100644 --- a/src/screens/updates/components/UpdateNovelCard.tsx +++ b/src/screens/updates/components/UpdateNovelCard.tsx @@ -158,7 +158,6 @@ const UpdateNovelCard: React.FC = ({ isUpdateCard novelName={chapterListInfo.novelName} chapter={item} - theme={theme} showChapterTitles={false} downloadChapter={() => handleDownloadChapter(item)} deleteChapter={() => deleteChapter(item)} @@ -182,13 +181,12 @@ const UpdateNovelCard: React.FC = ({ return ( c.task.data.chapterId === chapterList[0]?.id, )} - isUpdateCard novelName={chapterListInfo.novelName} chapter={chapterList[0]} - theme={theme} showChapterTitles={false} downloadChapter={() => handleDownloadChapter(chapterList[0])} deleteChapter={() => deleteChapter(chapterList[0])} diff --git a/src/services/ServiceManager.ts b/src/services/ServiceManager.ts index e78388beec..4901e86bae 100644 --- a/src/services/ServiceManager.ts +++ b/src/services/ServiceManager.ts @@ -47,6 +47,7 @@ export type GeneralBackgroundTask = | { name: 'SELF_HOST_RESTORE'; data: SelfHostData } | { name: 'MIGRATE_NOVEL'; data: MigrateNovelData } | DownloadChapterTask; + export type DownloadChapterTask = { name: 'DOWNLOAD_CHAPTER'; data: { chapterId: number; novelName: string; chapterName: string }; @@ -62,6 +63,7 @@ export type BackgroundTaskMetadata = { isRunning: boolean; progress: number | undefined; progressText: string | undefined; + finalStatus?: 'completed' | 'failed'; // NEW: Outcome for UI }; export type QueuedBackgroundTask = { @@ -83,6 +85,10 @@ export default class ServiceManager { currentPendingUpdate = 0; private static instance?: ServiceManager; + // NEW: Listeners for task completion/failure + private taskCompletionListeners: Set<(task: QueuedBackgroundTask) => void> = + new Set(); + private constructor() {} static get manager() { @@ -104,6 +110,16 @@ export default class ServiceManager { ).includes(task.name); } + public addCompletionListener(listener: (task: QueuedBackgroundTask) => void) { + this.taskCompletionListeners.add(listener); + } + + public removeCompletionListener( + listener: (task: QueuedBackgroundTask) => void, + ) { + this.taskCompletionListeners.delete(listener); + } + async start() { if (!this.isRunning) { const notificationsAllowed = await askForPostNotificationsPermission(); @@ -132,18 +148,21 @@ export default class ServiceManager { transformer: (meta: BackgroundTaskMetadata) => BackgroundTaskMetadata, ) { const taskList = [...this.getTaskList()]; + // Ensure taskList[0] exists before proceeding + if (!taskList[0]) { + return; + } + + const updatedMeta = transformer(taskList[0].meta); taskList[0] = { ...taskList[0], - meta: transformer(taskList[0].meta), + meta: updatedMeta, }; - if ( - taskList[0].meta.isRunning && - taskList[0].task.name !== 'DOWNLOAD_CHAPTER' - ) { + if (updatedMeta.isRunning && taskList[0].task.name !== 'DOWNLOAD_CHAPTER') { const now = Date.now(); if (now - this.lastNotifUpdate > 1000) { - const delay = 1000 - now - this.lastNotifUpdate; + const delay = Math.max(0, 1000 - (now - this.lastNotifUpdate)); // Ensure positive delay const id = ++this.currentPendingUpdate; setTimeout(() => { if (this.currentPendingUpdate !== id) { @@ -213,35 +232,73 @@ export default class ServiceManager { task.task.name === 'DOWNLOAD_CHAPTER' ? this.getProgressForNotification(task, startingTasks) : null; + + // Set task to running and update notification before executing + this.setMeta(prevMeta => ({ ...prevMeta, isRunning: true })); await BackgroundService.updateNotification({ taskTitle: task.meta.name, taskDesc: task.meta.progressText ?? '', progressBar: { indeterminate: progress === null, max: 100, - value: progress == null ? 0 : progress, + value: progress === null ? 0 : progress, }, }); this.lastNotifUpdate = Date.now(); this.currentPendingUpdate = 0; - switch (task.task.name) { - case 'IMPORT_EPUB': - return importEpub(task.task.data, this.setMeta.bind(this)); - case 'UPDATE_LIBRARY': - return updateLibrary(task.task.data || {}, this.setMeta.bind(this)); - case 'DRIVE_BACKUP': - return createDriveBackup(task.task.data, this.setMeta.bind(this)); - case 'DRIVE_RESTORE': - return driveRestore(task.task.data, this.setMeta.bind(this)); - case 'SELF_HOST_BACKUP': - return createSelfHostBackup(task.task.data, this.setMeta.bind(this)); - case 'SELF_HOST_RESTORE': - return selfHostRestore(task.task.data, this.setMeta.bind(this)); - case 'MIGRATE_NOVEL': - return migrateNovel(task.task.data, this.setMeta.bind(this)); - case 'DOWNLOAD_CHAPTER': - return downloadChapter(task.task.data, this.setMeta.bind(this)); + let success = false; + try { + switch (task.task.name) { + case 'IMPORT_EPUB': + await importEpub(task.task.data, this.setMeta.bind(this)); + break; + case 'UPDATE_LIBRARY': + await updateLibrary(task.task.data || {}, this.setMeta.bind(this)); + break; + case 'DRIVE_BACKUP': + await createDriveBackup(task.task.data, this.setMeta.bind(this)); + break; + case 'DRIVE_RESTORE': + await driveRestore(task.task.data, this.setMeta.bind(this)); + break; + case 'SELF_HOST_BACKUP': + await createSelfHostBackup(task.task.data, this.setMeta.bind(this)); + break; + case 'SELF_HOST_RESTORE': + await selfHostRestore(task.task.data, this.setMeta.bind(this)); + break; + case 'MIGRATE_NOVEL': + await migrateNovel(task.task.data, this.setMeta.bind(this)); + break; + case 'DOWNLOAD_CHAPTER': + await downloadChapter(task.task.data, this.setMeta.bind(this)); + break; + } + success = true; + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(`Task ${task.task.name} (ID: ${task.id}) failed:`, error); + await Notifications.scheduleNotificationAsync({ + content: { + title: `Failed: ${task.meta.name}`, + body: error?.message || String(error), + }, + trigger: null, + }); + } finally { + //? Notify listeners about task completion/failure + const finalTask: QueuedBackgroundTask = { + ...task, + meta: { + ...task.meta, + isRunning: false, + finalStatus: success ? 'completed' : 'failed', + progress: success ? 100 : undefined, + progressText: success ? getString('common.done') : 'Failed', + }, + }; + this.taskCompletionListeners.forEach(listener => listener(finalTask)); } } @@ -259,35 +316,23 @@ export default class ServiceManager { 'DOWNLOAD_CHAPTER': 0, }; const startingTasks = manager.getTaskList(); - const tasksSet = new Set(startingTasks.map(t => t.id)); + while (BackgroundService.isRunning()) { const currentTasks = manager.getTaskList(); const currentTask = currentTasks[0]; + if (!currentTask) { break; } - //Add any newly queued tasks to the starting tasks list - const newtasks = currentTasks.filter(t => !tasksSet.has(t.id)); - startingTasks.push(...newtasks); - newtasks.forEach(t => tasksSet.add(t.id)); + await manager.executeTask(currentTask, startingTasks); - try { - await manager.executeTask(currentTask, startingTasks); - doneTasks[currentTask.task.name] += 1; - } catch (error: any) { - await Notifications.scheduleNotificationAsync({ - content: { - title: currentTask.meta.name, - body: error?.message || String(error), - }, - trigger: null, - }); - } finally { - setMMKVObject(manager.STORE_KEY, manager.getTaskList().slice(1)); - } + // After execution, remove the current task from the queue + setMMKVObject(manager.STORE_KEY, currentTasks.slice(1)); + doneTasks[currentTask.task.name] += 1; } + // Final notification when all tasks are done if (manager.getTaskList().length === 0) { await Notifications.scheduleNotificationAsync({ content: { @@ -349,7 +394,7 @@ export default class ServiceManager { ); if (addableTasks.length) { const newTasks: QueuedBackgroundTask[] = addableTasks.map(task => ({ - task, + task: task as BackgroundTask, meta: { name: this.getTaskName(task), isRunning: false, @@ -358,6 +403,7 @@ export default class ServiceManager { task.name === 'DOWNLOAD_CHAPTER' ? task.data.chapterName : undefined, + finalStatus: undefined, // Initialize finalStatus }, id: makeId(), })); From 7fd60167f32928006bbd2da613559d59adc82c40 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:28:18 +0200 Subject: [PATCH 15/18] reworked library context --- src/components/Context/LibraryContext.tsx | 122 ++++++++++++++--- src/database/queries/LibraryQueries.ts | 17 ++- src/hooks/persisted/library/index.ts | 3 + src/hooks/persisted/library/useCategories.ts | 38 ++++++ .../persisted/library/useLibraryActions.ts | 30 +++++ .../persisted/library/useLibraryNovels.ts | 84 ++++++++++++ src/hooks/persisted/useDownload.ts | 2 + src/hooks/persisted/useImport.ts | 14 +- src/hooks/persisted/useSettings.ts | 20 ++- src/screens/library/hooks/useLibrary.ts | 126 ------------------ src/services/ServiceManager.ts | 7 +- 11 files changed, 296 insertions(+), 167 deletions(-) create mode 100644 src/hooks/persisted/library/index.ts create mode 100644 src/hooks/persisted/library/useCategories.ts create mode 100644 src/hooks/persisted/library/useLibraryActions.ts create mode 100644 src/hooks/persisted/library/useLibraryNovels.ts delete mode 100644 src/screens/library/hooks/useLibrary.ts diff --git a/src/components/Context/LibraryContext.tsx b/src/components/Context/LibraryContext.tsx index 16abe82c84..0db269fdbf 100644 --- a/src/components/Context/LibraryContext.tsx +++ b/src/components/Context/LibraryContext.tsx @@ -1,35 +1,127 @@ -import React, { createContext, useContext } from 'react'; -import { - useLibrary, - UseLibraryReturnType, -} from '@screens/library/hooks/useLibrary'; -import { useLibrarySettings } from '@hooks/persisted'; -import { LibrarySettings } from '@hooks/persisted/useSettings'; +// src/components/Context/LibraryContext.tsx +import React, { + createContext, + useContext, + useMemo, + useCallback, + useState, +} from 'react'; -// type Library = Category & { novels: LibraryNovelInfo[] }; +// Import existing settings hook +import { + useLibrarySettings, + LibrarySettings, +} from '@hooks/persisted/useSettings'; +import { NovelInfo } from '@database/types'; +import { + ExtendedCategory, + useFetchCategories, +} from '@hooks/persisted/library/useCategories'; +import { useLibraryNovels } from '@hooks/persisted/library/useLibraryNovels'; +import { useLibraryActions } from '@hooks/persisted/library/useLibraryActions'; +import ServiceManager, { QueuedBackgroundTask } from '@services/ServiceManager'; -type LibraryContextType = UseLibraryReturnType & { +// Define the shape of the context value +type LibraryContextType = { + library: NovelInfo[]; + categories: ExtendedCategory[]; + isLoading: boolean; settings: LibrarySettings; + refetchLibrary: () => void; + novelInLibrary: (pluginId: string, novelPath: string) => boolean; + switchNovelToLibrary: (novelPath: string, pluginId: string) => Promise; }; const defaultValue = {} as LibraryContextType; const LibraryContext = createContext(defaultValue); +interface LibraryContextProviderProps { + children: React.ReactNode; +} + export function LibraryContextProvider({ children, -}: { - children: React.ReactNode; -}) { - const useLibraryParams = useLibrary(); +}: LibraryContextProviderProps) { + const [searchText, setSearchText] = useState(''); const settings = useLibrarySettings(); + const { categories, categoriesLoading, refreshCategories } = + useFetchCategories(); + const { novels, novelsLoading, refetchNovels, refetchNovel } = + useLibraryNovels({ + sortOrder: settings.sortOrder, + filter: settings.filter, + searchText: searchText, + downloadedOnlyMode: settings.downloadedOnlyMode, + }); + + const { switchNovelToLibrary } = useLibraryActions({ + refreshCategories, + refetchNovels, + }); + + const isLoading = categoriesLoading || novelsLoading; + + const refetchLibrary = useCallback(async () => { + await Promise.all([refreshCategories(), refetchNovels()]); + }, [refreshCategories, refetchNovels]); + + const novelInLibrary = useCallback( + (pluginId: string, novelPath: string) => + novels?.some( + novel => novel.pluginId === pluginId && novel.path === novelPath, + ), + [novels], + ); + + const handleQueueChange = useCallback( + (task: QueuedBackgroundTask) => { + if (task.meta.finalStatus !== 'completed') return; + if (task.task.name === 'IMPORT_EPUB') { + refetchLibrary(); + } else if (task.task.name === 'DOWNLOAD_CHAPTER') { + refetchNovel(task.task.data.novelId); + } + }, + [refetchLibrary, refetchNovel], + ); + ServiceManager.manager.addCompletionListener(handleQueueChange); + + const contextValue = useMemo( + () => ({ + library: novels, + categories, + isLoading, + settings, + refetchLibrary, + novelInLibrary, + switchNovelToLibrary, + setSearchText, + }), + [ + novels, + categories, + isLoading, + settings, + refetchLibrary, + novelInLibrary, + switchNovelToLibrary, + ], + ); + return ( - + {children} ); } export const useLibraryContext = (): LibraryContextType => { - return useContext(LibraryContext); + const context = useContext(LibraryContext); + if (context === defaultValue) { + throw new Error( + 'useLibraryContext must be used within a LibraryContextProvider', + ); + } + return context; }; diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index 44f0aa86e7..7e15ae399f 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -2,12 +2,17 @@ import { LibraryFilter } from '@screens/library/constants/constants'; import { LibraryNovelInfo, NovelInfo } from '../types'; import { getAllSync } from '../utils/helpers'; -export const getLibraryNovelsFromDb = ( - sortOrder?: string, - filter?: string, - searchText?: string, - downloadedOnlyMode?: boolean, -): NovelInfo[] => { +export const getLibraryNovelsFromDb = ({ + sortOrder, + filter, + searchText, + downloadedOnlyMode, +}: { + sortOrder?: string; + filter?: string; + searchText?: string; + downloadedOnlyMode?: boolean; +}): NovelInfo[] => { let query = 'SELECT * FROM Novel WHERE inLibrary = 1'; if (filter) { diff --git a/src/hooks/persisted/library/index.ts b/src/hooks/persisted/library/index.ts new file mode 100644 index 0000000000..9a54ec0583 --- /dev/null +++ b/src/hooks/persisted/library/index.ts @@ -0,0 +1,3 @@ +export * from './useCategories'; +export * from './useLibraryActions'; +export * from './useLibraryNovels'; diff --git a/src/hooks/persisted/library/useCategories.ts b/src/hooks/persisted/library/useCategories.ts new file mode 100644 index 0000000000..b3a60d68de --- /dev/null +++ b/src/hooks/persisted/library/useCategories.ts @@ -0,0 +1,38 @@ +// src/hooks/library/useCategories.ts +import { useCallback, useEffect, useState } from 'react'; +import { getCategoriesFromDb } from '@database/queries/CategoryQueries'; +import { Category } from '@database/types'; + +export type ExtendedCategory = Category & { novelIds: number[] }; + +export const useFetchCategories = () => { + const [categories, setCategories] = useState([]); + const [categoriesLoading, setCategoriesLoading] = useState(true); + + const fetchCategories = useCallback(async () => { + setCategoriesLoading(true); + try { + const dbCategories = await getCategoriesFromDb(); + const res = dbCategories.map(c => ({ + ...c, + novelIds: (c.novelIds ?? '').split(',').map(Number).filter(Boolean), + })); + setCategories(res); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to fetch categories:', error); + } finally { + setCategoriesLoading(false); + } + }, []); + + useEffect(() => { + fetchCategories(); + }, [fetchCategories]); + + return { + categories, + categoriesLoading, + refreshCategories: fetchCategories, + }; +}; diff --git a/src/hooks/persisted/library/useLibraryActions.ts b/src/hooks/persisted/library/useLibraryActions.ts new file mode 100644 index 0000000000..b2a140fb6b --- /dev/null +++ b/src/hooks/persisted/library/useLibraryActions.ts @@ -0,0 +1,30 @@ +// src/hooks/library/useLibraryActions.ts +import { useCallback } from 'react'; +import { switchNovelToLibraryQuery } from '@database/queries/NovelQueries'; + +interface UseLibraryActionsOptions { + refreshCategories: () => Promise; + refetchNovels: () => Promise; +} + +export const useLibraryActions = ({ + refreshCategories, + refetchNovels, +}: UseLibraryActionsOptions) => { + const switchNovelToLibrary = useCallback( + async (novelPath: string, pluginId: string) => { + try { + await switchNovelToLibraryQuery(novelPath, pluginId); + await Promise.all([refreshCategories(), refetchNovels()]); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to switch novel to library:', error); + } + }, + [refreshCategories, refetchNovels], + ); + + return { + switchNovelToLibrary, + }; +}; diff --git a/src/hooks/persisted/library/useLibraryNovels.ts b/src/hooks/persisted/library/useLibraryNovels.ts new file mode 100644 index 0000000000..36967aa8b4 --- /dev/null +++ b/src/hooks/persisted/library/useLibraryNovels.ts @@ -0,0 +1,84 @@ +// src/hooks/library/useLibraryNovels.ts +import { useCallback, useState } from 'react'; +import { useFocusEffect } from '@react-navigation/native'; +import { getLibraryNovelsFromDb } from '@database/queries/LibraryQueries'; +import { NovelInfo } from '@database/types'; +import { + LibraryFilter, + LibrarySortOrder, +} from '../../../screens/library/constants/constants'; + +interface UseLibraryNovelsOptions { + sortOrder?: LibrarySortOrder; + filter?: LibraryFilter; + searchText: string; + downloadedOnlyMode?: boolean; +} + +export const useLibraryNovels = ({ + sortOrder, + filter, + searchText, + downloadedOnlyMode = false, +}: UseLibraryNovelsOptions) => { + const [novels, setNovels] = useState([]); + const [novelsLoading, setNovelsLoading] = useState(true); + + const fetchNovels = useCallback(async () => { + setNovelsLoading(true); + try { + const fetchedNovels = await getLibraryNovelsFromDb({ + sortOrder, + filter, + searchText, + downloadedOnlyMode, + }); + setNovels(fetchedNovels); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to fetch library novels:', error); + } finally { + setNovelsLoading(false); + } + }, [downloadedOnlyMode, filter, searchText, sortOrder]); + + const fetchNovel = useCallback(async (novelId: number) => { + setNovelsLoading(true); + try { + const fetchedNovels = await getLibraryNovelsFromDb({ + filter: `id = ${novelId}`, + }); + setNovels(prevNovels => { + const novelIndex = fetchedNovels.findIndex( + novel => novel.id === novelId, + ); + if (novelIndex !== -1) { + return [ + ...prevNovels.slice(0, novelIndex), + fetchedNovels[novelIndex], + ...prevNovels.slice(novelIndex + 1), + ]; + } + return [...prevNovels, fetchedNovels[0]]; + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to fetch library novels:', error); + } finally { + setNovelsLoading(false); + } + }, []); + + useFocusEffect( + useCallback(() => { + fetchNovels(); + }, [fetchNovels]), + ); + + return { + novels, + novelsLoading, + refetchNovels: fetchNovels, + refetchNovel: fetchNovel, + }; +}; diff --git a/src/hooks/persisted/useDownload.ts b/src/hooks/persisted/useDownload.ts index 2f39cff8ee..872026959c 100644 --- a/src/hooks/persisted/useDownload.ts +++ b/src/hooks/persisted/useDownload.ts @@ -16,6 +16,7 @@ export default function useDownload() { data: { chapterId: chapter.id, novelName: novel.name, + novelId: novel.id, chapterName: chapter.name, }, }), @@ -29,6 +30,7 @@ export default function useDownload() { data: { chapterId: chapter.id, novelName: novel.name, + novelId: novel.id, chapterName: chapter.name, }, })), diff --git a/src/hooks/persisted/useImport.ts b/src/hooks/persisted/useImport.ts index 0b0811277e..90962d0916 100644 --- a/src/hooks/persisted/useImport.ts +++ b/src/hooks/persisted/useImport.ts @@ -1,17 +1,8 @@ -import { useLibraryContext } from '@components/Context/LibraryContext'; -import { useQueue } from '@providers/Providers'; import ServiceManager from '@services/ServiceManager'; import { DocumentPickerResult } from 'expo-document-picker'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; export default function useImport() { - const { refetchLibrary } = useLibraryContext(); - const { importQueue } = useQueue(); - - useEffect(() => { - refetchLibrary(); - }, [importQueue.length, refetchLibrary]); - const importNovel = useCallback((pickedNovel: DocumentPickerResult) => { if (pickedNovel.canceled) return; ServiceManager.manager.addTask( @@ -27,14 +18,13 @@ export default function useImport() { const hookContent = useMemo( () => ({ - importQueue, importNovel, resumeImport: () => ServiceManager.manager.resume(), pauseImport: () => ServiceManager.manager.pause(), cancelImport: () => ServiceManager.manager.removeTasksByName('IMPORT_EPUB'), }), - [importNovel, importQueue], + [importNovel], ); return hookContent; diff --git a/src/hooks/persisted/useSettings.ts b/src/hooks/persisted/useSettings.ts index 22d6b6a77d..3d7a58bea7 100644 --- a/src/hooks/persisted/useSettings.ts +++ b/src/hooks/persisted/useSettings.ts @@ -5,6 +5,7 @@ import { } from '@screens/library/constants/constants'; import { useMMKVObject } from 'react-native-mmkv'; import { Voice } from 'expo-speech'; +import { useCallback, useMemo } from 'react'; export const APP_SETTINGS = 'APP_SETTINGS'; export const BROWSE_SETTINGS = 'BROWSE_SETTINGS'; @@ -245,13 +246,18 @@ export const useLibrarySettings = () => { const [librarySettings, setSettings] = useMMKVObject(LIBRARY_SETTINGS); - const setLibrarySettings = (value: Partial) => - setSettings({ ...librarySettings, ...value }); - - return { - ...{ ...defaultLibrarySettings, ...librarySettings }, - setLibrarySettings, - }; + const setLibrarySettings = useCallback( + (value: Partial) => + setSettings({ ...librarySettings, ...value }), + [librarySettings, setSettings], + ); + + return useMemo(() => { + return { + ...{ ...defaultLibrarySettings, ...librarySettings }, + setLibrarySettings, + }; + }, [librarySettings, setLibrarySettings]); }; export const useChapterGeneralSettings = () => { diff --git a/src/screens/library/hooks/useLibrary.ts b/src/screens/library/hooks/useLibrary.ts deleted file mode 100644 index bce2d76899..0000000000 --- a/src/screens/library/hooks/useLibrary.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { useCallback, useState } from 'react'; -import { useFocusEffect } from '@react-navigation/native'; - -import { getCategoriesFromDb } from '@database/queries/CategoryQueries'; -import { getLibraryNovelsFromDb } from '@database/queries/LibraryQueries'; - -import { Category, NovelInfo } from '@database/types'; - -import { useLibrarySettings } from '@hooks/persisted'; -import { LibrarySortOrder } from '../constants/constants'; -import { switchNovelToLibraryQuery } from '@database/queries/NovelQueries'; - -// type Library = Category & { novels: LibraryNovelInfo[] }; -export type ExtendedCategory = Category & { novelIds: number[] }; -export type UseLibraryReturnType = { - library: NovelInfo[]; - categories: ExtendedCategory[]; - isLoading: boolean; - setCategories: React.Dispatch>; - refreshCategories: () => Promise; - setLibrary: React.Dispatch>; - novelInLibrary: (pluginId: string, novelPath: string) => boolean; - switchNovelToLibrary: (novelPath: string, pluginId: string) => Promise; - refetchLibrary: () => void; - setLibrarySearchText: (text: string) => void; -}; - -export const useLibrary = (): UseLibraryReturnType => { - const { - filter, - sortOrder = LibrarySortOrder.DateAdded_DESC, - downloadedOnlyMode = false, - } = useLibrarySettings(); - - const [library, setLibrary] = useState([]); - const [categories, setCategories] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [searchText, setSearchText] = useState(''); - - const refreshCategories = useCallback(async () => { - const dbCategories = getCategoriesFromDb(); - - const res = dbCategories.map(c => ({ - ...c, - novelIds: (c.novelIds ?? '').split(',').map(Number), - })); - - setCategories(res); - }, []); - - const getLibrary = useCallback(async () => { - if (searchText) { - setIsLoading(true); - } - - const [_, novels] = await Promise.all([ - refreshCategories(), - getLibraryNovelsFromDb(sortOrder, filter, searchText, downloadedOnlyMode), - ]); - - setLibrary(novels); - setIsLoading(false); - }, [downloadedOnlyMode, filter, refreshCategories, searchText, sortOrder]); - - const novelInLibrary = useCallback( - (pluginId: string, novelPath: string) => - library?.some( - novel => novel.pluginId === pluginId && novel.path === novelPath, - ), - [library], - ); - - const switchNovelToLibrary = useCallback( - async (novelPath: string, pluginId: string) => { - await switchNovelToLibraryQuery(novelPath, pluginId); - - // Important to get correct chapters count - // Count is set by sql trigger - refreshCategories(); - const novels = getLibraryNovelsFromDb( - sortOrder, - filter, - searchText, - downloadedOnlyMode, - ); - - setLibrary(novels); - }, - [downloadedOnlyMode, filter, refreshCategories, searchText, sortOrder], - ); - - useFocusEffect(() => { - getLibrary(); - }); - - return { - library, - categories, - isLoading, - setLibrary, - setCategories, - refreshCategories, - novelInLibrary, - switchNovelToLibrary, - refetchLibrary: getLibrary, - setLibrarySearchText: setSearchText, - }; -}; - -export const useLibraryNovels = () => { - const [library, setLibrary] = useState([]); - - const getLibrary = async () => { - const novels = getLibraryNovelsFromDb(); - - setLibrary(novels); - }; - - useFocusEffect( - useCallback(() => { - getLibrary(); - }, []), - ); - - return { library, setLibrary }; -}; diff --git a/src/services/ServiceManager.ts b/src/services/ServiceManager.ts index 4901e86bae..51b97d1aa9 100644 --- a/src/services/ServiceManager.ts +++ b/src/services/ServiceManager.ts @@ -50,7 +50,12 @@ export type GeneralBackgroundTask = export type DownloadChapterTask = { name: 'DOWNLOAD_CHAPTER'; - data: { chapterId: number; novelName: string; chapterName: string }; + data: { + chapterId: number; + novelName: string; + novelId: number; + chapterName: string; + }; }; export type BackgroundTask = Extract< From c302a12c6ff53b083f5274eab8ef4e50d64c37ae Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:26:04 +0200 Subject: [PATCH 16/18] Library restructure --- src/components/Actionbar/Actionbar.tsx | 4 +- src/components/Context/LibraryContext.tsx | 4 + src/components/SearchbarV2/SearchbarV2.tsx | 2 +- src/screens/library/LibraryScreen.tsx | 523 ++++-------------- .../components/LibraryActionBarAndFab.tsx | 152 +++++ .../library/components/LibraryHeader.tsx | 106 ++++ .../library/components/LibraryTabs.tsx | 272 +++++++++ src/screens/novel/NovelScreen.tsx | 3 +- .../Chapter/ChapterDownloadButtons.tsx | 8 +- src/screens/novel/components/ChapterItem.tsx | 13 +- .../novel/components/NovelScreenList.tsx | 5 +- 11 files changed, 649 insertions(+), 443 deletions(-) create mode 100644 src/screens/library/components/LibraryActionBarAndFab.tsx create mode 100644 src/screens/library/components/LibraryHeader.tsx create mode 100644 src/screens/library/components/LibraryTabs.tsx diff --git a/src/components/Actionbar/Actionbar.tsx b/src/components/Actionbar/Actionbar.tsx index df64b45a8e..e517f3cf37 100644 --- a/src/components/Actionbar/Actionbar.tsx +++ b/src/components/Actionbar/Actionbar.tsx @@ -12,12 +12,12 @@ import MaterialCommunityIcons from '@react-native-vector-icons/material-design-i import { MaterialDesignIconName } from '@type/icon'; import Animated, { SlideInDown, SlideOutDown } from 'react-native-reanimated'; -type Action = { +export type Action = { icon: MaterialDesignIconName; onPress: () => void; }; -interface ActionbarProps { +export interface ActionbarProps { active: boolean; actions: Action[]; viewStyle?: StyleProp; diff --git a/src/components/Context/LibraryContext.tsx b/src/components/Context/LibraryContext.tsx index 0db269fdbf..0b8681269e 100644 --- a/src/components/Context/LibraryContext.tsx +++ b/src/components/Context/LibraryContext.tsx @@ -27,6 +27,8 @@ type LibraryContextType = { categories: ExtendedCategory[]; isLoading: boolean; settings: LibrarySettings; + searchText: string; + setSearchText: React.Dispatch>; refetchLibrary: () => void; novelInLibrary: (pluginId: string, novelPath: string) => boolean; switchNovelToLibrary: (novelPath: string, pluginId: string) => Promise; @@ -93,6 +95,7 @@ export function LibraryContextProvider({ categories, isLoading, settings, + searchText, refetchLibrary, novelInLibrary, switchNovelToLibrary, @@ -103,6 +106,7 @@ export function LibraryContextProvider({ categories, isLoading, settings, + searchText, refetchLibrary, novelInLibrary, switchNovelToLibrary, diff --git a/src/components/SearchbarV2/SearchbarV2.tsx b/src/components/SearchbarV2/SearchbarV2.tsx index b11f483719..bb7918bfca 100644 --- a/src/components/SearchbarV2/SearchbarV2.tsx +++ b/src/components/SearchbarV2/SearchbarV2.tsx @@ -6,7 +6,7 @@ import { ThemeColors } from '../../theme/types'; import { Menu } from 'react-native-paper'; import { MaterialDesignIconName } from '@type/icon'; -interface RightIcon { +export interface RightIcon { iconName: MaterialDesignIconName; color?: string; onPress: () => void; diff --git a/src/screens/library/LibraryScreen.tsx b/src/screens/library/LibraryScreen.tsx index ac4f8d02ce..5737e1f372 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -5,95 +5,54 @@ import React, { useRef, useState, } from 'react'; -import { - StyleProp, - StyleSheet, - Text, - TextStyle, - useWindowDimensions, - View, -} from 'react-native'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; -import { - NavigationState, - SceneRendererProps, - TabBar, - TabView, -} from 'react-native-tab-view'; -import Color from 'color'; -import { SearchbarV2, Button, SafeAreaView } from '@components/index'; -import { LibraryView } from './components/LibraryListView'; -import LibraryBottomSheet from './components/LibraryBottomSheet/LibraryBottomSheet'; -import { Banner } from './components/Banner'; -import { Actionbar } from '@components/Actionbar/Actionbar'; +import { SafeAreaView } from '@components/index'; +import LibraryBottomSheet from '@screens/library/components/LibraryBottomSheet/LibraryBottomSheet'; +import SetCategoryModal from '@screens/novel/components/SetCategoriesModal'; -import { useAppSettings, useHistory } from '@hooks/persisted'; +import { useAppSettings } from '@hooks/persisted'; import { useTheme } from '@providers/Providers'; -import { useSearch, useBackHandler, useBoolean } from '@hooks'; -import { getString } from '@strings/translations'; -import { FAB, Portal } from 'react-native-paper'; -import { - markAllChaptersRead, - markAllChaptersUnread, -} from '@database/queries/ChapterQueries'; -import { removeNovelsFromLibrary } from '@database/queries/NovelQueries'; -import SetCategoryModal from '@screens/novel/components/SetCategoriesModal'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading'; -import { Row } from '@components/Common'; +import { useBackHandler, useBoolean } from '@hooks'; import { LibraryScreenProps } from '@navigators/types'; -import { NovelInfo } from '@database/types'; import * as DocumentPicker from 'expo-document-picker'; import ServiceManager from '@services/ServiceManager'; import useImport from '@hooks/persisted/useImport'; -import { ThemeColors } from '@theme/types'; import { useLibraryContext } from '@components/Context/LibraryContext'; -type State = NavigationState<{ - key: string; - title: string; -}>; - -type TabViewLabelProps = { - route: { - id: number; - name: string; - sort: number; - novelIds: number[]; - key: string; - title: string; - }; - labelText?: string; - focused: boolean; - color: string; - allowFontScaling?: boolean; - style?: StyleProp; -}; +import { getString } from '@strings/translations'; +import LibraryActionBarAndFab from './components/LibraryActionBarAndFab'; +import LibraryTabs from './components/LibraryTabs'; +import LibraryHeader from './components/LibraryHeader'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; const LibraryScreen = ({ navigation }: LibraryScreenProps) => { - const { searchText, setSearchText, clearSearchbar } = useSearch(); const theme = useTheme(); - const styles = createStyles(theme); const { left: leftInset, right: rightInset } = useSafeAreaInsets(); + const bottomSheetStyle = useMemo( + () => ({ marginLeft: leftInset, marginRight: rightInset }), + [leftInset, rightInset], + ); + const { library, categories, refetchLibrary, isLoading, + searchText, + setSearchText, settings: { showNumberOfNovels, downloadedOnlyMode, incognitoMode }, } = useLibraryContext(); + const clearSearchbar = () => setSearchText(''); + const { importNovel } = useImport(); const { useLibraryFAB = false } = useAppSettings(); - const { isLoading: isHistoryLoading, history, error } = useHistory(); - - const layout = useWindowDimensions(); - const bottomSheetRef = useRef(null); - const [index, setIndex] = useState(0); + const [index, setIndex] = useState(0); // For TabView + const [selectedNovelIds, setSelectedNovelIds] = useState([]); const { value: setCategoryModalVisible, @@ -101,15 +60,8 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { setFalse: closeSetCategoryModal, } = useBoolean(); - const handleClearSearchbar = () => { - clearSearchbar(); - }; - - const [selectedNovelIds, setSelectedNovelIds] = useState([]); - const currentNovels = useMemo(() => { if (!categories.length) return []; - const ids = categories[index].novelIds; return library.filter(l => ids.includes(l.id)); }, [categories, index, library]); @@ -119,7 +71,6 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { setSelectedNovelIds([]); return true; } - return false; }); @@ -128,30 +79,47 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { navigation.addListener('tabPress', e => { if (navigation.isFocused()) { e.preventDefault(); - bottomSheetRef.current?.present?.(); } }), [navigation], ); - const searchbarPlaceholder = - selectedNovelIds.length === 0 - ? getString('libraryScreen.searchbar') - : `${selectedNovelIds.length} selected`; - - function openRandom() { - const novels = currentNovels; - const randomNovel = novels[Math.floor(Math.random() * novels.length)]; - if (randomNovel) { - navigation.navigate('ReaderStack', { - screen: 'Novel', - params: randomNovel, + // Callbacks for Header + const onSearchbarLeftIconPress = useCallback(() => { + if (selectedNovelIds.length > 0) { + setSelectedNovelIds([]); + } + }, [selectedNovelIds.length]); + + const onSelectAllNovels = useCallback(() => { + setSelectedNovelIds(currentNovels.map(novel => novel.id)); + }, [currentNovels]); + + const onFilterPress = useCallback(() => { + bottomSheetRef.current?.present(); + }, []); + + const onUpdateLibrary = useCallback(() => { + ServiceManager.manager.addTask({ + name: 'UPDATE_LIBRARY', + }); + }, []); + + const onUpdateCategory = useCallback(() => { + const currentCategory = categories[index]; + if (currentCategory && currentCategory.id !== 2) { + ServiceManager.manager.addTask({ + name: 'UPDATE_LIBRARY', + data: { + categoryId: currentCategory.id, + categoryName: currentCategory.name, + }, }); } - } + }, [categories, index]); - const pickAndImport = useCallback(() => { + const pickAndImportCallback = useCallback(() => { DocumentPicker.getDocumentAsync({ type: 'application/epub+zip', copyToCacheDirectory: true, @@ -159,268 +127,56 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { }).then(importNovel); }, [importNovel]); - const renderTabBar = useCallback( - (props: SceneRendererProps & { navigationState: State }) => { - return categories.length ? ( - - ) : null; - }, - [ - categories.length, - styles.tabBar, - styles.tabBarIndicator, - styles.tabStyle, - theme.isDark, - theme.primary, - theme.rippleColor, - theme.secondary, - theme.surface, - ], - ); - const renderScene = useCallback( - ({ - route, - }: { - route: { - id: number; - name: string; - sort: number; - novelIds: number[]; - key: string; - title: string; - }; - }) => { - const ids = route.novelIds; - const unfilteredNovels = library.filter(l => ids.includes(l.id)); - - const novels = unfilteredNovels.filter( - n => - n.name.toLowerCase().includes(searchText.toLowerCase()) || - (n.author?.toLowerCase().includes(searchText.toLowerCase()) ?? false), - ); - - return isLoading ? ( - - ) : ( - <> - {searchText ? ( -