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 ? (
-