From 46578c88039db566928935da04e234e52560b0c0 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:06:11 +0200 Subject: [PATCH 01/10] fixed search function --- src/components/Context/LibraryContext.tsx | 33 ++++++++++--------- src/database/queries/LibraryQueries.ts | 7 ++-- .../persisted/library/useLibraryNovels.ts | 32 +++++++++++++----- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/components/Context/LibraryContext.tsx b/src/components/Context/LibraryContext.tsx index e75b3e1ad5..2f4878cb93 100644 --- a/src/components/Context/LibraryContext.tsx +++ b/src/components/Context/LibraryContext.tsx @@ -1,11 +1,5 @@ // src/components/Context/LibraryContext.tsx -import React, { - createContext, - useContext, - useMemo, - useCallback, - useState, -} from 'react'; +import React, { createContext, useContext, useMemo, useCallback } from 'react'; // Import existing settings hook import { @@ -21,7 +15,6 @@ import { useLibraryNovels } from '@hooks/persisted/library/useLibraryNovels'; import { useLibraryActions } from '@hooks/persisted/library/useLibraryActions'; import ServiceManager, { QueuedBackgroundTask } from '@services/ServiceManager'; -// Define the shape of the context value type LibraryContextType = { library: NovelInfo[]; categories: ExtendedCategory[]; @@ -46,18 +39,23 @@ interface LibraryContextProviderProps { export function LibraryContextProvider({ children, }: 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 { + novels, + novelsLoading, + refetchNovels, + refetchNovel, + searchText, + setSearchText, + clearSearchbar, + } = useLibraryNovels({ + sortOrder: settings.sortOrder, + filter: settings.filter, + downloadedOnlyMode: settings.downloadedOnlyMode, + }); const { switchNovelToLibrary } = useLibraryActions({ refreshCategories, @@ -104,6 +102,7 @@ export function LibraryContextProvider({ setSearchText, refreshCategories, setCategories, + clearSearchbar, }), [ novels, @@ -114,8 +113,10 @@ export function LibraryContextProvider({ refetchLibrary, novelInLibrary, switchNovelToLibrary, + setSearchText, refreshCategories, setCategories, + clearSearchbar, ], ); diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index 7e15ae399f..b0dbffb881 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -5,7 +5,7 @@ import { getAllSync } from '../utils/helpers'; export const getLibraryNovelsFromDb = ({ sortOrder, filter, - searchText, + searchText = '', downloadedOnlyMode, }: { sortOrder?: string; @@ -23,13 +23,16 @@ export const getLibraryNovelsFromDb = ({ } if (searchText) { + if (!searchText.startsWith('%')) searchText = `%${searchText}`; + if (!searchText.endsWith('%')) searchText = `${searchText}%`; query += ' AND name LIKE ? '; } if (sortOrder) { query += ` ORDER BY ${sortOrder} `; } - return getAllSync([query, [searchText ?? '']]); + + return getAllSync([query, [searchText]]); }; const getLibraryWithCategoryQuery = 'SELECT * FROM Novel WHERE inLibrary = 1'; diff --git a/src/hooks/persisted/library/useLibraryNovels.ts b/src/hooks/persisted/library/useLibraryNovels.ts index 697a12b8f6..7de09b9959 100644 --- a/src/hooks/persisted/library/useLibraryNovels.ts +++ b/src/hooks/persisted/library/useLibraryNovels.ts @@ -1,5 +1,5 @@ // src/hooks/library/useLibraryNovels.ts -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useFocusEffect } from '@react-navigation/native'; import { getLibraryNovelsFromDb } from '@database/queries/LibraryQueries'; import { NovelInfo } from '@database/types'; @@ -7,22 +7,22 @@ import { LibraryFilter, LibrarySortOrder, } from '../../../screens/library/constants/constants'; +import { useSearch } from '@hooks'; 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 { searchText, setSearchText, clearSearchbar } = useSearch(); const fetchNovels = useCallback(async () => { setNovelsLoading(true); @@ -70,10 +70,24 @@ export const useLibraryNovels = ({ }, [fetchNovels]), ); - return { - novels, - novelsLoading, - refetchNovels: fetchNovels, - refetchNovel: fetchNovel, - }; + return useMemo( + () => ({ + novels, + novelsLoading, + refetchNovels: fetchNovels, + refetchNovel: fetchNovel, + searchText, + setSearchText, + clearSearchbar, + }), + [ + novels, + novelsLoading, + fetchNovels, + fetchNovel, + searchText, + setSearchText, + clearSearchbar, + ], + ); }; From 5d9089ea01eb187e29cdb17d451f68be2514d236 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:32:03 +0200 Subject: [PATCH 02/10] fixed novelscreen bottom sheet options --- src/database/queries/ChapterQueries.ts | 8 ++++++-- src/screens/novel/components/NovelScreenList.tsx | 10 +++++++++- src/screens/novel/context/NovelChaptersContext.tsx | 14 +++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 7189152848..49cf867bdb 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -248,9 +248,13 @@ export const getPageChapters = ( ); }; -export const getChapterCount = (novelId: number, page: string = '1') => +export const getChapterCount = ( + novelId: number, + page: string = '1', + filter: string = '', +) => db.getFirstSync<{ 'COUNT(*)': number }>( - 'SELECT COUNT(*) FROM Chapter WHERE novelId = ? AND page = ?', + `SELECT COUNT(*) FROM Chapter WHERE novelId = ? AND page = ? ${filter}`, novelId, page, )?.['COUNT(*)'] ?? 0; diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index dc64f67af1..b01f0b0ad6 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -298,9 +298,17 @@ const NovelScreenList = ({ selectedLength: selected.length, novelId: novel.id, loading, + showChapterTitles, downloadingIds: Array.from(downloadingIds).sort().join(','), // Convert to string for stable comparison }), - [chapters, selected.length, novel.id, loading, downloadingIds], + [ + chapters, + selected.length, + novel.id, + loading, + showChapterTitles, + downloadingIds, + ], ); const keyExtractor = useCallback((item: ChapterInfo) => 'c' + item.id, []); diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx index 5728c03ce7..035f4b24eb 100644 --- a/src/screens/novel/context/NovelChaptersContext.tsx +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -165,7 +165,11 @@ export function NovelChaptersContextProvider({ state.batchInformation.batch, ] as const; - let chapterCount = getChapterCount(novelId, currentPage); + let chapterCount = getChapterCount( + novelId, + currentPage, + novelSettings.filter, + ); if (chapterCount) { try { @@ -186,7 +190,11 @@ export function NovelChaptersContextProvider({ }); await insertChapters(novelId, sourceChapters); newChapters = await _getPageChapters(...config); - chapterCount = getChapterCount(novelId, currentPage); + chapterCount = getChapterCount( + novelId, + currentPage, + novelSettings.filter, + ); } const batchInformation = { @@ -259,7 +267,7 @@ export function NovelChaptersContextProvider({ cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [novelSettings.filter, novelSettings.sort]); const contextValue = useMemo( () => ({ From f8d1d6c91fcc8baae6c71edf67dd29f30481978d Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:42:30 +0200 Subject: [PATCH 03/10] fix undefined chapters state --- src/screens/novel/components/Info/NovelInfoHeader.tsx | 11 ++++++++--- src/screens/novel/context/NovelChaptersContext.tsx | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 0bb1fa398a..4ddb07ad26 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -107,6 +107,13 @@ const NovelInfoHeader = ({ showToast('Not available while loading'); }; + let chapterText = ''; + if (!fetching || totalChapters !== undefined) { + chapterText = `${totalChapters ?? 0} ${getString('novelScreen.chapters')}`; + } else { + chapterText = getString('common.loading'); + } + return ( <> - {!fetching || totalChapters !== undefined - ? `${totalChapters} ${getString('novelScreen.chapters')}` - : getString('common.loading')} + {chapterText} {page && Number(page) ? ( diff --git a/src/screens/novel/context/NovelChaptersContext.tsx b/src/screens/novel/context/NovelChaptersContext.tsx index 035f4b24eb..2b337e264e 100644 --- a/src/screens/novel/context/NovelChaptersContext.tsx +++ b/src/screens/novel/context/NovelChaptersContext.tsx @@ -116,7 +116,7 @@ export function NovelChaptersContextProvider({ const [state, dispatch] = useReducer(reducer, { chapters: [], - fetching: false, + fetching: true, batchInformation: { batch: 0, total: 0 }, }); From 4c71bc68baf84ea6e2265ced486a877b4094092e Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:01:34 +0200 Subject: [PATCH 04/10] fixed error if network request fails when preloading next chapter --- src/screens/reader/hooks/useChapter.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/screens/reader/hooks/useChapter.ts b/src/screens/reader/hooks/useChapter.ts index 3d3356b17b..7b9314fdba 100644 --- a/src/screens/reader/hooks/useChapter.ts +++ b/src/screens/reader/hooks/useChapter.ts @@ -102,12 +102,11 @@ export default function useChapter( if (NativeFile.exists(filePath)) { text = NativeFile.readFile(filePath); } else { - await fetchChapter(novel.pluginId, path) - .then(res => { - text = res; - }) - .catch(e => setError(e.message)); + await fetchChapter(novel.pluginId, path).then(res => { + text = res; + }); } + return text; }, [chapter.novelId, novel.pluginId], @@ -125,10 +124,17 @@ export default function useChapter( text, ]); if (nextChap && !chapterTextCache.get(nextChap.id)) { - chapterTextCache.set( - nextChap.id, - loadChapterText(nextChap.id, nextChap.path), - ); + try { + const nextText = await loadChapterText(nextChap.id, nextChap.path); + chapterTextCache.set(nextChap.id, nextText); + } catch (e: any) { + if ( + 'message' in e && + !e.message.includes('Network request failed') + ) { + setError(e.message); + } + } } if (!cachedText) { chapterTextCache.set(chap.id, text); From be5e6a20bbc96f46cdf791694632c944556c25ff Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sat, 23 Aug 2025 16:24:17 +0200 Subject: [PATCH 05/10] fix download behaviour --- src/components/Context/LibraryContext.tsx | 25 +- src/database/queries/LibraryQueries.ts | 4 +- .../persisted/library/useLibraryNovels.ts | 9 +- src/hooks/persisted/usePlugins.ts | 213 ++++++++++-------- src/navigators/Main.tsx | 35 +-- .../library/components/LibraryTabs.tsx | 5 +- src/services/ServiceManager.ts | 5 +- 7 files changed, 172 insertions(+), 124 deletions(-) diff --git a/src/components/Context/LibraryContext.tsx b/src/components/Context/LibraryContext.tsx index 2f4878cb93..ed34cfe53f 100644 --- a/src/components/Context/LibraryContext.tsx +++ b/src/components/Context/LibraryContext.tsx @@ -1,12 +1,17 @@ -// src/components/Context/LibraryContext.tsx -import React, { createContext, useContext, useMemo, useCallback } from 'react'; +import React, { + createContext, + useContext, + useMemo, + useCallback, + useEffect, +} from 'react'; // Import existing settings hook import { useLibrarySettings, LibrarySettings, } from '@hooks/persisted/useSettings'; -import { NovelInfo } from '@database/types'; +import { DBNovelInfo } from '@database/types'; import { ExtendedCategory, useFetchCategories, @@ -16,7 +21,7 @@ import { useLibraryActions } from '@hooks/persisted/library/useLibraryActions'; import ServiceManager, { QueuedBackgroundTask } from '@services/ServiceManager'; type LibraryContextType = { - library: NovelInfo[]; + library: DBNovelInfo[]; categories: ExtendedCategory[]; isLoading: boolean; settings: LibrarySettings; @@ -29,8 +34,7 @@ type LibraryContextType = { setCategories: React.Dispatch>; }; -const defaultValue = {} as LibraryContextType; -const LibraryContext = createContext(defaultValue); +const LibraryContext = createContext(null); interface LibraryContextProviderProps { children: React.ReactNode; @@ -87,7 +91,12 @@ export function LibraryContextProvider({ }, [refetchLibrary, refetchNovel], ); - ServiceManager.manager.addCompletionListener(handleQueueChange); + useEffect(() => { + ServiceManager.manager.addCompletionListener(handleQueueChange); + return () => { + ServiceManager.manager.removeCompletionListener(handleQueueChange); + }; + }, [handleQueueChange]); const contextValue = useMemo( () => ({ @@ -129,7 +138,7 @@ export function LibraryContextProvider({ export const useLibraryContext = (): LibraryContextType => { const context = useContext(LibraryContext); - if (context === defaultValue) { + if (context === null) { throw new Error( 'useLibraryContext must be used within a LibraryContextProvider', ); diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index b0dbffb881..a2f7eb3020 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -1,5 +1,5 @@ import { LibraryFilter } from '@screens/library/constants/constants'; -import { LibraryNovelInfo, NovelInfo } from '../types'; +import { DBNovelInfo, LibraryNovelInfo, NovelInfo } from '../types'; import { getAllSync } from '../utils/helpers'; export const getLibraryNovelsFromDb = ({ @@ -12,7 +12,7 @@ export const getLibraryNovelsFromDb = ({ filter?: string; searchText?: string; downloadedOnlyMode?: boolean; -}): NovelInfo[] => { +}): DBNovelInfo[] => { let query = 'SELECT * FROM Novel WHERE inLibrary = 1'; if (filter) { diff --git a/src/hooks/persisted/library/useLibraryNovels.ts b/src/hooks/persisted/library/useLibraryNovels.ts index 7de09b9959..9d026bcb1d 100644 --- a/src/hooks/persisted/library/useLibraryNovels.ts +++ b/src/hooks/persisted/library/useLibraryNovels.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useFocusEffect } from '@react-navigation/native'; import { getLibraryNovelsFromDb } from '@database/queries/LibraryQueries'; -import { NovelInfo } from '@database/types'; +import { DBNovelInfo, NovelInfo } from '@database/types'; import { LibraryFilter, LibrarySortOrder, @@ -20,7 +20,7 @@ export const useLibraryNovels = ({ filter, downloadedOnlyMode = false, }: UseLibraryNovelsOptions) => { - const [novels, setNovels] = useState([]); + const [novels, setNovels] = useState([]); const [novelsLoading, setNovelsLoading] = useState(true); const { searchText, setSearchText, clearSearchbar } = useSearch(); @@ -48,13 +48,16 @@ export const useLibraryNovels = ({ 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; + + // create a new array, so the state is updated + return [...prevNovels]; }); } catch (error) { // eslint-disable-next-line no-console diff --git a/src/hooks/persisted/usePlugins.ts b/src/hooks/persisted/usePlugins.ts index f03ee16270..8042acc8a1 100644 --- a/src/hooks/persisted/usePlugins.ts +++ b/src/hooks/persisted/usePlugins.ts @@ -11,7 +11,7 @@ import { } from '@plugins/pluginManager'; import { newer } from '@utils/compareVersion'; import { MMKVStorage, getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { getString } from '@strings/translations'; export const AVAILABLE_PLUGINS = 'AVAILABLE_PLUGINS'; @@ -21,17 +21,18 @@ export const LAST_USED_PLUGIN = 'LAST_USED_PLUGIN'; export const FILTERED_AVAILABLE_PLUGINS = 'FILTERED_AVAILABLE_PLUGINS'; export const FILTERED_INSTALLED_PLUGINS = 'FILTERED_INSTALLED_PLUGINS'; -const defaultLang = languagesMapping[locale.split('-')[0]] || 'English'; +const defaultLang = [languagesMapping[locale.split('-')[0]] || 'English']; export default function usePlugins() { const [lastUsedPlugin, setLastUsedPlugin] = useMMKVObject(LAST_USED_PLUGIN); - const [languagesFilter = [defaultLang], setLanguagesFilter] = + const [languagesFilter = defaultLang, setLanguagesFilter] = useMMKVObject(LANGUAGES_FILTER); const [filteredAvailablePlugins = [], setFilteredAvailablePlugins] = useMMKVObject(FILTERED_AVAILABLE_PLUGINS); const [filteredInstalledPlugins = [], setFilteredInstalledPlugins] = useMMKVObject(FILTERED_INSTALLED_PLUGINS); + /** * @param filter * We cant use the languagesFilter directly because it is updated only after component's lifecycle end. @@ -63,7 +64,7 @@ export default function usePlugins() { [setFilteredAvailablePlugins, setFilteredInstalledPlugins], ); - const refreshPlugins = useCallback(() => { + const refreshPlugins = useCallback(async () => { const installedPlugins = getMMKVObject(INSTALLED_PLUGINS) || []; return fetchPlugins().then(fetchedPlugins => { @@ -88,13 +89,16 @@ export default function usePlugins() { }); }, [filterPlugins, languagesFilter, lastUsedPlugin?.id, setLastUsedPlugin]); - const toggleLanguageFilter = (lang: string) => { - const newFilter = languagesFilter.includes(lang) - ? languagesFilter.filter(l => l !== lang) - : [lang, ...languagesFilter]; - setLanguagesFilter(newFilter); - filterPlugins(newFilter); - }; + const toggleLanguageFilter = useCallback( + (lang: string) => { + const newFilter = languagesFilter.includes(lang) + ? languagesFilter.filter(l => l !== lang) + : [lang, ...languagesFilter]; + setLanguagesFilter(newFilter); + filterPlugins(newFilter); + }, + [filterPlugins, languagesFilter, setLanguagesFilter], + ); /** * Variable scope naming @@ -103,88 +107,115 @@ export default function usePlugins() { * plg: parameter in JS class method callback (.map, .reducer, ...) */ - const installPlugin = (plugin: PluginItem) => { - return _install(plugin).then(_plg => { - if (_plg) { - const installedPlugins = - getMMKVObject(INSTALLED_PLUGINS) || []; - const actualPlugin: PluginItem = { - ...plugin, - version: _plg.version, - hasSettings: !!_plg.pluginSettings, - }; - // safe - if (!installedPlugins.some(plg => plg.id === plugin.id)) { - setMMKVObject(INSTALLED_PLUGINS, [...installedPlugins, actualPlugin]); + const installPlugin = useCallback( + async (plugin: PluginItem) => { + return _install(plugin).then(_plg => { + if (_plg) { + const installedPlugins = + getMMKVObject(INSTALLED_PLUGINS) || []; + const actualPlugin: PluginItem = { + ...plugin, + version: _plg.version, + hasSettings: !!_plg.pluginSettings, + }; + // safe + if (!installedPlugins.some(plg => plg.id === plugin.id)) { + setMMKVObject(INSTALLED_PLUGINS, [ + ...installedPlugins, + actualPlugin, + ]); + } + filterPlugins(languagesFilter); + } else { + throw new Error( + getString('browseScreen.installFailed', { name: plugin.name }), + ); } - filterPlugins(languagesFilter); - } else { - throw new Error( - getString('browseScreen.installFailed', { name: plugin.name }), - ); - } - }); - }; - - const uninstallPlugin = (plugin: PluginItem) => { - if (lastUsedPlugin?.id === plugin.id) { - MMKVStorage.delete(LAST_USED_PLUGIN); - } - const installedPlugins = - getMMKVObject(INSTALLED_PLUGINS) || []; - setMMKVObject( - INSTALLED_PLUGINS, - installedPlugins.filter(plg => plg.id !== plugin.id), - ); - filterPlugins(languagesFilter); - return _uninstall(plugin).then(() => {}); - }; + }); + }, + [filterPlugins, languagesFilter], + ); - const updatePlugin = (plugin: PluginItem) => { - return _update(plugin).then(_plg => { - if (plugin.version === _plg?.version && !__DEV__) { - throw new Error('No update found!'); - } - if (_plg) { - const installedPlugins = - getMMKVObject(INSTALLED_PLUGINS) || []; - setMMKVObject( - INSTALLED_PLUGINS, - installedPlugins.map(plg => { - if (plugin.id !== plg.id) { - return plg; - } - const newPlugin: PluginItem = { - ...plugin, - site: _plg.site, - name: _plg.name, - version: _plg.version, - hasUpdate: false, - }; - if (newPlugin.id === lastUsedPlugin?.id) { - setLastUsedPlugin(newPlugin); - } - return newPlugin; - }), - ); - filterPlugins(languagesFilter); - return _plg.version; - } else { - throw Error(getString('browseScreen.updateFailed')); + const uninstallPlugin = useCallback( + async (plugin: PluginItem) => { + if (lastUsedPlugin?.id === plugin.id) { + MMKVStorage.delete(LAST_USED_PLUGIN); } - }); - }; + const installedPlugins = + getMMKVObject(INSTALLED_PLUGINS) || []; + setMMKVObject( + INSTALLED_PLUGINS, + installedPlugins.filter(plg => plg.id !== plugin.id), + ); + filterPlugins(languagesFilter); + return _uninstall(plugin).then(() => {}); + }, + [filterPlugins, languagesFilter, lastUsedPlugin?.id], + ); - return { - filteredAvailablePlugins, - filteredInstalledPlugins, - lastUsedPlugin, - languagesFilter, - setLastUsedPlugin, - refreshPlugins, - toggleLanguageFilter, - installPlugin, - uninstallPlugin, - updatePlugin, - }; + const updatePlugin = useCallback( + async (plugin: PluginItem) => { + return _update(plugin).then(_plg => { + if (plugin.version === _plg?.version && !__DEV__) { + throw new Error('No update found!'); + } + if (_plg) { + const installedPlugins = + getMMKVObject(INSTALLED_PLUGINS) || []; + setMMKVObject( + INSTALLED_PLUGINS, + installedPlugins.map(plg => { + if (plugin.id !== plg.id) { + return plg; + } + const newPlugin: PluginItem = { + ...plugin, + site: _plg.site, + name: _plg.name, + version: _plg.version, + hasUpdate: false, + }; + if (newPlugin.id === lastUsedPlugin?.id) { + setLastUsedPlugin(newPlugin); + } + return newPlugin; + }), + ); + filterPlugins(languagesFilter); + return _plg.version; + } else { + throw Error(getString('browseScreen.updateFailed')); + } + }); + }, + [filterPlugins, languagesFilter, lastUsedPlugin?.id, setLastUsedPlugin], + ); + + const res = useMemo( + () => ({ + filteredAvailablePlugins, + filteredInstalledPlugins, + lastUsedPlugin, + languagesFilter, + setLastUsedPlugin, + refreshPlugins, + toggleLanguageFilter, + installPlugin, + uninstallPlugin, + updatePlugin, + }), + [ + filteredAvailablePlugins, + filteredInstalledPlugins, + lastUsedPlugin, + languagesFilter, + setLastUsedPlugin, + refreshPlugins, + toggleLanguageFilter, + installPlugin, + uninstallPlugin, + updatePlugin, + ], + ); + return res; } diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx index 5e70c3b151..3826ae02b8 100644 --- a/src/navigators/Main.tsx +++ b/src/navigators/Main.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { DefaultTheme, NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; @@ -43,9 +43,9 @@ import { UpdateContextProvider } from '@components/Context/UpdateContext'; const Stack = createNativeStackNavigator(); const MainNavigator = () => { - const theme = useTheme(); - const { updateLibraryOnLaunch } = useAppSettings(); const { refreshPlugins } = usePlugins(); + const { updateLibraryOnLaunch } = useAppSettings(); + const theme = useTheme(); const [isOnboarded] = useMMKVBoolean('IS_ONBOARDED'); useEffect(() => { @@ -71,24 +71,29 @@ const MainNavigator = () => { const { isNewVersion, latestRelease } = useGithubUpdateChecker(); + const NavTheme = useMemo( + () => ({ + colors: { + ...DefaultTheme.colors, + primary: theme.primary, + background: theme.background, + card: theme.surface, + text: theme.onSurface, + border: theme.outline, + }, + dark: theme.isDark, + fonts: DefaultTheme.fonts, + }), + [theme], + ); + if (!isOnboarded) { return ; } return ( - theme={{ - colors: { - ...DefaultTheme.colors, - primary: theme.primary, - background: theme.background, - card: theme.surface, - text: theme.onSurface, - border: theme.outline, - }, - dark: theme.isDark, - fonts: DefaultTheme.fonts, - }} + theme={NavTheme} linking={{ prefixes: ['lnreader://'], config: { diff --git a/src/screens/library/components/LibraryTabs.tsx b/src/screens/library/components/LibraryTabs.tsx index 930b2c9326..f57fd6b545 100644 --- a/src/screens/library/components/LibraryTabs.tsx +++ b/src/screens/library/components/LibraryTabs.tsx @@ -70,8 +70,7 @@ const LibraryTabs: React.FC = ({ theme, }) => { const layout = useWindowDimensions(); - const styles = useMemo(() => createTabStyles(theme), [theme]); // Memoize styles - + const styles = useMemo(() => createTabStyles(theme), [theme]); const renderTabBar = useCallback( ( props: SceneRendererProps & { @@ -172,7 +171,7 @@ const LibraryTabs: React.FC = ({ searchText, selectedNovelIds, setSelectedNovelIds, - styles.globalSearchBtn, // Needs to be passed down if this component doesn't create it + styles.globalSearchBtn, theme, ], ); diff --git a/src/services/ServiceManager.ts b/src/services/ServiceManager.ts index 51b97d1aa9..c5393b213d 100644 --- a/src/services/ServiceManager.ts +++ b/src/services/ServiceManager.ts @@ -331,9 +331,10 @@ export default class ServiceManager { } await manager.executeTask(currentTask, startingTasks); - + // Get the new taskList to preserve tasks that were added while executing + const newTasks = manager.getTaskList().slice(1); // After execution, remove the current task from the queue - setMMKVObject(manager.STORE_KEY, currentTasks.slice(1)); + setMMKVObject(manager.STORE_KEY, newTasks); doneTasks[currentTask.task.name] += 1; } From 9312c99dfcae1adf636848f283727497e9833819 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:19:20 +0200 Subject: [PATCH 06/10] handle deletion of chapter on downloads page correctly --- package-lock.json | 11 ---- src/screens/more/DownloadsScreen.tsx | 52 +++++++------------ .../updates/components/UpdateNovelCard.tsx | 12 ++++- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23b289700c..269e71dbdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8599,17 +8599,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/babel-plugin-react-compiler": { - "version": "19.0.0-beta-ebf51a3-20250411", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-ebf51a3-20250411.tgz", - "integrity": "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.26.0" - } - }, "node_modules/expo/node_modules/babel-preset-expo": { "version": "13.1.11", "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.1.11.tgz", diff --git a/src/screens/more/DownloadsScreen.tsx b/src/screens/more/DownloadsScreen.tsx index 46ac21ad4e..ca1360629b 100644 --- a/src/screens/more/DownloadsScreen.tsx +++ b/src/screens/more/DownloadsScreen.tsx @@ -20,8 +20,6 @@ import { getString } from '@strings/translations'; import { DownloadsScreenProps } from '@navigators/types'; import { DownloadedChapter } from '@database/types'; import { showToast } from '@utils/showToast'; -import dayjs from 'dayjs'; -import { parseChapterNumber } from '@utils/parseChapterNumber'; type DownloadGroup = Record; @@ -29,21 +27,22 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => { const theme = useTheme(); const [loading, setLoading] = useState(true); const [chapters, setChapters] = useState([]); - const groupUpdatesByDate = ( - localChapters: DownloadedChapter[], - ): DownloadedChapter[][] => { - const dateGroups = localChapters.reduce((groups, item) => { - const novelId = item.novelId; - if (!groups[novelId]) { - groups[novelId] = []; - } - - groups[novelId].push(item); - - return groups; - }, {} as DownloadGroup); - return Object.values(dateGroups); - }; + const groupDownloadsByDate = useCallback( + (localChapters: DownloadedChapter[]): DownloadedChapter[][] => { + const dateGroups = localChapters.reduce((groups, item) => { + const novelId = item.novelId; + if (!groups[novelId]) { + groups[novelId] = []; + } + + groups[novelId].push(item); + + return groups; + }, {} as DownloadGroup); + return Object.values(dateGroups); + }, + [], + ); /** * Confirm Clear downloads Dialog @@ -54,20 +53,7 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => { const getChapters = async () => { const res = await getDownloadedChapters(); - setChapters( - res.map(download => { - const parsedTime = dayjs(download.releaseTime); - return { - ...download, - releaseTime: parsedTime.isValid() - ? parsedTime.format('LL') - : download.releaseTime, - chapterNumber: download.chapterNumber - ? download.chapterNumber - : parseChapterNumber(download.novelName, download.name), - }; - }), - ); + setChapters(res); }; const ListEmptyComponent = useCallback( @@ -85,6 +71,8 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => { getChapters().finally(() => setLoading(false)); }, []); + const groupedChapters = groupDownloadsByDate(chapters); + return ( { ) : ( 'downloadGroup' + index} renderItem={({ item }) => { return ( diff --git a/src/screens/updates/components/UpdateNovelCard.tsx b/src/screens/updates/components/UpdateNovelCard.tsx index 1f2372b4a2..6df29e01c5 100644 --- a/src/screens/updates/components/UpdateNovelCard.tsx +++ b/src/screens/updates/components/UpdateNovelCard.tsx @@ -80,6 +80,16 @@ const UpdateNovelCard: React.FC = ({ } }; + const handleDeleteChapter = useCallback( + (chapter: Update | DownloadedChapter) => { + deleteChapter(chapter); + if (onlyDownloadedChapters) { + setChapterList(chapterList.filter(c => c.id !== chapter.id)); + } + }, + [chapterList, deleteChapter, onlyDownloadedChapters], + ); + const navigateToChapter = useCallback( (chapter: ChapterInfo) => { const { novelPath, pluginId, novelName } = chapter as @@ -160,7 +170,7 @@ const UpdateNovelCard: React.FC = ({ chapter={item} showChapterTitles={false} downloadChapter={() => handleDownloadChapter(item)} - deleteChapter={() => deleteChapter(item)} + deleteChapter={() => handleDeleteChapter(item)} navigateToChapter={navigateToChapter} left={ From 4a81abf72a290e96114240d32972eaec7c660e88 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:36:18 +0200 Subject: [PATCH 07/10] fixed crash on category deletion --- src/database/queries/LibraryQueries.ts | 4 +-- .../persisted/library/useLibraryNovels.ts | 2 +- src/screens/library/LibraryScreen.tsx | 34 +++++++++++-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index a2f7eb3020..5177562edf 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -1,5 +1,5 @@ import { LibraryFilter } from '@screens/library/constants/constants'; -import { DBNovelInfo, LibraryNovelInfo, NovelInfo } from '../types'; +import { DBNovelInfo, LibraryNovelInfo } from '../types'; import { getAllSync } from '../utils/helpers'; export const getLibraryNovelsFromDb = ({ @@ -32,7 +32,7 @@ export const getLibraryNovelsFromDb = ({ query += ` ORDER BY ${sortOrder} `; } - return getAllSync([query, [searchText]]); + return getAllSync([query, [searchText]]); }; const getLibraryWithCategoryQuery = 'SELECT * FROM Novel WHERE inLibrary = 1'; diff --git a/src/hooks/persisted/library/useLibraryNovels.ts b/src/hooks/persisted/library/useLibraryNovels.ts index 9d026bcb1d..446d0644b5 100644 --- a/src/hooks/persisted/library/useLibraryNovels.ts +++ b/src/hooks/persisted/library/useLibraryNovels.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useFocusEffect } from '@react-navigation/native'; import { getLibraryNovelsFromDb } from '@database/queries/LibraryQueries'; -import { DBNovelInfo, NovelInfo } from '@database/types'; +import { DBNovelInfo } from '@database/types'; import { LibraryFilter, LibrarySortOrder, diff --git a/src/screens/library/LibraryScreen.tsx b/src/screens/library/LibraryScreen.tsx index 5737e1f372..d84c130068 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -62,6 +62,7 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { const currentNovels = useMemo(() => { if (!categories.length) return []; + if (!categories[index]) return []; const ids = categories[index].novelIds; return library.filter(l => ids.includes(l.id)); }, [categories, index, library]); @@ -138,6 +139,9 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { } }, [currentNovels, navigation]); + // If there are categories but the current index is out of bounds, set it to 0 + if (categories.length && !categories[index]) setIndex(0); + return ( { theme={theme} /> - + {categories.length && categories[index] ? ( + + ) : null} Date: Fri, 29 Aug 2025 17:23:47 +0200 Subject: [PATCH 08/10] speed up history deletion --- src/database/queries/HistoryQueries.ts | 4 +++- src/hooks/persisted/useHistory.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/database/queries/HistoryQueries.ts b/src/database/queries/HistoryQueries.ts index d3ff9f98b1..538d0e2c1a 100644 --- a/src/database/queries/HistoryQueries.ts +++ b/src/database/queries/HistoryQueries.ts @@ -26,6 +26,8 @@ export const deleteChapterHistory = (chapterId: number) => db.runAsync('UPDATE Chapter SET readTime = NULL WHERE id = ?', chapterId); export const deleteAllHistory = async () => { - await db.execAsync('UPDATE Chapter SET readTime = NULL'); + await db.execAsync( + 'UPDATE Chapter SET readTime = NULL WHERE readTime IS NOT NULL', + ); showToast(getString('historyScreen.deleted')); }; diff --git a/src/hooks/persisted/useHistory.ts b/src/hooks/persisted/useHistory.ts index 5e73639d20..f58163bc37 100644 --- a/src/hooks/persisted/useHistory.ts +++ b/src/hooks/persisted/useHistory.ts @@ -37,9 +37,9 @@ const useHistory = () => { .catch((err: Error) => setError(err.message)) .finally(() => setIsLoading(false)); - const clearAllHistory = () => { - deleteAllHistory(); - getHistory(); + const clearAllHistory = async () => { + setHistory([]); + await deleteAllHistory(); }; const removeChapterFromHistory = async (chapterId: number) => { From 5b57025ef28bbc50e581040413e7271dbe66f784 Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:46:52 +0200 Subject: [PATCH 09/10] fix category update updates whole library --- src/database/queries/LibraryQueries.ts | 32 +++----------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index 5177562edf..730e53c13c 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -35,34 +35,8 @@ export const getLibraryNovelsFromDb = ({ return getAllSync([query, [searchText]]); }; -const getLibraryWithCategoryQuery = 'SELECT * FROM Novel WHERE inLibrary = 1'; -// ` -// SELECT * -// FROM -// ( -// SELECT NIL.*, chaptersUnread, chaptersDownloaded, lastReadAt, lastUpdatedAt -// FROM -// ( -// SELECT -// Novel.*, -// category, -// categoryId -// FROM -// Novel LEFT JOIN ( -// SELECT NovelId, name as category, categoryId FROM (NovelCategory JOIN Category ON NovelCategory.categoryId = Category.id) -// ) as NC ON Novel.id = NC.novelId -// WHERE inLibrary = 1 -// ) as NIL -// LEFT JOIN -// ( -// SELECT -// SUM(unread) as chaptersUnread, SUM(isDownloaded) as chaptersDownloaded, -// novelId, MAX(readTime) as lastReadAt, MAX(updatedTime) as lastUpdatedAt -// FROM Chapter -// GROUP BY novelId -// ) as C ON NIL.id = C.novelId -// ) WHERE 1 = 1 -// `; +const getLibraryWithCategoryQuery = + 'SELECT * FROM NovelCategory NC JOIN Novel N on N.id = NC.novelId WHERE 1=1'; export const getLibraryWithCategory = ({ filter, @@ -79,7 +53,7 @@ export const getLibraryWithCategory = ({ const preparedArgument: (string | number | null)[] = []; if (filter) { - // query += ` AND ${filter} `; + query += ` AND ${filter} `; } if (downloadedOnlyMode) { query += ' ' + LibraryFilter.DownloadedOnly; From 17e8bc9d6266a9da4df6ce573d614f8c756cdd3c Mon Sep 17 00:00:00 2001 From: CD-Z <69157453+CD-Z@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:21:36 +0200 Subject: [PATCH 10/10] fix snackbar not showing --- src/screens/novel/NovelScreen.tsx | 4 ++-- src/screens/novel/components/NovelScreenList.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index 6cbf5e82fd..74d62fb088 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -207,7 +207,6 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { openPage, closeDrawer, ]); - return ( { }> { deleteChapters(chapters.filter(c => c.isDownloaded)); }, }} - theme={{ colors: { primary: theme.primary } }} + theme={{ colors: theme }} style={styles.snackbar} > diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index b01f0b0ad6..4ebd395284 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -3,7 +3,7 @@ import ChapterItem from './ChapterItem'; import NovelInfoHeader from './Info/NovelInfoHeader'; import { useRef, useState, useCallback, useMemo } from 'react'; import { ChapterInfo } from '@database/types'; -import { useBoolean } from '@hooks/index'; +import { UseBooleanReturnType } from '@hooks/index'; import { useAppSettings, useDownload, @@ -43,6 +43,7 @@ type NovelScreenListProps = { selected: ChapterInfo[]; setSelected: React.Dispatch>; getNextChapterBatch: () => void; + deleteDownloadsSnackbar: UseBooleanReturnType; routeBaseNovel: { name: string; path: string; @@ -61,6 +62,7 @@ const NovelScreenList = ({ openDrawer, routeBaseNovel, selected, + deleteDownloadsSnackbar, setSelected, getNextChapterBatch, }: NovelScreenListProps) => { @@ -99,7 +101,6 @@ const NovelScreenList = ({ const novelBottomSheetRef = useRef(null); const trackerSheetRef = useRef(null); - const deleteDownloadsSnackbar = useBoolean(); // Memoize selected chapter IDs for faster lookup const selectedIds = useMemo( () => new Set(selected.map(chapter => chapter.id)),