diff --git a/App.tsx b/App.tsx index 90d1aa6427..ab802ff665 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 { Providers } from './src/providers/Providers'; 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/package-lock.json b/package-lock.json index 64d2453a58..23b289700c 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" } @@ -2219,9 +2216,9 @@ } }, "node_modules/@cd-z/epub-constructor": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cd-z/epub-constructor/-/epub-constructor-3.0.1.tgz", - "integrity": "sha512-gI8eZ2+YnkflO48p+IjRVjFMJXv/86MYNdL6cxZUNd+GPEv3XBebg/hrHKXvTGTcBA3502CnwBf0hWoQyx3OoQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@cd-z/epub-constructor/-/epub-constructor-3.0.3.tgz", + "integrity": "sha512-0Q4DBZ+H6kOihAfREu1dcfwkuwIoOtpXK24im24STP89aIb1HoAhGd3MKLKsZK3tIPVsdfjI2esPnYpmhVXxrw==", "license": "MIT", "dependencies": { "sanitize-html": "^2.13.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/components/Actionbar/Actionbar.tsx b/src/components/Actionbar/Actionbar.tsx index 5086004e23..e517f3cf37 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/Providers'; import React from 'react'; import { Dimensions, @@ -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/AppErrorBoundary/AppErrorBoundary.tsx b/src/components/AppErrorBoundary/AppErrorBoundary.tsx index 95e3f40722..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 '@hooks/persisted'; +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 c531c1cb46..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 '@hooks/persisted'; +import { useTheme } from '@providers/Providers'; import { ThemeProp } from 'react-native-paper/lib/typescript/types'; interface ButtonProps extends Partial { diff --git a/src/components/Context/LibraryContext.tsx b/src/components/Context/LibraryContext.tsx index 16abe82c84..e75b3e1ad5 100644 --- a/src/components/Context/LibraryContext.tsx +++ b/src/components/Context/LibraryContext.tsx @@ -1,35 +1,137 @@ -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; + searchText: string; + setSearchText: React.Dispatch>; + refetchLibrary: () => void; + novelInLibrary: (pluginId: string, novelPath: string) => boolean; + switchNovelToLibrary: (novelPath: string, pluginId: string) => Promise; + refreshCategories: () => Promise; + setCategories: React.Dispatch>; }; 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, setCategories } = + 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, + searchText, + refetchLibrary, + novelInLibrary, + switchNovelToLibrary, + setSearchText, + refreshCategories, + setCategories, + }), + [ + novels, + categories, + isLoading, + settings, + searchText, + refetchLibrary, + novelInLibrary, + switchNovelToLibrary, + refreshCategories, + setCategories, + ], + ); + 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/components/EmptyView.tsx b/src/components/EmptyView.tsx index 99af6b1276..c4db84c016 100644 --- a/src/components/EmptyView.tsx +++ b/src/components/EmptyView.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@hooks/persisted'; +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 b4e89e5bff..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 '@hooks/persisted'; +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 2bbce938e1..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 '@hooks/persisted'; +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 a865510900..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 '@hooks/persisted'; +import { useTheme } from '@providers/Providers'; import { Modal } from '@components'; interface NewUpdateDialogProps { 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/components/Skeleton/Skeleton.tsx b/src/components/Skeleton/Skeleton.tsx index acc9194941..eed7b097e4 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/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 25b742d8bc..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 '@hooks/persisted'; +import { useTheme } from '@providers/Providers'; interface SwitchProps { value: boolean; diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 024737682a..7189152848 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/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/common/useFullscreenMode.ts b/src/hooks/common/useFullscreenMode.ts index ed3f45c48d..299083797b 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/Providers'; 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 566413b08a..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'; @@ -11,6 +10,11 @@ export { } from './useSettings'; export { default as usePlugins } from './usePlugins'; export { getTracker, useTracker } from './useTracker'; -export { useTrackedNovel, useNovel } from './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/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..039e95bc1b --- /dev/null +++ b/src/hooks/persisted/library/useCategories.ts @@ -0,0 +1,39 @@ +// 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, + setCategories, + }; +}; 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..697a12b8f6 --- /dev/null +++ b/src/hooks/persisted/library/useLibraryNovels.ts @@ -0,0 +1,79 @@ +// 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 = prevNovels.findIndex(novel => novel?.id === novelId); + + if (novelIndex !== -1) { + prevNovels[novelIndex] = fetchedNovels[0]; + } + return prevNovels; + }); + } 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/novel/useNovelChapters.ts b/src/hooks/persisted/novel/useNovelChapters.ts new file mode 100644 index 0000000000..9d78976a4a --- /dev/null +++ b/src/hooks/persisted/novel/useNovelChapters.ts @@ -0,0 +1,324 @@ +/* eslint-disable no-console */ +import { NovelChaptersContext } from '@screens/novel/context/NovelChaptersContext'; +import { useCallback, useContext, 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, + getPageChaptersBatched, + updateChapterProgress as _updateChapterProgress, +} from '@database/queries/ChapterQueries'; +import { ChapterInfo } from '@database/types'; +import { getString } from '@strings/translations'; +import { showToast } from '@utils/showToast'; +import useNovelState from './useNovelState'; +import useNovelPages from './useNovelPages'; +import useNovelSettings from './useNovelSettings'; + +const useNovelChapters = () => { + const NovelChapters = useContext(NovelChaptersContext); + if (!NovelChapters) { + throw new Error( + 'useNovelChapters must be used within NovelChaptersContextProvider', + ); + } + const { + chapters, + fetching, + batchInformation, + setChapters, + getChapters, + updateChapter, + extendChapters, + mutateChapters, + } = NovelChapters; + const { novel, loading } = useNovelState(); + const { pages, pageIndex, page } = useNovelPages(); + const { novelSettings } = useNovelSettings(); + const currentPage = pages[pageIndex]; + + const getNextChapterBatch = useCallback(() => { + const nextBatch = batchInformation.batch + 1; + if (!loading && page && nextBatch <= batchInformation.total) { + let newChapters: ChapterInfo[] = []; + + try { + newChapters = + getPageChaptersBatched( + novel.id, + novel.name, + novelSettings.sort, + novelSettings.filter, + page, + nextBatch, + ) || []; + } catch (error) { + console.error('teaser', error); + } + extendChapters(newChapters, { ...batchInformation, batch: nextBatch }); + } + }, [ + batchInformation, + loading, + page, + extendChapters, + novel.id, + novel.name, + novelSettings.sort, + novelSettings.filter, + ]); + + // #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 (!loading) { + _markPreviuschaptersRead(chapterId, novel.id); + mutateChapters(chs => + chs.map(chapter => + chapter.id <= chapterId ? { ...chapter, unread: false } : chapter, + ), + ); + } + }, + [loading, mutateChapters, novel.id], + ); + + 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 (!loading) { + _markPreviousChaptersUnread(chapterId, novel.id); + mutateChapters(chs => + chs.map(chapter => + chapter.id <= chapterId ? { ...chapter, unread: true } : chapter, + ), + ); + } + }, + [loading, mutateChapters, novel.id], + ); + + 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( + async (_chapter: ChapterInfo) => { + if (!loading) { + _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 })); + }); + } + }, + [loading, mutateChapters, novel.id, novel.pluginId], + ); + + const deleteChapters = useCallback( + (_chaters: ChapterInfo[]) => { + if (!loading) { + _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; + }), + ); + }); + } + }, + [loading, novel.pluginId, novel.id, mutateChapters], + ); + + const refreshChapters = useCallback(() => { + if (!loading && !fetching) { + _getPageChapters( + novel.id, + novel.name, + novelSettings.sort, + novelSettings.filter, + currentPage, + ).then(chs => { + setChapters(chs, { ...batchInformation }); + }); + } + }, [ + loading, + fetching, + novel.id, + novel.name, + novelSettings.sort, + novelSettings.filter, + currentPage, + setChapters, + batchInformation, + ]); + + // #endregion + + const result = useMemo( + () => ({ + chapters, + fetching, + batchInformation, + getChapters, + updateChapter, + getNextChapterBatch, + bookmarkChapters, + markPreviouschaptersRead, + markChapterRead, + updateChapterProgress, + markChaptersRead, + markPreviousChaptersUnread, + markChaptersUnread, + deleteChapter, + deleteChapters, + refreshChapters, + }), + [ + chapters, + fetching, + batchInformation, + getChapters, + updateChapter, + getNextChapterBatch, + bookmarkChapters, + markPreviouschaptersRead, + markChapterRead, + updateChapterProgress, + markChaptersRead, + markPreviousChaptersUnread, + markChaptersUnread, + deleteChapter, + deleteChapters, + refreshChapters, + ], + ); + + return result; +}; + +export default useNovelChapters; 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/hooks/persisted/novel/useNovelPages.ts b/src/hooks/persisted/novel/useNovelPages.ts new file mode 100644 index 0000000000..54a4ab1597 --- /dev/null +++ b/src/hooks/persisted/novel/useNovelPages.ts @@ -0,0 +1,59 @@ +import { getCustomPages } from '@database/queries/ChapterQueries'; +import { NovelInfo } from '@database/types'; +import { NovelPageContext } from '@screens/novel/context/NovelPageContext'; +import { useCallback, useContext, useMemo } from 'react'; + +const useNovelPages = () => { + const novelPage = useContext(NovelPageContext); + if (!novelPage) { + throw new Error( + 'useNovelPages must be used within NovelPageContextProvider', + ); + } + const { pages, setPages, pageIndex, setPageIndex } = 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], + ); + + const openPage = useCallback( + (index: number) => { + setPageIndex(index); + }, + [setPageIndex], + ); + + const page = pages[pageIndex]; + + const result = useMemo( + () => ({ + page, + pages, + pageIndex, + setPageIndex, + calculatePages, + openPage, + }), + [page, pages, pageIndex, setPageIndex, calculatePages, openPage], + ); + + return result; +}; + +export default useNovelPages; diff --git a/src/hooks/persisted/novel/useNovelSettings.ts b/src/hooks/persisted/novel/useNovelSettings.ts new file mode 100644 index 0000000000..8e1ed6cf78 --- /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( + 'useNovelSettings 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/hooks/persisted/novel/useNovelState.ts b/src/hooks/persisted/novel/useNovelState.ts new file mode 100644 index 0000000000..14814686c9 --- /dev/null +++ b/src/hooks/persisted/novel/useNovelState.ts @@ -0,0 +1,145 @@ +import { useLibraryContext } from '@components/Context/LibraryContext'; +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 { getString } from '@strings/translations'; +import { showToast } from '@utils/showToast'; +import { StorageAccessFramework } from 'expo-file-system'; +import { useCallback, useContext, useMemo } from 'react'; +import { NovelInfo } from '@database/types'; + +type NovelState = { + path: string; + pluginId: string; + setNovel: (novel: NovelInfo) => void; + followNovel: () => void; + setCustomNovelCover: () => Promise; + saveNovelCover: () => Promise; + getNovel: () => Promise; +} & ( + | { novel: NovelInfo; loading: false } + | { novel: RouteNovel; loading: true } +); + +const useNovelState = (): NovelState => { + const novelState = useContext(NovelStateContext); + if (!novelState) { + throw new Error( + 'useNovelState must be used within NovelStateContextProvider', + ); + } + + const { novel, setNovel, path, pluginId, loading, getNovel } = novelState; + + const { switchNovelToLibrary } = useLibraryContext(); + + const followNovel = useCallback(() => { + switchNovelToLibrary(path, pluginId).then(() => { + if (!loading) { + setNovel({ + ...novel, + inLibrary: !novel?.inLibrary, + }); + } + }); + }, [loading, novel, path, pluginId, setNovel, switchNovelToLibrary]); + + const setCustomNovelCover = useCallback(async () => { + if (loading) { + return; + } + const newCover = await pickCustomNovelCover(novel); + if (newCover) { + setNovel({ + ...novel, + cover: newCover, + }); + } + }, [loading, 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]); + + const result = useMemo( + () => ({ + novel, + loading, + path, + pluginId, + getNovel, + setNovel, + followNovel, + setCustomNovelCover, + saveNovelCover, + }), + [ + novel, + loading, + path, + pluginId, + getNovel, + setNovel, + followNovel, + setCustomNovelCover, + saveNovelCover, + ], + ); + + return result as NovelState; +}; + +export default useNovelState; diff --git a/src/hooks/persisted/novel/useTrackedNovel.ts b/src/hooks/persisted/novel/useTrackedNovel.ts new file mode 100644 index 0000000000..7ba87384be --- /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 { TrackerMetadata, getTracker } from '../useTracker'; +import { TRACKED_NOVEL_PREFIX } from '@utils/constants/mmkv'; + +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/hooks/persisted/useDownload.ts b/src/hooks/persisted/useDownload.ts index e4622b3a0c..872026959c 100644 --- a/src/hooks/persisted/useDownload.ts +++ b/src/hooks/persisted/useDownload.ts @@ -1,58 +1,55 @@ import { ChapterInfo, NovelInfo } from '@database/types'; -import ServiceManager, { - BackgroundTaskMetadata, - DownloadChapterTask, - QueuedBackgroundTask, -} from '@services/ServiceManager'; -import { useMemo } from 'react'; -import { useMMKVObject } from 'react-native-mmkv'; +import { useQueue } from '@providers/Providers'; +import ServiceManager from '@services/ServiceManager'; +import { useCallback, useMemo } from 'react'; 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 = (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, + novelId: novel.id, 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, + novelId: novel.id, + 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..90962d0916 100644 --- a/src/hooks/persisted/useImport.ts +++ b/src/hooks/persisted/useImport.ts @@ -1,23 +1,8 @@ -import { useLibraryContext } from '@components/Context/LibraryContext'; -import ServiceManager, { BackgroundTask } from '@services/ServiceManager'; +import ServiceManager from '@services/ServiceManager'; import { DocumentPickerResult } from 'expo-document-picker'; -import { useCallback, useEffect, useMemo } from 'react'; -import { useMMKVObject } from 'react-native-mmkv'; +import { useCallback, useMemo } from 'react'; 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], - ); - - useEffect(() => { - refetchLibrary(); - }, [importQueue, refetchLibrary]); - const importNovel = useCallback((pickedNovel: DocumentPickerResult) => { if (pickedNovel.canceled) return; ServiceManager.manager.addTask( @@ -30,18 +15,17 @@ export default function useImport() { })), ); }, []); - const resumeImport = () => ServiceManager.manager.resume(); - - const pauseImport = () => ServiceManager.manager.pause(); - const cancelImport = () => - ServiceManager.manager.removeTasksByName('IMPORT_EPUB'); + const hookContent = useMemo( + () => ({ + importNovel, + resumeImport: () => ServiceManager.manager.resume(), + pauseImport: () => ServiceManager.manager.pause(), + cancelImport: () => + ServiceManager.manager.removeTasksByName('IMPORT_EPUB'), + }), + [importNovel], + ); - return { - importQueue, - importNovel, - resumeImport, - pauseImport, - cancelImport, - }; + return hookContent; } diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts deleted file mode 100644 index 5102d17a4e..0000000000 --- a/src/hooks/persisted/useNovel.ts +++ /dev/null @@ -1,719 +0,0 @@ -/* 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 { - 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: '_', - -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 = { - showChapterTitles: true, -}; -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 - -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 [novelSettings = defaultNovelSettings, setNovelSettings] = - useMMKVObject( - `${NOVEL_SETTINSG_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 }, - ); - - 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); - }, - [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) { - setNovelSettings({ - showChapterTitles: novelSettings?.showChapterTitles, - sort, - filter, - }); - } - }, - [novel, novelSettings?.showChapterTitles, setNovelSettings], - ); - - const setShowChapterTitles = useCallback( - (v: boolean) => { - setNovelSettings({ ...novelSettings, showChapterTitles: v }); - }, - [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]; - - 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(() => { - 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]); - - 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( - () => ({ - 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 - -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/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/navigators/BottomNavigator.tsx b/src/navigators/BottomNavigator.tsx index eee21c02c7..26cc4e95e7 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/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 ed08f5b2f6..5e70c3b151 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/Providers'; import { useGithubUpdateChecker } from '@hooks/common/githubUpdateChecker'; /** diff --git a/src/navigators/ReaderStack.tsx b/src/navigators/ReaderStack.tsx index 41a2afe8ce..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/NovelContext'; +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/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..fd51bb6308 --- /dev/null +++ b/src/providers/context/QueueContext.tsx @@ -0,0 +1,54 @@ +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, + }; + }, [taskQueue]); + + return ( + + {children} + + ); +} + +export const useQueue = () => { + return useContext(QueueContext)!; +}; diff --git a/src/hooks/persisted/useTheme.ts b/src/providers/context/ThemeContext.tsx similarity index 79% rename from src/hooks/persisted/useTheme.ts rename to src/providers/context/ThemeContext.tsx index 684de33934..3b4f0695f0 100644 --- a/src/hooks/persisted/useTheme.ts +++ b/src/providers/context/ThemeContext.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..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 '@hooks/persisted'; +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 6720e1aff0..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 '@hooks/persisted'; +import { useTheme } from '@providers/Providers'; import { FilterTypes, FilterToValues, diff --git a/src/screens/Categories/CategoriesScreen.tsx b/src/screens/Categories/CategoriesScreen.tsx index b6d0411eb3..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 '@hooks/persisted'; +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 05d3f7a952..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 '@hooks/persisted'; +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 bf8bb017cc..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 '@hooks/persisted'; +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 d6a61ca713..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 '@hooks/persisted'; +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 042b3b32b0..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 '@hooks/persisted'; +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 2fb96afb24..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 '@hooks/persisted'; +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 ec3eab0bf6..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 '@hooks/persisted'; +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 e2e887b533..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 '@hooks/persisted'; +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 4f80881fe4..37521bfab8 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/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 eb3036edb8..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 '@hooks/persisted'; +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 1b7a2db186..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 '@hooks/persisted'; +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 71d629f3b0..e867f05c5f 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/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 6fdc19aa8c..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 '@hooks/persisted'; +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 80f66edce4..b61eccc3d3 100644 --- a/src/screens/browse/migration/Migration.tsx +++ b/src/screens/browse/migration/Migration.tsx @@ -3,17 +3,18 @@ import { StyleSheet, View, FlatList, Text, FlatListProps } from 'react-native'; import MigrationSourceItem from './MigrationSourceItem'; -import { usePlugins, useTheme } from '@hooks/persisted'; -import { useLibraryNovels } from '@screens/library/hooks/useLibrary'; +import { usePlugins } from '@hooks/persisted'; +import { useTheme } from '@providers/Providers'; import { Appbar } from '@components'; import { MigrationScreenProps } from '@navigators/types'; import { PluginItem } from '@plugins/types'; import { getString } from '@strings/translations'; +import { useLibraryContext } from '@components/Context/LibraryContext'; const Migration = ({ navigation }: MigrationScreenProps) => { const theme = useTheme(); - const { library } = useLibraryNovels(); + const { library } = useLibraryContext(); const { filteredInstalledPlugins } = usePlugins(); const novelsPerSource = (pluginId: string) => diff --git a/src/screens/browse/migration/MigrationNovels.tsx b/src/screens/browse/migration/MigrationNovels.tsx index 75d06d9642..30f83c6f17 100644 --- a/src/screens/browse/migration/MigrationNovels.tsx +++ b/src/screens/browse/migration/MigrationNovels.tsx @@ -1,17 +1,18 @@ 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/Providers'; import EmptyView from '@components/EmptyView'; import MigrationNovelList from './MigrationNovelList'; import { getPlugin } from '@plugins/pluginManager'; -import { useLibraryNovels } from '@screens/library/hooks/useLibrary'; import { Appbar, SafeAreaView } from '@components'; import GlobalSearchSkeletonLoading from '../loadingAnimation/GlobalSearchSkeletonLoading'; import { MigrateNovelScreenProps } from '@navigators/types'; import { NovelItem } from '@plugins/types'; +import { useLibraryContext } from '@components/Context/LibraryContext'; export interface SourceSearchResult { id: string; @@ -31,7 +32,7 @@ const MigrationNovels = ({ navigation, route }: MigrateNovelScreenProps) => { const [progress, setProgress] = useState(0); const [searchResults, setSearchResults] = useState([]); - const { library } = useLibraryNovels(); + const { library } = useLibraryContext(); const { filteredInstalledPlugins } = usePlugins(); diff --git a/src/screens/browse/settings/BrowseSettings.tsx b/src/screens/browse/settings/BrowseSettings.tsx index d0aecd79a0..a47acce5a2 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/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 dfd569e688..7557f135d6 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/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 048be3e3bf..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 '@hooks/persisted'; +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 14ca73cf70..5737e1f372 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -5,94 +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 { useAppSettings, useHistory, useTheme } from '@hooks/persisted'; -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 { SafeAreaView } from '@components/index'; +import LibraryBottomSheet from '@screens/library/components/LibraryBottomSheet/LibraryBottomSheet'; 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 { useAppSettings } from '@hooks/persisted'; +import { useTheme } from '@providers/Providers'; +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, @@ -100,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]); @@ -118,7 +71,6 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { setSelectedNovelIds([]); return true; } - return false; }); @@ -127,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, @@ -158,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 ? ( -