From 1bcc0e66643b2eb9e9f632056028f6c82a137f9e Mon Sep 17 00:00:00 2001 From: nyagami Date: Mon, 19 Feb 2024 12:48:42 +0700 Subject: [PATCH] LNReader beta version (#961) * beta host * Refactor: url -> path (#944) * refactor: url -> pathr * fix: pluginId * page support (#947) * add new fields * minor fix * pages for novel * render novel pages with drawer * support real paginated novels * descending novel chapter list * update when has new latest chapter * set hasUpdate for all pages after update * fix: insert pageIndex * fix: refreshChapters * minor refactor: pageIndex * fix: update page logic * fix: order by pageIndex * refactor: Chapter.pageindex -> index (position in page) * Revert "refactor: Chapter.pageindex -> index (position in page)" This reverts commit 109298e0ccfa4a0d3d369087e05cb6f307a786b4. * refactor: pageindex -> position * fix: reader drawer * temporary fix migration * fix: Serialize plugins into one file (#949) * serialize plugins into one file * fix typo * Fix: insertChapters (#950) * Fix: re-renders in novel screen(#951) * remove unnecessary wrap * fix: add local novel back to local category * fix: use variables for default states * fix: do not re-render when no FAB * close drawer if pages length = 1 * add Portal.Host * Cheaper useNovel hook (#952) * fix: using pure state for novel and chapters * fix: remove latestChapter mmkv hook * shorter key * fix: bookmark * fix: save chapter progress in db * minor refactor: chapterProgressElement * Fix: Error/Empty actions (#956) * fix: reader error handler * fix: opacity for novel summary * fix: library empty view * fix: upsert and pages logic * fix: update novel logic * fix: import epub * fix: plugin site in global search * fix: scrollbar font size * minor fix --- App.tsx | 4 +- android/app/src/main/assets/css/index.css | 1 + android/app/src/main/assets/js/index.js | 17 +- .../ZipArchive/ZipArchive.java | 8 +- reader_playground/template.index.html | 4 +- src/components/Checkbox/Checkbox.tsx | 8 +- src/components/EmptyView/EmptyView.tsx | 38 +- .../ErrorScreenV2/ErrorScreenV2.tsx | 50 +- src/components/NovelList.tsx | 2 +- src/database/queries/ChapterQueries.ts | 117 ++- src/database/queries/HistoryQueries.ts | 3 +- src/database/queries/LibraryQueries.ts | 2 +- src/database/queries/NovelQueries.ts | 99 +-- src/database/tables/ChapterTable.ts | 7 +- src/database/tables/NovelTable.ts | 5 +- src/database/types/index.ts | 20 +- src/hooks/persisted/useDownload.ts | 9 +- src/hooks/persisted/useNovel.ts | 258 +++--- src/hooks/persisted/useSettings.ts | 2 +- src/navigators/types/index.ts | 10 +- src/plugins/pluginManager.ts | 86 +- src/plugins/types/index.ts | 27 +- .../BrowseSourceScreen/BrowseSourceScreen.tsx | 18 +- .../BrowseSourceScreen/useBrowseSource.ts | 8 +- .../components/GlobalSearchNovelItem.tsx | 2 +- .../components/GlobalSearchResultsList.tsx | 25 +- .../hooks/useGlobalSearch.ts | 10 +- src/screens/WebviewScreen/WebviewScreen.tsx | 11 +- .../browse/components/PluginSection.tsx | 2 +- .../browse/migration/MigrationNovelList.tsx | 21 +- .../browse/migration/MigrationNovels.tsx | 5 +- src/screens/history/HistoryScreen.tsx | 26 +- .../components/HistoryCard/HistoryCard.tsx | 76 +- src/screens/library/LibraryScreen.tsx | 47 +- .../library/components/LibraryListView.tsx | 16 +- src/screens/more/DownloadQueueScreen.tsx | 4 +- src/screens/novel/NovelScreen.tsx | 783 +++++++++--------- src/screens/novel/components/ChapterItem.tsx | 19 +- .../novel/components/Info/NovelInfoHeader.tsx | 45 +- .../novel/components/NovelBottomSheet.tsx | 10 +- src/screens/novel/components/NovelDrawer.tsx | 99 +++ .../NovelScreenButtonGroup.tsx | 19 +- .../components/NovelSummary/NovelSummary.tsx | 6 +- .../novel/components/Tracker/TrackSheet.tsx | 10 +- src/screens/reader/ReaderScreen.tsx | 68 +- .../reader/components/ChapterDrawer.tsx | 35 +- .../reader/components/ReaderAppbar.tsx | 8 +- .../reader/components/WebViewReader.tsx | 33 +- .../settings/SettingsAdvancedScreen.tsx | 11 - .../SettingsGeneralScreen.tsx | 2 +- .../components/DefaultChapterSortModal.tsx | 12 +- .../updates/components/UpdateNovelCard.tsx | 6 +- src/services/backup/utils.ts | 4 +- src/services/epub/import.ts | 33 +- src/services/migrate/migrateNovel.ts | 50 +- src/services/plugin/fetch.ts | 50 +- src/services/updates/LibraryUpdateQueries.ts | 75 +- src/services/updates/index.ts | 2 +- src/utils/handleNavigateParams.ts | 38 - 59 files changed, 1410 insertions(+), 1056 deletions(-) create mode 100644 src/screens/novel/components/NovelDrawer.tsx delete mode 100644 src/utils/handleNavigateParams.ts diff --git a/App.tsx b/App.tsx index cf96a91a1..f5b7ce4e2 100644 --- a/App.tsx +++ b/App.tsx @@ -14,7 +14,7 @@ import BackgroundService from 'react-native-background-actions'; import { createTables } from '@database/db'; import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; -import { collectPlugins } from '@plugins/pluginManager'; +import { deserializePlugins } from '@plugins/pluginManager'; import Main from './src/navigators/Main'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; @@ -36,7 +36,7 @@ const App = () => { const { refreshPlugins } = usePlugins(); useEffect(() => { createTables(); - collectPlugins().then(() => LottieSplashScreen.hide()); + deserializePlugins().then(() => LottieSplashScreen.hide()); refreshPlugins(); if (!BackgroundService.isRunning()) { MMKVStorage.delete(BACKGROUND_ACTION); diff --git a/android/app/src/main/assets/css/index.css b/android/app/src/main/assets/css/index.css index d5995e4a0..c46f82825 100644 --- a/android/app/src/main/assets/css/index.css +++ b/android/app/src/main/assets/css/index.css @@ -85,6 +85,7 @@ img { border-radius: 1.2rem; background-color: var(--theme-surface-0-9); touch-action: none; + font-size: 16; } .scrollbar-item { diff --git a/android/app/src/main/assets/js/index.js b/android/app/src/main/assets/js/index.js index 8cdfbdd06..a505ec0c5 100644 --- a/android/app/src/main/assets/js/index.js +++ b/android/app/src/main/assets/js/index.js @@ -17,12 +17,9 @@ class Reader { () => this.post({ type: 'save', - data: { - offsetY: window.scrollY, - percentage: parseInt( - ((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100, - ), - }, + data: parseInt( + ((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100, + ), }), autoSaveInterval, ); @@ -175,7 +172,7 @@ class TextToSpeech { }; } -const reader = new Reader(); -const scrollHandler = new ScrollHandler(reader); -const swipeHandler = new SwipeHandler(reader); -const tts = new TextToSpeech(reader); +var reader = new Reader(); +var scrollHandler = new ScrollHandler(reader); +var swipeHandler = new SwipeHandler(reader); +var tts = new TextToSpeech(reader); diff --git a/android/app/src/main/java/com/rajarsheechatterjee/ZipArchive/ZipArchive.java b/android/app/src/main/java/com/rajarsheechatterjee/ZipArchive/ZipArchive.java index d03715c9b..2c2d00ed9 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/ZipArchive/ZipArchive.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/ZipArchive/ZipArchive.java @@ -43,17 +43,13 @@ private void unzipProcess(ZipInputStream zis, String distDirPath) throws Excepti int len; byte[] buffer = new byte[4096]; while ((zipEntry = zis.getNextEntry()) != null) { + if(zipEntry.getName().endsWith("/")) continue; String escapedFilePath = this.escapeFilePath(zipEntry.getName()); File newFile = new File(distDirPath, escapedFilePath); newFile.getParentFile().mkdirs(); FileOutputStream fos = new FileOutputStream(newFile); - boolean isWritten = false; // ignore folder entry; - while ((len = zis.read(buffer)) > 0) { - isWritten = true; - fos.write(buffer, 0, len); - } + while ((len = zis.read(buffer)) > 0) fos.write(buffer, 0, len); fos.close(); - if(!isWritten) newFile.delete(); } zis.closeEntry(); } diff --git a/reader_playground/template.index.html b/reader_playground/template.index.html index c760dcc7c..8499a5d00 100644 --- a/reader_playground/template.index.html +++ b/reader_playground/template.index.html @@ -42,6 +42,7 @@ }else{ scrollHandler.show(); } + hidden = !hidden; break; default: console.log('unsupported event', event.type); @@ -55,7 +56,6 @@ var swipeGestures = false; var autoSaveInterval = 2222; - @@ -67,5 +67,5 @@
- + diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 4c0b50a6e..5972dd49c 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -59,7 +59,13 @@ export const Checkbox: React.FC = ({ ); -export const SortItem = ({ label, status, onPress, theme }) => ( +interface SortItemProps { + label: string; + status?: string; + onPress: () => void; + theme: ThemeColors; +} +export const SortItem = ({ label, status, onPress, theme }: SortItemProps) => ( void; + }>; } -const EmptyView: React.FC = ({ icon, description, theme }) => ( +const EmptyView: React.FC = ({ + icon, + description, + theme, + actions, +}) => ( {icon ? ( {icon} ) : null} {description} + {actions?.length ? ( + + {actions.map(action => ( + + + + ))} + + ) : null} ); @@ -34,4 +62,12 @@ const styles = StyleSheet.create({ text: { marginTop: 16, }, + buttonWrapper: { + marginHorizontal: 4, + flexDirection: 'row', + }, + actionsCtn: { + marginTop: 20, + flexDirection: 'row', + }, }); diff --git a/src/components/ErrorScreenV2/ErrorScreenV2.tsx b/src/components/ErrorScreenV2/ErrorScreenV2.tsx index 10f1dd342..d5aaf4c38 100644 --- a/src/components/ErrorScreenV2/ErrorScreenV2.tsx +++ b/src/components/ErrorScreenV2/ErrorScreenV2.tsx @@ -1,21 +1,44 @@ import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; - +import { Pressable, StyleSheet, Text, View } from 'react-native'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { useTheme } from '@hooks/persisted'; interface ErrorScreenProps { error: string; - onRetry?: () => void; - retryIconColor?: string; + actions?: Array<{ + iconName: string; + title: string; + onPress: () => void; + }>; } -const ErrorScreen: React.FC = ({ error }) => { +const ErrorScreen: React.FC = ({ error, actions }) => { const theme = useTheme(); return ( ಥ_ಥ {error} + {actions?.length ? ( + + {actions.map(action => ( + + + + {action.title} + + + ))} + + ) : null} ); }; @@ -36,4 +59,21 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, textAlign: 'center', }, + buttonWrapper: { + overflow: 'hidden', + borderRadius: 50, + marginHorizontal: 4, + flexDirection: 'row', + flex: 1 / 3, + }, + buttonCtn: { + flex: 1, + paddingVertical: 8, + alignItems: 'center', + justifyContent: 'center', + }, + actionsCtn: { + marginTop: 20, + flexDirection: 'row', + }, }); diff --git a/src/components/NovelList.tsx b/src/components/NovelList.tsx index 3c33440bf..022e97d4f 100644 --- a/src/components/NovelList.tsx +++ b/src/components/NovelList.tsx @@ -45,7 +45,7 @@ const NovelList: React.FC< ]} numColumns={numColumns} key={numColumns} - keyExtractor={item => item.url} + keyExtractor={(item, index) => index + '_' + item.path} {...props} /> ); diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 9851763f6..c6ba30089 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -16,11 +16,8 @@ import { getString } from '@strings/translations'; const db = SQLite.openDatabase('lnreader.db'); const insertChapterQuery = ` -INSERT OR IGNORE INTO Chapter ( - url, name, releaseTime, novelId, chapterNumber -) -Values - (?, ?, ?, ?, ?) +INSERT OR IGNORE INTO Chapter (path, name, releaseTime, novelId, chapterNumber, page, position) +VALUES (?, ?, ?, ?, ?, ?, ?) `; export const insertChapters = async ( @@ -31,34 +28,68 @@ export const insertChapters = async ( return; } db.transaction(tx => { - chapters.forEach(chapter => { + chapters.forEach((chapter, index) => { tx.executeSql( insertChapterQuery, [ - chapter.url, + chapter.path, chapter.name, chapter.releaseTime || '', novelId, chapter.chapterNumber || null, + chapter.page || '1', + index, ], - noop, + (txObj, { insertId }) => { + if (!insertId) { + tx.executeSql( + ` + UPDATE Chapter SET + page = ?, position = ? + WHERE path = ? AND novelId = ? (AND page != ? OR position != ?) + `, + [ + chapter.page || '1', + index, + chapter.path, + novelId, + chapter.page || '1', + index, + ], + ); + } + }, ); }); }); }; -const getChaptersQuery = (sort = 'ORDER BY id ASC', filter = '') => - `SELECT * FROM Chapter WHERE novelId = ? ${filter} ${sort}`; +const getPageChaptersQuery = ( + sort = 'ORDER BY position ASC', + filter = '', + page = '1', +) => + `SELECT * FROM Chapter WHERE novelId = ? AND page = '${page}' ${filter} ${sort}`; -export const getChapters = ( +export const getCustomPages = ( novelId: number, - sort?: string, - filter?: string, -): Promise => { +): Promise<{ page: string }[]> => { + return new Promise(resolve => { + db.transaction(tx => { + tx.executeSql( + 'SELECT DISTINCT page from Chapter WHERE novelId = ?', + [novelId], + (txObj, { rows }) => resolve(rows._array), + ); + }); + }); +}; + +export const getNovelChapters = (novelId: number): Promise => { return new Promise(resolve => db.transaction(tx => { tx.executeSql( - getChaptersQuery(sort, filter), + 'SELECT * FROM Chapter WHERE novelId = ?', [novelId], (txObj, { rows }) => resolve((rows as any)._array), txnErrorCallback, @@ -67,17 +98,18 @@ export const getChapters = ( ); }; -// downloaded chapter -const getChapterQuery = - 'SELECT chapterText FROM Download WHERE Download.chapterId = ?'; - -export const getChapterFromDB = (chapterId: number): Promise => { +export const getPageChapters = ( + novelId: number, + sort?: string, + filter?: string, + page?: string, +): Promise => { return new Promise(resolve => db.transaction(tx => { tx.executeSql( - getChapterQuery, - [chapterId], - (txObj, { rows }) => resolve(rows.item(0)?.chapterText), + getPageChaptersQuery(sort, filter, page), + [novelId], + (txObj, { rows }) => resolve((rows as any)._array), txnErrorCallback, ); }), @@ -86,7 +118,7 @@ export const getChapterFromDB = (chapterId: number): Promise => { const getPrevChapterQuery = ` SELECT - id, * + * FROM Chapter WHERE @@ -117,7 +149,7 @@ export const getPrevChapter = ( const getNextChapterQuery = ` SELECT - id, * + * FROM Chapter WHERE @@ -250,14 +282,14 @@ export const downloadChapter = async ( pluginId: string, novelId: number, chapterId: number, - chapterUrl: string, + chapterPath: string, ) => { try { const plugin = getPlugin(pluginId); if (!plugin) { throw new Error(getString('downloadScreen.pluginNotFound')); } - const chapterText = await plugin.parseChapter(chapterUrl); + const chapterText = await plugin.parseChapter(chapterPath); if (chapterText && chapterText.length) { await downloadFiles(chapterText, plugin, novelId, chapterId); db.transaction(tx => { @@ -386,19 +418,24 @@ export const deleteReadChaptersFromDb = async () => { showToast(getString('novelScreen.readChaptersDeleted')); }; -const bookmarkChapterQuery = 'UPDATE Chapter SET bookmark = ? WHERE id = ?'; +export const updateChapterProgress = async ( + chapterId: number, + progress: number, +) => { + db.transaction(tx => { + tx.executeSql('UPDATE Chapter SET progress = ? WHERE id = ?', [ + progress, + chapterId, + ]); + }); +}; + +const bookmarkChapterQuery = + 'UPDATE Chapter SET bookmark = (CASE WHEN bookmark = 0 THEN 1 ELSE 0 END) WHERE id = ?'; -export const bookmarkChapter = async (bookmark: boolean, chapterId: number) => { +export const bookmarkChapter = async (chapterId: number) => { db.transaction(tx => { - tx.executeSql( - bookmarkChapterQuery, - [1 - Number(bookmark), chapterId], - (_txObj, _res) => {}, - (_txObj, _error) => { - // console.log('Error ', error) - return false; - }, - ); + tx.executeSql(bookmarkChapterQuery, [chapterId]); }); }; @@ -445,7 +482,7 @@ export const markPreviousChaptersUnread = async ( const getDownloadedChaptersQuery = ` SELECT Chapter.*, - Novel.pluginId, Novel.name as novelName, Novel.cover as novelCover, Novel.url as novelUrl + Novel.pluginId, Novel.name as novelName, Novel.cover as novelCover, Novel.path as novelPath FROM Chapter JOIN Novel ON Chapter.novelId = Novel.id @@ -473,7 +510,7 @@ export const getDownloadedChapters = (): Promise => { const getUpdatesQuery = ` SELECT Chapter.*, - pluginId, Novel.id as novelId, Novel.name as novelName, Novel.url as novelUrl, cover as novelCover + pluginId, Novel.id as novelId, Novel.name as novelName, Novel.path as novelPath, cover as novelCover FROM Chapter JOIN diff --git a/src/database/queries/HistoryQueries.ts b/src/database/queries/HistoryQueries.ts index ffdeda32d..abd9ad00b 100644 --- a/src/database/queries/HistoryQueries.ts +++ b/src/database/queries/HistoryQueries.ts @@ -9,8 +9,7 @@ import { getString } from '@strings/translations'; const getHistoryQuery = ` SELECT - Chapter.novelId, Novel.pluginId, Novel.name as novelName, Novel.url as novelUrl, Novel.cover as novelCover, - Chapter.id, Chapter.name as chapterName, Chapter.url as chapterUrl, Chapter.bookmark, Chapter.readTime + Chapter.*, Novel.pluginId, Novel.name as novelName, Novel.path as novelPath, Novel.cover as novelCover FROM Chapter JOIN Novel ON Chapter.novelId = Novel.id AND Chapter.readTime IS NOT NULL diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index f87a1ba72..807fd70ea 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -71,7 +71,7 @@ const getLibraryWithCategoryQuery = ` ) as NC ON Novel.id = NC.novelId WHERE inLibrary = 1 ) as NIL - JOIN + LEFT JOIN ( SELECT SUM(unread) as chaptersUnread, SUM(isDownloaded) as chaptersDownloaded, diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts index 258af0f3b..b93f04551 100644 --- a/src/database/queries/NovelQueries.ts +++ b/src/database/queries/NovelQueries.ts @@ -20,13 +20,13 @@ export const insertNovelAndChapters = async ( sourceNovel: SourceNovel, ): Promise => { const insertNovelQuery = - 'INSERT INTO Novel (url, pluginId, name, cover, summary, author, artist, status, genres) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)'; + 'INSERT INTO Novel (path, pluginId, name, cover, summary, author, artist, status, genres, totalPages) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const novelId: number | undefined = await new Promise(resolve => { db.transaction(tx => { tx.executeSql( insertNovelQuery, [ - sourceNovel.url, + sourceNovel.path, pluginId, sourceNovel.name, sourceNovel.cover || null, @@ -35,6 +35,7 @@ export const insertNovelAndChapters = async ( sourceNovel.artist || null, sourceNovel.status || null, sourceNovel.genres || null, + sourceNovel.totalPages || 0, ], async (txObj, resultSet) => resolve(resultSet.insertId), txnErrorCallback, @@ -78,12 +79,15 @@ export const getAllNovels = async (): Promise => { ); }; -export const getNovel = async (novelUrl: string): Promise => { +export const getNovel = async ( + novelPath: string, + pluginId: string, +): Promise => { return new Promise(resolve => db.transaction(tx => { tx.executeSql( - 'SELECT * FROM Novel WHERE url = ?', - [novelUrl], + 'SELECT * FROM Novel WHERE path = ? AND pluginId = ?', + [novelPath, pluginId], (txObj, { rows }) => resolve(rows.item(0)), txnErrorCallback, ); @@ -95,10 +99,10 @@ export const getNovel = async (novelUrl: string): Promise => { // else remove all it's categories export const switchNovelToLibrary = async ( - novelUrl: string, + novelPath: string, pluginId: string, ) => { - const novel = await getNovel(novelUrl); + const novel = await getNovel(novelPath, pluginId); if (novel) { db.transaction(tx => { tx.executeSql( @@ -121,16 +125,22 @@ export const switchNovelToLibrary = async ( () => showToast(getString('browseScreen.addedToLibrary')), txnErrorCallback, ); + if (novel.pluginId === 'local') { + tx.executeSql( + 'INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, 2)', + [novel.id], + ); + } } }); } else { - const sourceNovel = await fetchNovel(pluginId, novelUrl); + const sourceNovel = await fetchNovel(pluginId, novelPath); const novelId = await insertNovelAndChapters(pluginId, sourceNovel); if (novelId) { db.transaction(tx => { tx.executeSql( - 'UPDATE Novel SET inLibrary = 1 WHERE url = ?', - [novelUrl], + 'UPDATE Novel SET inLibrary = 1 WHERE id = ?', + [novelId], () => showToast(getString('browseScreen.addedToLibrary')), txnErrorCallback, ); @@ -183,10 +193,10 @@ export const deleteCachedNovels = async () => { }; const restoreFromBackupQuery = - 'INSERT OR REPLACE INTO Novel (url, name, pluginId, cover, summary, author, artist, status, genres) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)'; + 'INSERT OR REPLACE INTO Novel (path, name, pluginId, cover, summary, author, artist, status, genres) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)'; export const restoreLibrary = async (novel: NovelInfo) => { - const sourceNovel = await fetchNovel(novel.pluginId, novel.url).catch(e => { + const sourceNovel = await fetchNovel(novel.pluginId, novel.path).catch(e => { throw e; }); const novelId: number | undefined = await new Promise(resolve => { @@ -194,7 +204,7 @@ export const restoreLibrary = async (novel: NovelInfo) => { tx.executeSql( restoreFromBackupQuery, [ - sourceNovel.url, + sourceNovel.path, novel.name, novel.pluginId, novel.cover || '', @@ -238,11 +248,11 @@ export const restoreLibrary = async (novel: NovelInfo) => { export const updateNovelInfo = async (info: NovelInfo) => { db.transaction(tx => { tx.executeSql( - 'UPDATE Novel SET name = ?, cover = ?, url = ? , summary = ?, author = ?, artist = ?, genres = ?, status = ?, isLocal = ? WHERE id = ?', + 'UPDATE Novel SET name = ?, cover = ?, path = ?, summary = ?, author = ?, artist = ?, genres = ?, status = ?, isLocal = ? WHERE id = ?', [ info.name, info.cover || '', - info.url, + info.path, info.summary || '', info.author || '', info.artist || '', @@ -319,53 +329,28 @@ export const updateNovelCategories = async ( }); }; -export const _restoreNovelAndChapters = (novel: BackupNovel) => { +const restoreObjectQuery = (table: string, obj: any) => { + return ` + INSERT INTO ${table} + (${Object.keys(obj).join(',')}) + VALUES (${Object.keys(obj) + .map(() => '?') + .join(',')}) + `; +}; + +export const _restoreNovelAndChapters = (backupNovel: BackupNovel) => { + const { chapters, ...novel } = backupNovel; db.transaction(tx => { tx.executeSql('DELETE FROM Novel WHERE id = ?', [novel.id]); tx.executeSql( - `INSERT INTO - Novel ( - id, url, pluginId, name, cover, summary, - author, artist, status, genres, inLibrary, isLocal - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - [ - novel.id, - novel.url, - novel.pluginId, - novel.name, - novel.cover || null, - novel.summary || null, - novel.author || null, - novel.artist || null, - novel.status || null, - novel.genres || null, - Number(novel.inLibrary), - Number(novel.isLocal), - ], + restoreObjectQuery('Novel', novel), + Object.values(novel) as string[] | number[], ); - for (const chapter of novel.chapters) { + for (const chapter of chapters) { tx.executeSql( - `INSERT INTO - Chapter ( - id, novelId, url, name, releaseTime, bookmark, - unread, readTime, isDownloaded, updatedTime - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - [ - chapter.id, - chapter.novelId, - chapter.url, - chapter.name, - chapter.releaseTime || null, - Number(chapter.bookmark), - Number(chapter.unread), - chapter.readTime, - Number(chapter.isDownloaded), - chapter.updatedTime, - ], + restoreObjectQuery('Chapter', chapter), + Object.values(chapter) as string[] | number[], ); } }); diff --git a/src/database/tables/ChapterTable.ts b/src/database/tables/ChapterTable.ts index 391550b96..a3d555bca 100644 --- a/src/database/tables/ChapterTable.ts +++ b/src/database/tables/ChapterTable.ts @@ -2,7 +2,7 @@ export const createChapterTableQuery = ` CREATE TABLE IF NOT EXISTS Chapter ( id INTEGER PRIMARY KEY AUTOINCREMENT, novelId INTEGER NOT NULL, - url TEXT NOT NULL UNIQUE, + path TEXT NOT NULL, name TEXT NOT NULL, releaseTime TEXT, bookmark INTEGER DEFAULT 0, @@ -11,7 +11,10 @@ export const createChapterTableQuery = ` isDownloaded INTEGER DEFAULT 0, updatedTime TEXT, chapterNumber REAL NULL, - page INTEGER DEFAULT 1, + page TEXT DEFAULT "1", + position INTEGER DEFAULT 0, + progress INTEGER, + UNIQUE(path, novelId), FOREIGN KEY (novelId) REFERENCES Novel(id) ON DELETE CASCADE ) `; diff --git a/src/database/tables/NovelTable.ts b/src/database/tables/NovelTable.ts index 983e51685..3cd2d8b30 100644 --- a/src/database/tables/NovelTable.ts +++ b/src/database/tables/NovelTable.ts @@ -1,7 +1,7 @@ export const createNovelTableQuery = ` CREATE TABLE IF NOT EXISTS Novel ( id INTEGER PRIMARY KEY AUTOINCREMENT, - url TEXT NOT NULL UNIQUE, + path TEXT NOT NULL, pluginId TEXT NOT NULL, name TEXT NOT NULL, cover TEXT, @@ -12,6 +12,7 @@ export const createNovelTableQuery = ` genres TEXT, inLibrary INTEGER DEFAULT 0, isLocal INTEGER DEFAULT 0, - totalPages INTEGER DEFAULT 1 + totalPages INTEGER DEFAULT 0, + UNIQUE(path, pluginId) ); `; diff --git a/src/database/types/index.ts b/src/database/types/index.ts index 9c637776f..3553818f7 100644 --- a/src/database/types/index.ts +++ b/src/database/types/index.ts @@ -1,7 +1,7 @@ import { NovelStatus } from '@plugins/types'; export interface NovelInfo { id: number; - url: string; + path: string; pluginId: string; name: string; cover?: string; @@ -12,6 +12,7 @@ export interface NovelInfo { genres?: string; inLibrary: boolean; isLocal: boolean; + totalPages: number; } export interface LibraryNovelInfo extends NovelInfo { @@ -23,7 +24,7 @@ export interface LibraryNovelInfo extends NovelInfo { export interface ChapterInfo { id: number; novelId: number; - url: string; + path: string; name: string; releaseTime?: string; readTime: string | null; @@ -32,33 +33,30 @@ export interface ChapterInfo { isDownloaded: boolean; updatedTime: string | null; chapterNumber?: number; + page: string; + progress: number | null; } export interface DownloadedChapter extends ChapterInfo { pluginId: string; novelName: string; - novelUrl: string; + novelPath: string; novelCover: string; } -export interface History { - id: number; // chapterId xD +export interface History extends ChapterInfo { pluginId: string; - novelId: number; novelName: string; - novelUrl: string; + novelPath: string; novelCover: string; - chapterName: string; - chapterUrl: string; readTime: string; - bookmark: number; } export interface Update extends ChapterInfo { updatedTime: string; pluginId: string; novelName: string; - novelUrl: string; + novelPath: string; novelCover: string; } diff --git a/src/hooks/persisted/useDownload.ts b/src/hooks/persisted/useDownload.ts index 07afd62ec..26b3969b0 100644 --- a/src/hooks/persisted/useDownload.ts +++ b/src/hooks/persisted/useDownload.ts @@ -21,6 +21,8 @@ interface TaskData { delay: number; } +const defaultQueue: DownloadData[] = []; + const downloadChapterAction = async (taskData?: TaskData) => { try { MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.DOWNLOAD_CHAPTER); @@ -39,7 +41,7 @@ const downloadChapterAction = async (taskData?: TaskData) => { novel.pluginId, novel.id, chapter.id, - chapter.url, + chapter.path, ).catch((error: Error) => Notifications.scheduleNotificationAsync({ content: { @@ -51,7 +53,7 @@ const downloadChapterAction = async (taskData?: TaskData) => { trigger: null, }), ); - // get the newtest queue; + // get the newest queue; queue = getMMKVObject(DOWNLOAD_QUEUE) || []; setMMKVObject(DOWNLOAD_QUEUE, queue.slice(1)); queue = getMMKVObject(DOWNLOAD_QUEUE) || []; @@ -73,7 +75,8 @@ const downloadChapterAction = async (taskData?: TaskData) => { }; export default function useDownload() { - const [queue = [], setQueue] = useMMKVObject(DOWNLOAD_QUEUE); + const [queue = defaultQueue, setQueue] = + useMMKVObject(DOWNLOAD_QUEUE); const downloadChapter = (novel: NovelInfo, chapter: ChapterInfo) => { setQueue([...queue, { novel, chapter }]); diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts index 88cfe1da1..9de23249a 100644 --- a/src/hooks/persisted/useNovel.ts +++ b/src/hooks/persisted/useNovel.ts @@ -1,8 +1,8 @@ import { SearchResult, UserListEntry } from '@services/Trackers'; -import { useMMKVObject } from 'react-native-mmkv'; +import { useMMKVNumber, useMMKVObject } from 'react-native-mmkv'; import { TrackerMetadata, getTracker } from './useTracker'; import { ChapterInfo, NovelInfo } from '@database/types'; -import { MMKVStorage, getMMKVObject } from '@utils/mmkv/mmkv'; +import { MMKVStorage, getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv'; import { getNovel as _getNovel, deleteCachedNovels as _deleteCachedNovels, @@ -18,48 +18,43 @@ import { markChapterUnread as _markChapterUnread, deleteChapter as _deleteChapter, deleteChapters as _deleteChapters, + getPageChapters as _getPageChapters, + insertChapters, + getCustomPages, } from '@database/queries/ChapterQueries'; -import { fetchNovel } from '@services/plugin/fetch'; -import { getChapters } from '@database/queries/ChapterQueries'; -import { updateNovel as _updateNovel } from '@services/updates/LibraryUpdateQueries'; -import { APP_SETTINGS, AppSettings } from './useSettings'; +import { fetchNovel, fetchPage } from '@services/plugin/fetch'; import { showToast } from '@utils/showToast'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { NovelDownloadFolder } from '@utils/constants/download'; import * as RNFS from 'react-native-fs'; import { getString } from '@strings/translations'; +import { ChapterItem } from '@plugins/types'; -// store key: PREFIX + '_' + novel.url, +// store key: '__', +// store key: '_', export const TRACKED_NOVEL_PREFIX = 'TRACKED_NOVEL_PREFIX'; -export const NOVEL_PREFIX = 'NOVEL_PREFIX'; -export const NOVEL_CHAPTERS_PREFIX = 'NOVEL_CHAPTERS_PREFIX'; - +export const NOVEL_LATEST_CHAPTER_PREFIX = 'NOVEL_LATEST_CHAPTER'; +export const NOVEL_PAGE_INDEX_PREFIX = 'NOVEL_PAGE_INDEX_PREFIX'; +export const NOVEL_PAGE_UPDATES_PREFIX = 'NOVEL_PAGES_UPDATES_PREFIX'; export const NOVEL_SETTINSG_PREFIX = 'NOVEL_SETTINGS'; export const LAST_READ_PREFIX = 'LAST_READ_PREFIX'; -export const PROGRESS_PREFIX = 'PROGRESS_PREFIX'; type TrackedNovel = SearchResult & UserListEntry; -interface NovelSettings { +export interface NovelSettings { sort?: string; filter?: string; showChapterTitles?: boolean; } -interface ChapterProgress { - offsetY: number; - percentage: number; -} - -export interface NovelProgress { - [chapterId: number]: ChapterProgress; -} +const defaultNovelSettings: NovelSettings = {}; +const defaultPageIndex = 0; -export const useTrackedNovel = (url: string) => { +export const useTrackedNovel = (novelId: number) => { const [trackedNovel, setValue] = useMMKVObject( - TRACKED_NOVEL_PREFIX + '_' + url, + `${TRACKED_NOVEL_PREFIX}_${novelId}`, ); const trackNovel = (tracker: TrackerMetadata, novel: SearchResult) => { @@ -112,68 +107,82 @@ export const useTrackedNovel = (url: string) => { }; }; -export const useNovel = (url: string, pluginId: string) => { - const [novel, setNovel] = useMMKVObject(NOVEL_PREFIX + '_' + url); - const [chapters = [], setChapters] = useMMKVObject( - NOVEL_CHAPTERS_PREFIX + '_' + url, - ); +export const useNovel = (novelPath: string, pluginId: string) => { + const [loading, setLoading] = useState(true); + const [novel, setNovel] = useState(); + const [chapters, setChapters] = useState([]); + const [pages, setPages] = useState([]); + + const [pageIndex = defaultPageIndex, setPageIndex] = useMMKVNumber(` + ${NOVEL_PAGE_INDEX_PREFIX}_${pluginId}_${novelPath} + `); const [lastRead, setLastRead] = useMMKVObject( - LAST_READ_PREFIX + '_' + url, + `${LAST_READ_PREFIX}_${pluginId}_${novelPath}`, ); - const [novelSettings = {}, setNovelSettings] = useMMKVObject( - NOVEL_SETTINSG_PREFIX + '_' + url, - ); - const [progress = {}, _setProgress] = useMMKVObject( - PROGRESS_PREFIX + '_' + url, - ); - - const getNovel = () => { - return _getNovel(url) - .then(_novel => { - if (_novel) { - return _novel; - } else { - // if novel is not in db, fetch it - return fetchNovel(pluginId, url).then(sourceNovel => - insertNovelAndChapters(pluginId, sourceNovel).then(() => - _getNovel(url), + const [novelSettings = defaultNovelSettings, setNovelSettings] = + useMMKVObject( + `${NOVEL_SETTINSG_PREFIX}_${pluginId}_${novelPath}`, + ); + const getNovel = async () => { + let novel = await _getNovel(novelPath, pluginId); + if (!novel) { + const sourceNovel = await fetchNovel(pluginId, novelPath).catch(() => { + throw new Error(getString('updatesScreen.unableToGetNovel')); + }); + await insertNovelAndChapters(pluginId, sourceNovel); + novel = await _getNovel(novelPath, pluginId); + if (!novel) { + return; + } + } + let pages: string[]; + if (novel.totalPages > 0) { + pages = Array(novel.totalPages) + .fill(0) + .map((v, idx) => String(idx + 1)); + const key = `${NOVEL_PAGE_UPDATES_PREFIX}_${novel.id}`; + const hasUpdates = getMMKVObject(key); + if (hasUpdates) { + if (pages.length > hasUpdates.length) { + setMMKVObject( + key, + hasUpdates.concat( + Array(pages.length - hasUpdates.length).fill(false), ), ); } - }) - .then(async novel => { - if (novel) { - setNovel(novel); - return await getChapters( - novel.id, - novelSettings?.sort, - novelSettings?.filter, - ).then(chapters => setChapters(chapters)); - } else { - throw new Error(getString('updatesScreen.unableToGetNovel')); - } - }); + } else { + setMMKVObject(key, Array(novel.totalPages).fill(false)); + } + } else { + pages = (await getCustomPages(novel.id)).map(c => c.page); + } + setPages(pages); + setNovel(novel); }; - // should call this when only data in db changed. - const refreshChapters = () => { + const openPage = useCallback((index: number) => { + setPageIndex(index); + }, []); + + const refreshChapters = useCallback(() => { if (novel) { - getChapters(novel.id, novelSettings.sort, novelSettings.filter).then( - chapters => setChapters(chapters), - ); + _getPageChapters( + novel.id, + novelSettings.sort, + novelSettings.filter, + pages[pageIndex], + ).then(chapters => setChapters(chapters)); } - }; + }, [novel, pageIndex]); const sortAndFilterChapters = async (sort?: string, filter?: string) => { - if (novel?.id) { + if (novel) { setNovelSettings({ showChapterTitles: novelSettings?.showChapterTitles, sort, filter, }); - await getChapters(novel.id, sort, filter).then(chapters => { - setChapters(chapters); - }); } }; @@ -182,7 +191,7 @@ export const useNovel = (url: string, pluginId: string) => { }; const followNovel = () => { - switchNovelToLibrary(url, pluginId).then(() => { + switchNovelToLibrary(novelPath, pluginId).then(() => { if (novel) { setNovel({ ...novel, @@ -194,7 +203,7 @@ export const useNovel = (url: string, pluginId: string) => { const bookmarkChapters = (_chapters: ChapterInfo[]) => { _chapters.map(_chapter => { - _bookmarkChapter(_chapter.bookmark, _chapter.id); + _bookmarkChapter(_chapter.id); }); setChapters( chapters.map(chapter => { @@ -276,18 +285,6 @@ export const useNovel = (url: string, pluginId: string) => { ); }; - const updateNovel = async () => { - const settings = getMMKVObject(APP_SETTINGS); - if (novel) { - await _updateNovel(pluginId, url, novel.id, { - downloadNewChapters: settings?.downloadNewChapters, - refreshNovelMetadata: settings?.refreshNovelMetadata, - }).then(() => { - getNovel(); - }); - } - }; - const deleteChapter = (_chapter: ChapterInfo) => { if (novel) { _deleteChapter(novel.pluginId, novel.id, _chapter.id).then(() => { @@ -333,31 +330,77 @@ export const useNovel = (url: string, pluginId: string) => { [novel], ); - const setProgress = ( - chapterId: number, - offsetY: number, - percentage: number, - ) => { - _setProgress({ - ...progress, - [chapterId]: { - offsetY, - percentage, - }, - }); - }; + useEffect(() => { + getNovel().then(() => setLoading(false)); + }, []); + + useEffect(() => { + const getChapters = async () => { + const page = pages[pageIndex]; + if (novel && page) { + const hasUpdatesKey = `${NOVEL_PAGE_UPDATES_PREFIX}_${novel.id}`; + const hasUpdates = getMMKVObject(hasUpdatesKey); + let chapters = await _getPageChapters( + novel.id, + novelSettings.sort, + novelSettings.filter, + page, + ); + if (hasUpdates && (hasUpdates[pageIndex] || !chapters.length)) { + const sourcePage = await fetchPage(pluginId, novelPath, page); + const sourceChapters = sourcePage.chapters.map(ch => { + return { + ...ch, + page, + }; + }); + await insertChapters(novel.id, sourceChapters); + const latestChapkey = `${NOVEL_LATEST_CHAPTER_PREFIX}_${novel.id}`; + const latestChapter = getMMKVObject(latestChapkey); + if ( + sourcePage.latestChapter && + sourcePage.latestChapter.path !== latestChapter?.path + ) { + setMMKVObject(latestChapkey, sourcePage.latestChapter); + setMMKVObject( + hasUpdatesKey, + hasUpdates?.map((val, idx) => { + if (idx !== pageIndex) { + return false; + } + return true; + }), + ); + } else { + hasUpdates[pageIndex] = false; + setMMKVObject(hasUpdatesKey, hasUpdates); + } + chapters = await _getPageChapters( + novel.id, + novelSettings.sort, + novelSettings.filter, + page, + ); + } + setChapters(chapters); + } + }; + getChapters(); + }, [novel, novelSettings, pageIndex]); return { - progress, + loading, + pageIndex, + pages, novel, lastRead, chapters, novelSettings, - setProgress, getNovel, + setPageIndex, + openPage, setNovel, setLastRead, - setNovelSettings, sortAndFilterChapters, followNovel, bookmarkChapters, @@ -365,7 +408,6 @@ export const useNovel = (url: string, pluginId: string) => { markChaptersRead, markPreviousChaptersUnread, markChaptersUnread, - updateNovel, setShowChapterTitles, markChapterRead, refreshChapters, @@ -377,12 +419,16 @@ export const useNovel = (url: string, pluginId: string) => { export const deleteCachedNovels = async () => { const cachedNovels = await _getCachedNovels(); for (let novel of cachedNovels) { - MMKVStorage.delete(TRACKED_NOVEL_PREFIX + '_' + novel.url); - MMKVStorage.delete(NOVEL_PREFIX + '_' + novel.url); - MMKVStorage.delete(NOVEL_CHAPTERS_PREFIX + '_' + novel.url); - MMKVStorage.delete(NOVEL_SETTINSG_PREFIX + '_' + novel.url); - MMKVStorage.delete(LAST_READ_PREFIX + '_' + novel.url); - MMKVStorage.delete(PROGRESS_PREFIX + '_' + novel.url); + MMKVStorage.delete(`${TRACKED_NOVEL_PREFIX}_${novel.id}`); + MMKVStorage.delete(`${NOVEL_LATEST_CHAPTER_PREFIX}_${novel.id}`); + MMKVStorage.delete(`${NOVEL_PAGE_UPDATES_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 = NovelDownloadFolder + '/' + novel.pluginId + '/' + novel.id; if (await RNFS.exists(novelDir)) { diff --git a/src/hooks/persisted/useSettings.ts b/src/hooks/persisted/useSettings.ts index b8d23fbbe..53de1f9da 100644 --- a/src/hooks/persisted/useSettings.ts +++ b/src/hooks/persisted/useSettings.ts @@ -139,7 +139,7 @@ const initialAppSettings: AppSettings = { */ hideBackdrop: false, - defaultChapterSort: 'ORDER BY id ASC', + defaultChapterSort: 'ORDER BY position ASC', }; const initialBrowseSettings: BrowseSettings = { diff --git a/src/navigators/types/index.ts b/src/navigators/types/index.ts index 4251cfcba..2db685ef2 100644 --- a/src/navigators/types/index.ts +++ b/src/navigators/types/index.ts @@ -8,7 +8,7 @@ import { StackScreenProps } from '@react-navigation/stack'; export type RootStackParamList = { BottomNavigator: NavigatorScreenParams; - Novel: { name: string; url: string; pluginId: string }; + Novel: { name: string; path: string; pluginId: string }; Chapter: { novel: NovelInfo; chapter: ChapterInfo; @@ -17,7 +17,7 @@ export type RootStackParamList = { SourceScreen: { pluginId: string; pluginName: string; - pluginUrl: string; + site: string; showLatestNovels?: boolean; }; BrowseMal: undefined; @@ -30,6 +30,7 @@ export type RootStackParamList = { WebviewScreen: { name: string; url: string; + pluginId?: string; }; }; @@ -46,6 +47,11 @@ export type LibraryScreenProps = CompositeScreenProps< StackScreenProps >; +export type HistoryScreenProps = CompositeScreenProps< + MaterialBottomTabScreenProps, + StackScreenProps +>; + export type BrowseScreenProps = CompositeScreenProps< MaterialBottomTabScreenProps, StackScreenProps diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 7fcda4064..ad7a86539 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -14,6 +14,8 @@ import { defaultCover } from './helpers/constants'; import { encode, decode } from 'urlencode'; import { getString } from '@strings/translations'; +const pluginsFilePath = PluginDownloadFolder + '/plugins.json'; + const packages: Record = { 'cheerio': { load }, 'dayjs': dayjs, @@ -29,7 +31,7 @@ const _require = (packageName: string) => { return packages[packageName]; }; -const initPlugin = (rawCode: string, path?: string) => { +const initPlugin = (rawCode: string) => { try { /* eslint no-new-func: "off", curly: "error" */ const plugin: Plugin = Function( @@ -39,20 +41,46 @@ const initPlugin = (rawCode: string, path?: string) => { ${rawCode}; return exports.default`, )(_require, {}); - plugin.path = path || `${PluginDownloadFolder}/${plugin.id}.js`; return plugin; } catch (e) { return undefined; } }; -let plugins: Record = {}; +const plugins: Record = {}; -// get existing plugin in device -const setupPlugin = async (path: string) => { - const rawCode = await RNFS.readFile(path, 'utf8'); - const plugin = initPlugin(rawCode, path); - return plugin; +const serializePlugin = async ( + pluginId: string, + rawCode: string, + installed: boolean, +) => { + let serializedPlugins: Record = {}; + if (await RNFS.exists(pluginsFilePath)) { + const content = await RNFS.readFile(pluginsFilePath); + serializedPlugins = JSON.parse(content); + } + if (installed) { + serializedPlugins[pluginId] = rawCode; + } else { + delete serializedPlugins[pluginId]; + } + if (!(await RNFS.exists(PluginDownloadFolder))) { + await RNFS.mkdir(PluginDownloadFolder); + } + await RNFS.writeFile(pluginsFilePath, JSON.stringify(serializedPlugins)); +}; + +const deserializePlugins = async () => { + if (await RNFS.exists(pluginsFilePath)) { + const content = await RNFS.readFile(pluginsFilePath); + const serializedPlugins: Record = JSON.parse(content); + for (const pluginId in serializedPlugins) { + const plugin = initPlugin(serializedPlugins[pluginId]); + if (plugin) { + plugins[pluginId] = plugin; + } + } + } }; const installPlugin = async (url: string): Promise => { @@ -66,20 +94,13 @@ const installPlugin = async (url: string): Promise => { if (!plugin) { return undefined; } - const oldPlugin = plugins[plugin.id]; - if (oldPlugin) { - if (newer(plugin.version, oldPlugin.version)) { - plugins[oldPlugin.id] = plugin; - await RNFS.writeFile(plugin.path, rawCode, 'utf8'); - return plugin; - } else { - return oldPlugin; - } - } else { + let currentPlugin = plugins[plugin.id]; + if (!currentPlugin || newer(plugin.version, currentPlugin.version)) { plugins[plugin.id] = plugin; - await RNFS.writeFile(plugin.path, rawCode, 'utf8'); - return plugin; + currentPlugin = plugin; + await serializePlugin(plugin.id, rawCode, true); } + return currentPlugin; }); } catch (e: any) { throw e; @@ -87,38 +108,21 @@ const installPlugin = async (url: string): Promise => { }; const uninstallPlugin = async (_plugin: PluginItem) => { - const plugin = plugins[_plugin.id]; - if (plugin && (await RNFS.exists(plugin.path))) { - delete plugins[plugin.id]; - await RNFS.unlink(plugin.path); - } + delete plugins[_plugin.id]; + serializePlugin(_plugin.id, '', false); }; const updatePlugin = async (plugin: PluginItem) => { return installPlugin(plugin.url); }; -const collectPlugins = async () => { - if (!(await RNFS.exists(PluginDownloadFolder))) { - await RNFS.mkdir(PluginDownloadFolder); - return; - } - const paths = await RNFS.readDir(PluginDownloadFolder); - for (let item of paths) { - const plugin = await setupPlugin(item.path); - if (plugin) { - plugins[plugin.id] = plugin; - } - } -}; - const fetchPlugins = async () => { // plugins host const githubUsername = 'LNReader'; const githubRepository = 'lnreader-sources'; const availablePlugins: Record> = await fetch( - `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/dist/.dist/plugins.min.json`, + `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/beta-dist/.dist/plugins.min.json`, ) .then(res => res.json()) .catch(() => { @@ -140,7 +144,7 @@ export { installPlugin, uninstallPlugin, updatePlugin, - collectPlugins, + deserializePlugins, fetchPlugins, LOCAL_PLUGIN_ID, }; diff --git a/src/plugins/types/index.ts b/src/plugins/types/index.ts index 3ed8165bd..f00a97c1b 100644 --- a/src/plugins/types/index.ts +++ b/src/plugins/types/index.ts @@ -3,15 +3,16 @@ import { Language } from '@utils/constants/languages'; export interface NovelItem { name: string; - url: string; //must be absoulute + path: string; cover?: string; } export interface ChapterItem { name: string; - url: string; //must be absoulute + path: string; chapterNumber?: number; releaseTime?: string; + page?: string; } export enum NovelStatus { @@ -24,16 +25,19 @@ export enum NovelStatus { OnHiatus = 'On Hiatus', } -export interface SourceNovel { - url: string; //must be absoulute - name: string; - cover?: string; +export interface SourceNovel extends NovelItem { genres?: string; summary?: string; author?: string; artist?: string; status?: NovelStatus; - chapters?: ChapterItem[]; + chapters: ChapterItem[]; + totalPages?: number; +} + +export interface SourcePage { + chapters: ChapterItem[]; + latestChapter?: ChapterItem; } export interface PopularNovelsOptions { @@ -54,17 +58,14 @@ export interface PluginItem { } export interface Plugin extends PluginItem { - path: string; // path in device filters?: Filters; popularNovels: ( pageNo: number, options?: PopularNovelsOptions, ) => Promise; - parseNovelAndChapters: ( - novelUrl: string, - pageNo?: number, - ) => Promise; - parseChapter: (chapterUrl: string) => Promise; + parseNovel: (novelPath: string) => Promise; + parsePage?: (novelPath: string, page: string) => Promise; + parseChapter: (chapterPath: string) => Promise; searchNovels: (searchTerm: string, pageNo: number) => Promise; fetchImage: (url: string) => Promise; } diff --git a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx index 6ff83fdf6..d797345c1 100644 --- a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx +++ b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx @@ -25,7 +25,7 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { const theme = useTheme(); const previousScreen = usePreviousRouteName(); - const { pluginId, pluginName, pluginUrl, showLatestNovels } = route.params; + const { pluginId, pluginName, site, showLatestNovels } = route.params; const { isLoading, @@ -70,16 +70,17 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { const handleOpenWebView = async () => { navigation.navigate('WebviewScreen', { - pluginId, name: pluginName, - url: pluginUrl, + url: site, }); }; const { library, setLibrary } = useLibraryNovels(); - const novelInLibrary = (novelUrl: string) => - library?.some(novel => novel.url === novelUrl); + const novelInLibrary = (novelPath: string) => + library?.some( + novel => novel.pluginId === pluginId && novel.path === novelPath, + ); const navigateToNovel = useCallback( (item: NovelItem) => @@ -115,7 +116,7 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { { - const inLibrary = novelInLibrary(item.url); + const inLibrary = novelInLibrary(item.path); return ( { onPress={() => navigateToNovel(item)} isSelected={false} onLongPress={async () => { - await switchNovelToLibrary(item.url, pluginId); + await switchNovelToLibrary(item.path, pluginId); setLibrary(prevValues => { if (inLibrary) { return [ - ...prevValues.filter(novel => novel.url !== item.url), + ...prevValues.filter(novel => novel.path !== item.path), ]; } else { return [ ...prevValues, { ...item, + pluginId: pluginId, inLibrary: true, isLocal: false, } as NovelInfo, diff --git a/src/screens/BrowseSourceScreen/useBrowseSource.ts b/src/screens/BrowseSourceScreen/useBrowseSource.ts index 9585c791b..2dc023643 100644 --- a/src/screens/BrowseSourceScreen/useBrowseSource.ts +++ b/src/screens/BrowseSourceScreen/useBrowseSource.ts @@ -14,7 +14,7 @@ export const useBrowseSource = ( const [currentPage, setCurrentPage] = useState(1); const [filterValues, setFilterValues] = useState( - getPlugin(pluginId).filters, + getPlugin(pluginId)?.filters, ); const [selectedFilters, setSelectedFilters] = useState< FilterToValues | undefined @@ -28,6 +28,9 @@ export const useBrowseSource = ( if (isScreenMounted.current === true) { try { const plugin = getPlugin(pluginId); + if (!plugin) { + throw new Error(`Unknown plugin: ${pluginId}`); + } await plugin .popularNovels(page, { showLatestNovels, @@ -122,6 +125,9 @@ export const useSearchSource = (pluginId: string) => { if (isScreenMounted.current === true) { try { const plugin = getPlugin(pluginId); + if (!plugin) { + throw new Error(`Unknown plugin: ${pluginId}`); + } const res = await plugin.searchNovels(searchText, page); setSearchResults(prevState => page === 1 ? res : [...prevState, ...res], diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx index 6d73099f1..68a77846c 100644 --- a/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx @@ -13,7 +13,7 @@ interface Props { pluginId: string; navigateToNovel: (item: { name: string; - url: string; + path: string; pluginId: string; }) => void; theme: ThemeColors; diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx index dacb0d075..2c7667f27 100644 --- a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx @@ -33,13 +33,15 @@ const GlobalSearchResultsList: React.FC = ({ ); const { library, setLibrary } = useLibraryNovels(); - const novelInLibrary = (novelUrl: string) => - library?.some(novel => novel.url === novelUrl); + const novelInLibrary = (pluginId: string, novelPath: string) => + library?.some( + novel => novel.pluginId === pluginId && novel.path === novelPath, + ); const errorColor = useMemo(() => (theme.isDark ? '#B3261E' : '#F2B8B5'), []); const navigateToNovel = useCallback( - (item: { name: string; url: string; pluginId: string }) => + (item: { name: string; path: string; pluginId: string }) => navigation.push('Novel', item), [], ); @@ -61,7 +63,7 @@ const GlobalSearchResultsList: React.FC = ({ navigation.navigate('SourceScreen', { pluginId: item.plugin.id, pluginName: item.plugin.name, - url: item.plugin.url, + site: item.plugin.site, }) } > @@ -91,7 +93,9 @@ const GlobalSearchResultsList: React.FC = ({ novelItem.url} + keyExtractor={novelItem => + item.plugin.id + '_' + novelItem.path + } data={item.novels} ListEmptyComponent={ = ({ } renderItem={({ item: novelItem }) => { - const inLibrary = novelInLibrary(novelItem.url); + const inLibrary = novelInLibrary( + item.plugin.id, + novelItem.path, + ); return ( = ({ if (inLibrary) { return [ ...prevValues.filter( - novel => novel.url !== novelItem.url, + novel => novel.path !== novelItem.path, ), ]; } else { return [ ...prevValues, { - url: novelItem.url, + path: novelItem.path, } as LibraryNovelInfo, ]; } }); - switchNovelToLibrary(novelItem.url, item.plugin.id); + switchNovelToLibrary(novelItem.path, item.plugin.id); }} /> ); diff --git a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts index ce0c63766..a93ebb001 100644 --- a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts +++ b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts @@ -35,10 +35,14 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { setSearchResults(defaultResult); - filteredInstalledPlugins.forEach(async plugin => { + filteredInstalledPlugins.forEach(async _plugin => { if (isMounted.current) { try { - const res = await getPlugin(plugin.id).searchNovels(searchText, 1); + const plugin = getPlugin(_plugin.id); + if (!plugin) { + throw new Error(`Unknown plugin: ${_plugin.id}`); + } + const res = await plugin.searchNovels(searchText, 1); setSearchResults(prevState => prevState.map(prevResult => @@ -68,7 +72,7 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { } catch (error: any) { setSearchResults(prevState => prevState.map(prevResult => - prevResult.plugin.id === plugin.id + prevResult.plugin.id === _plugin.id ? { ...prevResult, novels: [], diff --git a/src/screens/WebviewScreen/WebviewScreen.tsx b/src/screens/WebviewScreen/WebviewScreen.tsx index 2b9095707..347077ff9 100644 --- a/src/screens/WebviewScreen/WebviewScreen.tsx +++ b/src/screens/WebviewScreen/WebviewScreen.tsx @@ -5,11 +5,13 @@ import { Appbar } from '@components'; import { useTheme } from '@hooks/persisted'; import { WebviewScreenProps } from '@navigators/types'; import { getUserAgent } from '@hooks/persisted/useUserAgent'; +import { isUrlAbsolute } from '@plugins/helpers/isAbsoluteUrl'; +import { getPlugin } from '@plugins/pluginManager'; const WebviewScreen = ({ route, navigation }: WebviewScreenProps) => { const theme = useTheme(); - const { name, url } = route.params; + const { name, url, pluginId } = route.params; return ( <> @@ -22,7 +24,12 @@ const WebviewScreen = ({ route, navigation }: WebviewScreenProps) => { ); diff --git a/src/screens/browse/components/PluginSection.tsx b/src/screens/browse/components/PluginSection.tsx index 6fdbeb9b2..23451edbb 100644 --- a/src/screens/browse/components/PluginSection.tsx +++ b/src/screens/browse/components/PluginSection.tsx @@ -38,7 +38,7 @@ const PluginSection = ({ navigation.navigate('SourceScreen', { pluginId: plugin.id, pluginName: plugin.name, - pluginUrl: plugin.site, + site: plugin.site, showLatestNovels, }); setLastUsedPlugin(plugin); diff --git a/src/screens/browse/migration/MigrationNovelList.tsx b/src/screens/browse/migration/MigrationNovelList.tsx index 108ca6084..1c333b4e7 100644 --- a/src/screens/browse/migration/MigrationNovelList.tsx +++ b/src/screens/browse/migration/MigrationNovelList.tsx @@ -23,7 +23,7 @@ interface MigrationNovelListProps { } interface SelectedNovel { - url: string; + path: string; name: string; } @@ -42,25 +42,26 @@ const MigrationNovelList = ({ const showMigrateNovelDialog = () => setMigrateNovelDialog(true); const hideMigrateNovelDialog = () => setMigrateNovelDialog(false); - const inLibrary = (url: string) => library.some(obj => obj.url === url); + const inLibrary = (path: string) => + library.some(obj => obj.pluginId === pluginId && obj.path === path); const renderItem: FlatListProps['renderItem'] = ({ item }) => ( showModal(item.url, item.name)} + onPress={() => showModal(item.path, item.name)} onLongPress={() => navigation.push('Novel', { pluginId: pluginId, ...item }) } - inLibrary={inLibrary(item.url)} + inLibrary={inLibrary(item.path)} /> ); - const showModal = (url: string, name: string) => { - if (inLibrary(url)) { + const showModal = (path: string, name: string) => { + if (inLibrary(path)) { showToast(getString('browseScreen.migration.novelAlreadyInLibrary')); } else { - setSelectedNovel({ url, name }); + setSelectedNovel({ path, name }); showMigrateNovelDialog(); } }; @@ -71,7 +72,7 @@ const MigrationNovelList = ({ contentContainerStyle={styles.flatListCont} horizontal={true} data={data.novels} - keyExtractor={(item, index) => index + item.url} + keyExtractor={(item, index) => index + item.path} renderItem={renderItem} ListEmptyComponent={ {getString('browseScreen.migration.dialogMessage', { - url: selectedNovel.url, + url: selectedNovel.path, })} { hideMigrateNovelDialog(); - migrateNovel(pluginId, fromNovel, selectedNovel.url).catch( + migrateNovel(pluginId, fromNovel, selectedNovel.path).catch( error => showToast(error.message), ); }} diff --git a/src/screens/browse/migration/MigrationNovels.tsx b/src/screens/browse/migration/MigrationNovels.tsx index bd8bb0306..2fbb59d93 100644 --- a/src/screens/browse/migration/MigrationNovels.tsx +++ b/src/screens/browse/migration/MigrationNovels.tsx @@ -52,7 +52,10 @@ const MigrationNovels = ({ navigation, route }: MigrateNovelScreenProps) => { if (isMounted.current === true) { try { const source = getPlugin(item.id); - const data = await source.searchNovels(novel.name); + if (!source) { + throw new Error(`Unknown plugin: ${item.id}`); + } + const data = await source.searchNovels(novel.name, 1); setSearchResults(prevState => prevState.map(pluginItem => pluginItem.id === item.id diff --git a/src/screens/history/HistoryScreen.tsx b/src/screens/history/HistoryScreen.tsx index 416a5209e..899a09981 100644 --- a/src/screens/history/HistoryScreen.tsx +++ b/src/screens/history/HistoryScreen.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import { StyleSheet, SectionList, Text } from 'react-native'; -import { NavigationProp, useNavigation } from '@react-navigation/native'; import dayjs from 'dayjs'; import { Portal } from 'react-native-paper'; @@ -12,22 +11,14 @@ import { useTheme, useHistory } from '@hooks/persisted'; import { convertDateToISOString } from '@database/utils/convertDateToISOString'; -import { History, NovelInfo } from '@database/types'; +import { History } from '@database/types'; import { getString } from '@strings/translations'; import ClearHistoryDialog from './components/ClearHistoryDialog'; -import { - openChapterChapterTypes, - openChapterNovelTypes, - openNovel, - openNovelProps, -} from '@utils/handleNavigateParams'; import HistorySkeletonLoading from './components/HistorySkeletonLoading'; -import { RootStackParamList } from '@navigators/types'; +import { HistoryScreenProps } from '@navigators/types'; -const HistoryScreen = () => { +const HistoryScreen = ({ navigation }: HistoryScreenProps) => { const theme = useTheme(); - const { navigate } = useNavigation>(); - const { isLoading, history, @@ -74,14 +65,6 @@ const HistoryScreen = () => { return groupedHistory; }; - const handleNavigateToChapter = ( - novel: openChapterNovelTypes, - chapter: openChapterChapterTypes, - ) => navigate('Chapter', { novel: novel as NovelInfo, chapter: chapter }); - - const handleNavigateToNovel = (novel: openNovelProps) => - navigate('Novel', openNovel(novel) as openNovelProps); - const { value: clearHistoryDialogVisible, setTrue: openClearHistoryDialog, @@ -122,9 +105,8 @@ const HistoryScreen = () => { renderItem={({ item }) => ( )} diff --git a/src/screens/history/components/HistoryCard/HistoryCard.tsx b/src/screens/history/components/HistoryCard/HistoryCard.tsx index c1306f0a8..03a7cc077 100644 --- a/src/screens/history/components/HistoryCard/HistoryCard.tsx +++ b/src/screens/history/components/HistoryCard/HistoryCard.tsx @@ -7,53 +7,37 @@ import { Image } from 'react-native'; import { IconButtonV2 } from '@components'; import { parseChapterNumber } from '@utils/parseChapterNumber'; -import { History } from '@database/types'; +import { History, NovelInfo } from '@database/types'; import { ThemeColors } from '@theme/types'; import { coverPlaceholderColor } from '@theme/colors'; -import { - openChapterChapterTypes, - openChapterNovelTypes, - openNovelProps, -} from '@utils/handleNavigateParams'; import { getString } from '@strings/translations'; +import { HistoryScreenProps } from '@navigators/types'; interface HistoryCardProps { history: History; - handleNavigateToChapter: ( - novel: openChapterNovelTypes, - chapter: openChapterChapterTypes, - ) => void; handleRemoveFromHistory: (chapterId: number) => void; - handleNavigateToNovel: (novel: openNovelProps) => void; + navigation: HistoryScreenProps['navigation']; theme: ThemeColors; } const HistoryCard: React.FC = ({ history, - handleNavigateToChapter, + navigation, handleRemoveFromHistory, - handleNavigateToNovel, theme, }) => { - const { - id, - pluginId, - novelId, - novelName, - novelUrl, - chapterName, - novelCover, - readTime, - chapterUrl, - bookmark, - } = history; const chapterNoAndTime = useMemo( () => `${getString('historyScreen.chapter')} ${parseChapterNumber( - novelName, - chapterName, - )} • ${dayjs(readTime).format('LT').toUpperCase()}`, - [chapterName, readTime], + history.novelName, + history.name, + )} • ${dayjs(history.readTime).format('LT').toUpperCase()}` + + `${ + history.progress && history.progress > 0 + ? ' • ' + history.progress + '%' + : '' + }`, + [history, history.readTime], ); return ( @@ -61,38 +45,34 @@ const HistoryCard: React.FC = ({ style={styles.container} android_ripple={{ color: theme.rippleColor }} onPress={() => - handleNavigateToChapter( - { url: novelUrl, pluginId: pluginId, name: novelName }, - { - id: id, - url: chapterUrl, - novelId: novelId, - name: chapterName, - bookmark: bookmark, - }, - ) + navigation.navigate('Chapter', { + novel: { + path: history.novelPath, + name: history.novelName, + pluginId: history.pluginId, + } as NovelInfo, + chapter: history, + }) } > - handleNavigateToNovel({ - pluginId, - id: novelId, - url: novelUrl, - name: novelName, - cover: novelCover, + navigation.navigate('Novel', { + name: history.name, + path: history.novelPath, + pluginId: history.pluginId, }) } > - + - {novelName} + {history.novelName} {chapterNoAndTime} @@ -103,7 +83,7 @@ const HistoryCard: React.FC = ({ handleRemoveFromHistory(id)} + onPress={() => handleRemoveFromHistory(history.id)} /> diff --git a/src/screens/library/LibraryScreen.tsx b/src/screens/library/LibraryScreen.tsx index c83311d8d..0fb8bfa74 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -36,7 +36,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading'; import { Row } from '@components/Common'; import { LibraryScreenProps } from '@navigators/types'; -import { ChapterInfo, NovelInfo } from '@database/types'; +import { NovelInfo } from '@database/types'; +import { importEpub } from '@services/epub/import'; type State = NavigationState<{ key: string; @@ -151,20 +152,28 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { }} onChangeText={onChangeText} leftIcon={selectedNovelIds.length ? 'close' : 'magnify'} - rightIcons={[ + rightIcons={ selectedNovelIds.length - ? { - iconName: 'select-all', - onPress: () => - setSelectedNovelIds( - library[index].novels.map(novel => novel.id), - ), - } - : { - iconName: 'filter-variant', - onPress: () => bottomSheetRef.current?.present(), - }, - ]} + ? [ + { + iconName: 'select-all', + onPress: () => + setSelectedNovelIds( + library[index].novels.map(novel => novel.id), + ), + }, + ] + : [ + { + iconName: 'book-arrow-up-outline', + onPress: importEpub, + }, + { + iconName: 'filter-variant', + onPress: () => bottomSheetRef.current?.present(), + }, + ] + } theme={theme} /> {downloadedOnlyMode && ( @@ -242,17 +251,11 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { onPress={() => { navigation.navigate('Chapter', { novel: { - id: history[0].novelId, - url: history[0].novelUrl, + path: history[0].novelPath, pluginId: history[0].pluginId, name: history[0].novelName, } as NovelInfo, - chapter: { - id: history[0].id, - url: history[0].chapterUrl, - name: history[0].chapterName, - novelId: history[0].novelId, - } as ChapterInfo, + chapter: history[0], }); }} /> diff --git a/src/screens/library/components/LibraryListView.tsx b/src/screens/library/components/LibraryListView.tsx index c946f9707..4df1f6556 100644 --- a/src/screens/library/components/LibraryListView.tsx +++ b/src/screens/library/components/LibraryListView.tsx @@ -12,6 +12,7 @@ import { getString } from '@strings/translations'; import { useTheme } from '@hooks/persisted'; import { updateLibrary } from '@services/updates'; import { LibraryScreenProps } from '@navigators/types'; +import { importEpub } from '@services/epub/import'; interface Props { categoryId: number; @@ -41,7 +42,7 @@ export const LibraryView: React.FC = ({ } else { navigation.navigate('Novel', { name: item.name, - url: item.url, + path: item.path, pluginId: item.pluginId, }); } @@ -73,6 +74,19 @@ export const LibraryView: React.FC = ({ theme={theme} icon="Σ(ಠ_ಠ)" description={getString('libraryScreen.empty')} + actions={[ + categoryId !== 2 + ? { + iconName: 'compass-outline', + title: getString('browse'), + onPress: () => navigation.navigate('Browse'), + } + : { + iconName: 'book-arrow-up-outline', + title: getString('advancedSettingsScreen.importEpub'), + onPress: importEpub, + }, + ]} /> } refreshControl={ diff --git a/src/screens/more/DownloadQueueScreen.tsx b/src/screens/more/DownloadQueueScreen.tsx index 094813092..6ae9d5e53 100644 --- a/src/screens/more/DownloadQueueScreen.tsx +++ b/src/screens/more/DownloadQueueScreen.tsx @@ -63,7 +63,9 @@ const DownloadQueue = ({ navigation }: DownloadQueueScreenProps) => { item.chapter.url.toString()} + keyExtractor={item => + item.novel.pluginId + '_' + item.chapter.path.toString() + } data={queue} renderItem={({ item }) => ( diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index 77726c75f..eaa3539d4 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -8,12 +8,12 @@ import { Text, NativeSyntheticEvent, NativeScrollEvent, + DrawerLayoutAndroid, } from 'react-native'; import { FlashList } from '@shopify/flash-list'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { - Provider, Portal, Appbar, IconButton, @@ -45,21 +45,38 @@ import { NovelScreenProps } from '@navigators/types'; import { ChapterInfo } from '@database/types'; import ChapterItem from './components/ChapterItem'; import { getString } from '@strings/translations'; +import NovelDrawer from './components/NovelDrawer'; +import { updateNovel } from '@services/updates/LibraryUpdateQueries'; +import { useFocusEffect } from '@react-navigation/native'; const Novel = ({ route, navigation }: NovelScreenProps) => { - const { name, url, pluginId } = route.params; + const { name, path, pluginId } = route.params; + const drawerRef = useRef(null); const [updating, setUpdating] = useState(false); const { - progress, + useFabForContinueReading, + defaultChapterSort, + disableHapticFeedback, + downloadNewChapters, + refreshNovelMetadata, + } = useAppSettings(); + const { + loading, + pageIndex, + pages, novel, chapters, lastRead, - novelSettings, + novelSettings: { + sort = defaultChapterSort, + filter = '', + showChapterTitles = false, + }, + openPage, setNovel, getNovel, sortAndFilterChapters, setShowChapterTitles, - updateNovel, bookmarkChapters, markChaptersRead, markChaptersUnread, @@ -69,9 +86,7 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { deleteChapter, refreshChapters, deleteChapters, - } = useNovel(url, pluginId); - - const [loading, setLoading] = useState(!novel); + } = useNovel(path, pluginId); const theme = useTheme(); const { top: topInset, bottom: bottomInset } = useSafeAreaInsets(); @@ -95,37 +110,31 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { const deleteDownloadsSnackbar = useBoolean(); - const { - useFabForContinueReading, - defaultChapterSort, - disableHapticFeedback, - } = useAppSettings(); - - const { - sort = defaultChapterSort, - filter = '', - showChapterTitles = false, - } = novelSettings; - const onPageScroll = (event: NativeSyntheticEvent) => { const y = event.nativeEvent.contentOffset.y; const currentScrollPosition = Math.floor(y) ?? 0; - setIsFabExtended(currentScrollPosition <= 0); + if (useFabForContinueReading && lastRead) { + setIsFabExtended(currentScrollPosition <= 0); + } }; - useEffect(() => { - getNovel().finally(() => setLoading(false)); - }, []); - useEffect(() => { refreshChapters(); }, [downloadQueue]); + useFocusEffect(refreshChapters); + const onRefresh = () => { - setUpdating(true); - updateNovel() - .then(() => showToast(getString('novelScreen.updatedToast', { name }))) - .finally(() => setUpdating(false)); + if (novel) { + setUpdating(true); + updateNovel(pluginId, novel.path, novel.id, { + downloadNewChapters, + refreshNovelMetadata, + }) + .then(() => getNovel()) + .then(() => showToast(getString('novelScreen.updatedToast', { name }))) + .finally(() => setUpdating(false)); + } }; const refreshControl = () => ( @@ -159,7 +168,6 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { list.push({ icon: 'trash-can-outline', onPress: () => { - // dispatch(deleteAllChaptersAction(pluginId, novel.id, selected)); setSelected([]); }, }); @@ -283,31 +291,6 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { navigation.navigate('Chapter', { novel, chapter }); }; - const showProgressPercentage = (chapter: ChapterInfo) => { - const savedProgress = - progress && progress[chapter.id] && progress[chapter.id].percentage; - if ( - savedProgress && - savedProgress < 97 && - savedProgress > 0 && - chapter.unread - ) { - return ( - - {chapter.releaseTime ? '• ' : null} - {getString('novelScreen.progress', { progress: savedProgress })} - - ); - } - }; - const setCustomNovelCover = async () => { showExtraMenu(false); const newCover = await pickCustomNovelCover(novel); @@ -320,379 +303,395 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { }; return ( - - - - {selected.length === 0 ? ( - - navigation.goBack()} - /> - - - Share.share({ - message: novel.url, - }) - } - /> + ( + + )} + > + + + + {selected.length === 0 ? ( + showJumpToChapterModal(true)} + size={24} + style={{ marginTop: (StatusBar.currentHeight || 0) + 8 }} + onPress={() => navigation.goBack()} /> - {!novel.isLocal && ( + + + Share.share({ + message: novel.pluginId + '|' + novel.path, + }) + } + /> + showJumpToChapterModal(true)} + /> + {!novel.isLocal && ( + showDownloadMenu(false)} + anchor={ + showDownloadMenu(true)} + /> + } + contentStyle={{ backgroundColor: theme.surface2 }} + > + { + showDownloadMenu(false); + const finded = chapters.find( + chapter => chapter.unread && !chapter.isDownloaded, + ); + if (novel && finded) { + downloadChapter(novel, finded); + } + }} + /> + { + showDownloadMenu(false); + if (novel) { + downloadChapters( + novel, + chapters + .filter( + chapter => + chapter.unread && !chapter.isDownloaded, + ) + .slice(0, 5), + ); + } + }} + /> + { + showDownloadMenu(false); + if (novel) { + downloadChapters( + novel, + chapters + .filter( + chapter => + chapter.unread && !chapter.isDownloaded, + ) + .slice(0, 10), + ); + } + }} + /> + { + downloadCustomChapterModal.setTrue(); + showDownloadMenu(false); + }} + /> + { + showDownloadMenu(false); + if (novel) { + downloadChapters( + novel, + chapters.filter(chapter => chapter.unread), + ); + } + }} + /> + { + if (novel) { + downloadChapters(novel, chapters); + } + showDownloadMenu(false); + }} + /> + { + showDownloadMenu(false); + deleteChapters(chapters.filter(c => c.isDownloaded)); + }} + /> + + )} + showDownloadMenu(false)} + visible={extraMenu} + onDismiss={() => showExtraMenu(false)} anchor={ showDownloadMenu(true)} + onPress={() => showExtraMenu(true)} /> } - contentStyle={{ backgroundColor: theme.surface2 }} + contentStyle={{ + backgroundColor: theme.surface2, + }} > { - showDownloadMenu(false); - const finded = chapters.find( - chapter => chapter.unread && !chapter.isDownloaded, - ); - if (novel && finded) { - downloadChapter(novel, finded); - } - }} - /> - { - showDownloadMenu(false); - if (novel) { - downloadChapters( - novel, - chapters - .filter( - chapter => - chapter.unread && !chapter.isDownloaded, - ) - .slice(0, 5), - ); - } + showEditInfoModal(true); + showExtraMenu(false); }} /> { - showDownloadMenu(false); - if (novel) { - downloadChapters( - novel, - chapters - .filter( - chapter => - chapter.unread && !chapter.isDownloaded, - ) - .slice(0, 10), - ); - } - }} - /> - { - downloadCustomChapterModal.setTrue(); - showDownloadMenu(false); - }} - /> - { - showDownloadMenu(false); - if (novel) { - downloadChapters( - novel, - chapters.filter(chapter => chapter.unread), - ); - } - }} - /> - { - if (novel) { - downloadChapters(novel, chapters); - } - showDownloadMenu(false); - }} - /> - { - showDownloadMenu(false); - deleteChapters(chapters.filter(c => c.isDownloaded)); - }} + onPress={setCustomNovelCover} /> - )} - - showExtraMenu(false)} - anchor={ - showExtraMenu(true)} - /> - } - contentStyle={{ - backgroundColor: theme.surface2, - }} - > - { - showEditInfoModal(true); - showExtraMenu(false); - }} - /> - - - - - ) : ( - - setSelected([])} - /> - - { - setSelected(chapters); + + + ) : ( + - - )} - - - ( - c.chapter.id === item.id, - )} - isLocal={novel.isLocal} - theme={theme} - chapter={item} - showChapterTitles={showChapterTitles} - deleteChapter={() => deleteChapter(item)} - downloadChapter={() => downloadChapter(novel, item)} - isSelected={isSelected} - onSelectPress={onSelectPress} - onSelectLongPress={onSelectLongPress} - navigateToChapter={navigateToChapter} - showProgressPercentage={showProgressPercentage} - novelName={name} - /> + > + setSelected([])} + /> + + { + setSelected(chapters); + }} + /> + )} - keyExtractor={(item, index) => 'chapter' + item.id + index} - contentContainerStyle={{ paddingBottom: 100 }} - ListHeaderComponent={ - - } - refreshControl={refreshControl()} - onScroll={onPageScroll} - /> - - {useFabForContinueReading && lastRead && ( - { - if (lastRead) { - navigation.navigate('Chapter', { - novel: novel, - chapter: lastRead, - }); + + + ( + c.chapter.id === item.id, + )} + isLocal={novel.isLocal} + theme={theme} + chapter={item} + showChapterTitles={showChapterTitles} + deleteChapter={() => deleteChapter(item)} + downloadChapter={() => downloadChapter(novel, item)} + isSelected={isSelected} + onSelectPress={onSelectPress} + onSelectLongPress={onSelectLongPress} + navigateToChapter={navigateToChapter} + novelName={name} + /> + )} + keyExtractor={item => 'chapter_' + item.id} + contentContainerStyle={{ paddingBottom: 100 }} + ListHeaderComponent={ + 1 ? pages[pageIndex] : undefined} + drawerRef={drawerRef} + /> } - }} - /> - )} - - 0} actions={actions} /> - { - deleteChapters(chapters.filter(c => c.isDownloaded)); - }, - }} - theme={{ colors: { primary: theme.primary } }} - style={{ backgroundColor: theme.surface, marginBottom: 32 }} - > - - {getString('novelScreen.deleteMessage')} - - - - - showJumpToChapterModal(false)} - chapters={chapters} - novel={novel} - chapterListRef={flatlistRef.current} - navigation={navigation} - /> - showEditInfoModal(false)} - novel={novel} - setNovel={setNovel} + refreshControl={refreshControl()} + onScroll={onPageScroll} + /> + + {useFabForContinueReading && lastRead && ( + { + if (lastRead) { + navigation.navigate('Chapter', { + novel: novel, + chapter: lastRead, + }); + } + }} + /> + )} + + 0} actions={actions} /> + { + deleteChapters(chapters.filter(c => c.isDownloaded)); + }, + }} + theme={{ colors: { primary: theme.primary } }} + style={{ backgroundColor: theme.surface, marginBottom: 32 }} + > + + {getString('novelScreen.deleteMessage')} + + + + + showJumpToChapterModal(false)} + chapters={chapters} + novel={novel} + chapterListRef={flatlistRef.current} + navigation={navigation} + /> + showEditInfoModal(false)} + novel={novel} + setNovel={setNovel} + theme={theme} + /> + + + - - - - - - + + + ); }; diff --git a/src/screens/novel/components/ChapterItem.tsx b/src/screens/novel/components/ChapterItem.tsx index aee2d0139..f5499f0a9 100644 --- a/src/screens/novel/components/ChapterItem.tsx +++ b/src/screens/novel/components/ChapterItem.tsx @@ -27,7 +27,6 @@ interface ChapterItemProps { onSelectPress?: (chapter: ChapterInfo) => void; onSelectLongPress?: (chapter: ChapterInfo) => void; navigateToChapter: (chapter: ChapterInfo) => void; - showProgressPercentage?: (chapter: ChapterInfo) => any; left?: ReactNode; isLocal: boolean; isUpdateCard?: boolean; @@ -45,13 +44,13 @@ const ChapterItem: React.FC = ({ onSelectPress, onSelectLongPress, navigateToChapter, - showProgressPercentage, isLocal, left, isUpdateCard, novelName, }) => { - const { id, name, unread, releaseTime, bookmark, chapterNumber } = chapter; + const { id, name, unread, releaseTime, bookmark, chapterNumber, progress } = + chapter; const [deleteChapterMenuVisible, setDeleteChapterMenuVisible] = useState(false); const showDeleteChapterMenu = () => setDeleteChapterMenuVisible(true); @@ -138,7 +137,19 @@ const ChapterItem: React.FC = ({ {parsedTime.isValid() ? parsedTime.format('LL') : releaseTime} ) : null} - {showProgressPercentage?.(chapter)} + {!isUpdateCard && progress && progress > 0 && chapter.unread ? ( + + {chapter.releaseTime ? '• ' : null} + {getString('novelScreen.progress', { progress })} + + ) : null} diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index b9835e461..2ddd067ea 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -1,5 +1,11 @@ -import React, { memo, useCallback } from 'react'; -import { View, Text, StyleSheet, Pressable } from 'react-native'; +import React, { RefObject, memo, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + Pressable, + DrawerLayoutAndroid, +} from 'react-native'; import color from 'color'; import * as Clipboard from 'expo-clipboard'; @@ -45,6 +51,8 @@ interface NovelInfoHeaderProps { followNovel: () => void; novelBottomSheetRef: React.RefObject; deleteDownloadsSnackbar: UseBooleanReturnType; + page?: string; + drawerRef: RefObject; } const NovelInfoHeader = ({ @@ -60,6 +68,8 @@ const NovelInfoHeader = ({ followNovel, novelBottomSheetRef, deleteDownloadsSnackbar, + page, + drawerRef, }: NovelInfoHeaderProps) => { const { hideBackdrop = false } = useAppSettings(); @@ -176,18 +186,33 @@ const NovelInfoHeader = ({ /> novelBottomSheetRef.current?.present()} + onPress={() => + page + ? drawerRef.current?.openDrawer() + : novelBottomSheetRef.current?.present() + } android_ripple={{ color: color(theme.primary).alpha(0.12).string(), }} > - - {`${chapters?.length} ${getString('novelScreen.chapters')}`} - + + {page ? ( + + Page: {page} + + ) : null} + + {`${chapters?.length} ${getString('novelScreen.chapters')}`} + + novelBottomSheetRef.current?.present()} /> @@ -205,15 +230,19 @@ const styles = StyleSheet.create({ paddingLeft: 12, justifyContent: 'center', }, - chapters: { + pageTitle: { paddingHorizontal: 16, - paddingVertical: 4, fontSize: 16, }, + chapters: { + paddingHorizontal: 16, + fontSize: 14, + }, bottomsheet: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + paddingVertical: 4, paddingRight: 12, }, infoItem: { diff --git a/src/screens/novel/components/NovelBottomSheet.tsx b/src/screens/novel/components/NovelBottomSheet.tsx index b5c55d091..0c184d50f 100644 --- a/src/screens/novel/components/NovelBottomSheet.tsx +++ b/src/screens/novel/components/NovelBottomSheet.tsx @@ -88,16 +88,16 @@ const ChaptersSettingsSheet = ({ - sort === 'ORDER BY id ASC' - ? sortChapters('ORDER BY id DESC') - : sortChapters('ORDER BY id ASC') + sort === 'ORDER BY position ASC' + ? sortChapters('ORDER BY position DESC') + : sortChapters('ORDER BY position ASC') } theme={theme} /> diff --git a/src/screens/novel/components/NovelDrawer.tsx b/src/screens/novel/components/NovelDrawer.tsx new file mode 100644 index 000000000..e0ea22901 --- /dev/null +++ b/src/screens/novel/components/NovelDrawer.tsx @@ -0,0 +1,99 @@ +import { FlashList, ListRenderItem } from '@shopify/flash-list'; +import { ThemeColors } from '@theme/types'; +import color from 'color'; +import { RefObject } from 'react'; +import { DrawerLayoutAndroid, Pressable, StyleSheet, View } from 'react-native'; +import { Text } from 'react-native-paper'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +interface NovelDrawerProps { + theme: ThemeColors; + pages: string[]; + pageIndex: number; + openPage: (index: number) => void; + drawerRef: RefObject; +} +export default function NovelDrawer({ + theme, + pages, + pageIndex, + openPage, + drawerRef, +}: NovelDrawerProps) { + const insets = useSafeAreaInsets(); + const renderItem: ListRenderItem = ({ item, index }) => ( + + { + openPage(index); + drawerRef.current?.closeDrawer(); + }} + > + + {item} + + + + ); + return ( + + + Novel pages + + + + ); +} + +const styles = StyleSheet.create({ + drawer: { + flex: 1, + paddingTop: 60, + height: 100, + }, + headerCtn: { + fontSize: 16, + padding: 16, + marginBottom: 4, + fontWeight: 'bold', + borderBottomWidth: 1, + }, + drawerElementContainer: { + margin: 4, + marginLeft: 16, + marginRight: 16, + borderRadius: 50, + overflow: 'hidden', + minHeight: 48, + }, + pageCtn: { + flex: 1, + paddingHorizontal: 20, + paddingVertical: 10, + justifyContent: 'center', + }, +}); diff --git a/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx b/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx index 44cc69eb0..7966978c4 100644 --- a/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx +++ b/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx @@ -7,7 +7,6 @@ import { useNavigation } from '@react-navigation/native'; import { useBoolean } from '@hooks'; import { ThemeColors } from '@theme/types'; import { getString } from '@strings/translations'; -import { Portal } from 'react-native-paper'; import SetCategoryModal from '../SetCategoriesModal'; import { NovelScreenProps } from '@navigators/types'; import { useTrackedNovel, useTracker } from '@hooks/persisted'; @@ -29,15 +28,15 @@ const NovelScreenButtonGroup: React.FC = ({ const { navigate } = useNavigation(); const followButtonColor = inLibrary ? theme.primary : theme.outline; const { tracker } = useTracker(); - const { trackedNovel } = useTrackedNovel(novel.url); + const { trackedNovel } = useTrackedNovel(novel.id); const trackerButtonColor = trackedNovel ? theme.primary : theme.outline; const handleOpenWebView = async () => { navigate('WebviewScreen', { - pluginId: novel.pluginId, name: novel.pluginId, - url: novel.url, + url: novel.path, + pluginId: novel.pluginId, }); }; const handleMigrateNovel = () => @@ -132,13 +131,11 @@ const NovelScreenButtonGroup: React.FC = ({ ) : null} - - - + ); }; diff --git a/src/screens/novel/components/NovelSummary/NovelSummary.tsx b/src/screens/novel/components/NovelSummary/NovelSummary.tsx index e37a9406c..cac708e1a 100644 --- a/src/screens/novel/components/NovelSummary/NovelSummary.tsx +++ b/src/screens/novel/components/NovelSummary/NovelSummary.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; -import { ThemeColors } from '../../../../theme/types'; +import { ThemeColors } from '@theme/types'; interface NovelSummaryProps { summary: string; @@ -17,7 +17,6 @@ const NovelSummary: React.FC = ({ theme, }) => { const textColor = theme.onSurfaceVariant; - const iconBackground = `${theme.background}D1`; const [expanded, setExpanded] = useState(isExpanded); const toggleExpanded = () => { @@ -48,8 +47,9 @@ const NovelSummary: React.FC = ({ style={[ styles.iconContainer, { - backgroundColor: iconBackground, + backgroundColor: theme.background, bottom, + opacity: expanded ? 1 : 0.7, }, ]} > diff --git a/src/screens/novel/components/Tracker/TrackSheet.tsx b/src/screens/novel/components/Tracker/TrackSheet.tsx index 3bfe3be04..773cc1d58 100644 --- a/src/screens/novel/components/Tracker/TrackSheet.tsx +++ b/src/screens/novel/components/Tracker/TrackSheet.tsx @@ -12,18 +12,18 @@ import { ThemeColors } from '@theme/types'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { useTracker, useTrackedNovel } from '@hooks/persisted'; import { UserListStatus } from '@services/Trackers'; +import { NovelInfo } from '@database/types'; interface Props { bottomSheetRef: React.RefObject; - novelUrl: string; - novelName: string; + novel: NovelInfo; theme: ThemeColors; } -const TrackSheet = ({ bottomSheetRef, novelUrl, novelName, theme }: Props) => { +const TrackSheet = ({ bottomSheetRef, novel, theme }: Props) => { const { tracker } = useTracker(); const { trackedNovel, trackNovel, untrackNovel, updateTrackedNovel } = - useTrackedNovel(novelUrl); + useTrackedNovel(novel.id); const [trackSearchDialog, setTrackSearchDialog] = useState(false); const [trackStatusDialog, setTrackStatusDialog] = useState(false); @@ -161,7 +161,7 @@ const TrackSheet = ({ bottomSheetRef, novelUrl, novelName, theme }: Props) => { trackNovel={trackNovel} trackSearchDialog={trackSearchDialog} setTrackSearchDialog={setTrackSearchDialog} - novelName={novelName} + novelName={novel.name} theme={theme} /> )} diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx index b145d2310..4704b3fb0 100644 --- a/src/screens/reader/ReaderScreen.tsx +++ b/src/screens/reader/ReaderScreen.tsx @@ -19,6 +19,8 @@ import { useKeepAwake } from 'expo-keep-awake'; import { getNextChapter, getPrevChapter, + markChapterRead, + updateChapterProgress, } from '@database/queries/ChapterQueries'; import { fetchChapter } from '@services/plugin/fetch'; import { showToast } from '@utils/showToast'; @@ -53,19 +55,29 @@ import { getString } from '@strings/translations'; const Chapter = ({ route, navigation }: ChapterScreenProps) => { const drawerRef = useRef(null); + const { chapters, novelSettings, pages, setLastRead, setPageIndex } = + useNovel(route.params.novel.path, route.params.novel.pluginId); return ( ( - + )} > ); @@ -73,22 +85,17 @@ const Chapter = ({ route, navigation }: ChapterScreenProps) => { type ChapterContentProps = ChapterScreenProps & { drawerRef: React.RefObject; + setLastRead: (chapter: ChapterInfo | undefined) => void; }; export const ChapterContent = ({ route, navigation, drawerRef, + setLastRead, }: ChapterContentProps) => { useKeepAwake(); const { novel, chapter } = route.params; - const { - markChapterRead, - setLastRead, - bookmarkChapters, - progress, - setProgress, - } = useNovel(novel.url, novel.pluginId); const webViewRef = useRef(null); const readerSheetRef = useRef(null); @@ -110,11 +117,11 @@ export const ChapterContent = ({ const [hidden, setHidden] = useState(true); const { tracker } = useTracker(); - const { trackedNovel, updateNovelProgess } = useTrackedNovel(novel.url); + const { trackedNovel, updateNovelProgess } = useTrackedNovel(novel.id); const [sourceChapter, setChapter] = useState({ ...chapter, chapterText: '' }); const [loading, setLoading] = useState(true); - const [error, setError] = useState(); + const [error, setError] = useState(); const [[nextChapter, prevChapter], setAdjacentChapter] = useState< ChapterInfo[] >([]); @@ -165,7 +172,7 @@ export const ChapterContent = ({ if (await RNFS.exists(filePath)) { sourceChapter.chapterText = await RNFS.readFile(filePath); } else { - await fetchChapter(novel.pluginId, chapter.url) + await fetchChapter(novel.pluginId, chapter.path) .then(res => { sourceChapter.chapterText = res; }) @@ -227,9 +234,9 @@ export const ChapterContent = ({ }; const saveProgress = useCallback( - async (offsetY: number, percentage: number) => { + (percentage: number) => { if (!incognitoMode) { - setProgress(chapter.id, offsetY, percentage); + updateChapterProgress(chapter.id, percentage > 100 ? 100 : percentage); } if (!incognitoMode && percentage >= 97) { @@ -277,7 +284,7 @@ export const ChapterContent = ({ const onWebViewNavigationStateChange = async ({ url }: WebViewNavigation) => { if (url !== 'about:blank') { setLoading(true); - fetchChapter(novel.pluginId, chapter.url) + fetchChapter(novel.pluginId, chapter.path) .then(res => { sourceChapter.chapterText = res; setChapter(sourceChapter); @@ -312,7 +319,32 @@ export const ChapterContent = ({ } if (error) { - return ; + return ( + { + setError(''); + setLoading(true); + getChapter(); + }, + }, + { + iconName: 'earth', + title: 'WebView', + onPress: () => + navigation.navigate('WebviewScreen', { + name: `${chapter.name} | ${novel.name}`, + url: chapter.path, + pluginId: novel.pluginId, + }), + }, + ]} + /> + ); } return ( @@ -326,9 +358,6 @@ export const ChapterContent = ({ saveProgress={saveProgress} onLayout={() => { useVolumeButtons && onLayout(); - if (progress[chapter.id]) { - scrollTo(progress[chapter.id]?.offsetY || 0); - } }} onPress={hideHeader} navigateToChapterBySwipe={navigateToChapterBySwipe} @@ -341,7 +370,6 @@ export const ChapterContent = ({ { +type ChapterDrawerProps = ChapterScreenProps & { + chapters: ChapterInfo[]; + novelSettings: NovelSettings; + pages: string[]; + setPageIndex: (value: number) => void; +}; + +const ChapterDrawer = ({ + route, + navigation, + chapters, + novelSettings, + pages, + setPageIndex, +}: ChapterDrawerProps) => { const theme = useTheme(); const insets = useSafeAreaInsets(); const styles = createStylesheet(theme, insets); const listRef = useRef>(null); const { chapter, novel: novelItem } = route.params; const { defaultChapterSort } = useAppSettings(); - const { chapters, novelSettings } = useNovel( - novelItem.url, - novelItem.pluginId, - ); const { sort = defaultChapterSort } = novelSettings; - const listAscending = sort === 'ORDER BY id ASC'; + const listAscending = sort === 'ORDER BY position ASC'; const scrollToIndex = useMemo(() => { if (chapters.length < 1) { return 0; @@ -159,7 +170,13 @@ const ChapterDrawer = ({ route, navigation }: ChapterScreenProps) => { }); } }; - + useEffect(() => { + let pageIndex = pages.indexOf(chapter.page); + if (pageIndex === -1) { + pageIndex = 0; + } + setPageIndex(pageIndex); + }, [chapter]); return ( {getString('common.chapters')} diff --git a/src/screens/reader/components/ReaderAppbar.tsx b/src/screens/reader/components/ReaderAppbar.tsx index 8c34be651..6a63401df 100644 --- a/src/screens/reader/components/ReaderAppbar.tsx +++ b/src/screens/reader/components/ReaderAppbar.tsx @@ -8,6 +8,7 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { ThemeColors } from '@theme/types'; import { TextToSpeechStatus } from '@hooks'; import { ChapterInfo } from '@database/types'; +import { bookmarkChapter } from '@database/queries/ChapterQueries'; interface ReaderAppbarProps { novelName: string; @@ -15,7 +16,6 @@ interface ReaderAppbarProps { tts: () => void; textToSpeech: TextToSpeechStatus; theme: ThemeColors; - bookmarkChapters: (chapters: ChapterInfo[]) => void; goBack: () => void; } @@ -23,7 +23,6 @@ const ReaderAppbar = ({ novelName, chapter, tts, - bookmarkChapters, goBack, textToSpeech, theme, @@ -83,8 +82,9 @@ const ReaderAppbar = ({ name={bookmarked ? 'bookmark' : 'bookmark-outline'} size={24} onPress={() => { - bookmarkChapters([chapter]); - setBookmarked(!bookmarked); + bookmarkChapter(chapter.id).then(() => + setBookmarked(!bookmarked), + ); }} color={theme.onSurface} theme={theme} diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index d4e414816..a64cfba04 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -29,7 +29,7 @@ type WebViewReaderProps = { swipeGestures: boolean; nextChapter: ChapterInfo; webViewRef: React.RefObject; - saveProgress(offsetY: number, percentage: number): Promise; + saveProgress(percentage: number): void; onPress(): void; onLayout(): void; navigateToChapterBySwipe(name: string): void; @@ -90,14 +90,8 @@ const WebViewReader: FC = props => { } break; case 'save': - if ( - event.data && - event.data.offsetY && - event.data.percentage && - typeof event.data.offsetY === 'number' && - typeof event.data.percentage === 'number' - ) { - saveProgress(event.data.offsetY, event.data.percentage); + if (event.data && typeof event.data === 'number') { + saveProgress(event.data); } break; } @@ -143,11 +137,6 @@ const WebViewReader: FC = props => { var swipeGestures = ${swipeGestures}; var autoSaveInterval = 2222; - -
@@ -178,7 +167,21 @@ const WebViewReader: FC = props => {
` } - + + `, }} diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx index aca4d3f51..673b2b0f6 100644 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ b/src/screens/settings/SettingsAdvancedScreen.tsx @@ -22,10 +22,7 @@ import { } from '@database/queries/ChapterQueries'; import { Appbar, Button, List } from '@components'; -import { importEpub } from '@services/epub/import'; import { AdvancedSettingsScreenProps } from '@navigators/types'; -import { useMMKVString } from 'react-native-mmkv'; -import { BACKGROUND_ACTION } from '@services/constants'; import { StyleSheet, View } from 'react-native'; import { getUserAgentSync } from 'react-native-device-info'; @@ -56,8 +53,6 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => { setFalse: hideUserAgentModal, } = useBoolean(); - const [hasAction] = useMMKVString(BACKGROUND_ACTION); - return ( <> { onPress={showDeleteReadChaptersDialog} theme={theme} /> - = ({ navigation }) => { - defaultChapterSort === 'ORDER BY id ASC' - ? setAppSettings({ defaultChapterSort: 'ORDER BY id DESC' }) - : setAppSettings({ defaultChapterSort: 'ORDER BY id ASC' }) + defaultChapterSort === 'ORDER BY position ASC' + ? setAppSettings({ + defaultChapterSort: 'ORDER BY position DESC', + }) + : setAppSettings({ defaultChapterSort: 'ORDER BY position ASC' }) } /> diff --git a/src/screens/updates/components/UpdateNovelCard.tsx b/src/screens/updates/components/UpdateNovelCard.tsx index 27f9dbcb0..df10b69e1 100644 --- a/src/screens/updates/components/UpdateNovelCard.tsx +++ b/src/screens/updates/components/UpdateNovelCard.tsx @@ -62,12 +62,12 @@ const UpdateNovelCard: React.FC = ({ ); const navigateToChapter = useCallback((chapter: ChapterInfo) => { - const { novelUrl, pluginId, novelName } = chapter as + const { novelPath, pluginId, novelName } = chapter as | Update | DownloadedChapter; navigate('Chapter', { novel: { - url: novelUrl, + path: novelPath, pluginId: pluginId, name: novelName, } as NovelInfo, @@ -79,7 +79,7 @@ const UpdateNovelCard: React.FC = ({ if (chapterList.length) { navigate('Novel', { pluginId: chapterList[0].pluginId, - url: chapterList[0].novelUrl, + path: chapterList[0].novelPath, name: chapterList[0].novelName, }); } diff --git a/src/services/backup/utils.ts b/src/services/backup/utils.ts index fab996dbf..7e08e79ce 100644 --- a/src/services/backup/utils.ts +++ b/src/services/backup/utils.ts @@ -9,7 +9,7 @@ import { _restoreNovelAndChapters, getAllNovels, } from '@database/queries/NovelQueries'; -import { getChapters } from '@database/queries/ChapterQueries'; +import { getNovelChapters } from '@database/queries/ChapterQueries'; import { _restoreCategory, getAllNovelCategories, @@ -67,7 +67,7 @@ export const prepareBackupData = async (cacheDirPath: string) => { // novels await getAllNovels().then(async novels => { for (const novel of novels) { - const chapters = await getChapters(novel.id); + const chapters = await getNovelChapters(novel.id); await RNFS.writeFile( novelDirPath + '/' + novel.id + '.json', JSON.stringify({ diff --git a/src/services/epub/import.ts b/src/services/epub/import.ts index fa77bc5bb..4fc8a2c12 100644 --- a/src/services/epub/import.ts +++ b/src/services/epub/import.ts @@ -26,15 +26,15 @@ interface TaskData { const insertLocalNovel = ( name: string, - url: string, + path: string, cover?: string, author?: string, ): Promise => { return new Promise((resolve, reject) => { db.transaction(tx => { tx.executeSql( - "INSERT INTO Novel(name, url, cover, author, pluginId, inLibrary, isLocal) VALUES(?, ?, ?, ?, 'local', 1, 1)", - [name, url, cover || null, author || null], + "INSERT INTO Novel(name, path, cover, author, pluginId, inLibrary, isLocal) VALUES(?, ?, ?, ?, 'local', 1, 1)", + [name, path, cover || null, author || null], async (txObj, resultSet) => { if (resultSet.insertId) { await updateNovelCategoryById(resultSet.insertId, [2]); @@ -50,11 +50,12 @@ const insertLocalNovel = ( pluginId: LOCAL_PLUGIN_ID, id: resultSet.insertId, author: author, - url: NovelDownloadFolder + '/local/' + resultSet.insertId, + path: NovelDownloadFolder + '/local/' + resultSet.insertId, cover: newCoverPath, name: name, inLibrary: true, isLocal: true, + totalPages: 0, }); resolve(resultSet.insertId); } else { @@ -76,26 +77,26 @@ const insertLocalChapter = ( novelId: number, fakeId: number, name: string, - url: string, + path: string, releaseTime: string, ): Promise => { return new Promise((resolve, reject) => { db.transaction(tx => { tx.executeSql( - 'INSERT INTO Chapter(novelId, name, url, releaseTime) VALUES(?, ?, ?, ?)', + 'INSERT INTO Chapter(novelId, name, path, releaseTime, position) VALUES(?, ?, ?, ?, ?)', [ novelId, name, - // use fakeid just for make the url is unique :D NovelDownloadFolder + '/local/' + novelId + '/' + fakeId, releaseTime, + fakeId, ], async (txObj, resultSet) => { if (resultSet.insertId) { - let chapterText = await RNFS.readFile(url); + let chapterText = await RNFS.readFile(path); const staticPaths: string[] = []; const novelDir = NovelDownloadFolder + '/local/' + novelId; - const epubContentDir = url.replace(/[^\\\/]+$/, ''); + const epubContentDir = path.replace(/[^\\\/]+$/, ''); chapterText = chapterText.replace( /(href|src)=(["'])(.*?)\2/g, ($0, $1, $2, $3: string) => { @@ -194,7 +195,7 @@ const parseNovelAndChapters = async ( const href = itemMap[ele.attribs.idref]; return { name: tocMap[href], - url: `${contentDir}/${href}`, + path: `${contentDir}/${href}`, }; }); BackgroundService.updateNotification({ @@ -208,7 +209,7 @@ const parseNovelAndChapters = async ( name: novelName, author: author, cover: cover, - url: contentDir + novelName, // temporary + path: contentDir + novelName, // temporary chapters: chapters, }; return novel; @@ -238,7 +239,7 @@ const importEpubAction = async (taskData?: TaskData) => { }); const novelId = await insertLocalNovel( novel.name, - novel.url, + novel.path, novel.cover, novel.author, ).catch(e => { @@ -265,13 +266,13 @@ const importEpubAction = async (taskData?: TaskData) => { }); const chapter = novel.chapters[i]; if (!chapter.name) { - chapter.name = chapter.url.split(/[\\\/]/).pop() || 'unknown'; + chapter.name = chapter.path.split(/[\\\/]/).pop() || 'unknown'; } const filePaths = await insertLocalChapter( novelId, i, chapter.name, - chapter.url, + chapter.path, now, ).catch(e => { throw e; @@ -330,6 +331,10 @@ const importEpubAction = async (taskData?: TaskData) => { export const importEpub = async () => { try { + const currentAction = MMKVStorage.getString(BACKGROUND_ACTION); + if (currentAction) { + throw new Error('Another serivce is running'); + } const epubFile = await DocumentPicker.getDocumentAsync({ type: 'application/epub+zip', copyToCacheDirectory: false, diff --git a/src/services/migrate/migrateNovel.ts b/src/services/migrate/migrateNovel.ts index f8cf050bf..d32a684ac 100644 --- a/src/services/migrate/migrateNovel.ts +++ b/src/services/migrate/migrateNovel.ts @@ -7,7 +7,7 @@ import { getNovel, insertNovelAndChapters, } from '@database/queries/NovelQueries'; -import { getChapters } from '@database/queries/ChapterQueries'; +import { getNovelChapters } from '@database/queries/ChapterQueries'; import { downloadChapter } from '@database/queries/ChapterQueries'; import { fetchNovel } from '@services/plugin/fetch'; @@ -20,8 +20,6 @@ import { MMKVStorage, getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv'; import { LAST_READ_PREFIX, NOVEL_SETTINSG_PREFIX, - NovelProgress, - PROGRESS_PREFIX, } from '@hooks/persisted/useNovel'; import { BACKGROUND_ACTION, BackgoundAction } from '@services/constants'; import { getString } from '@strings/translations'; @@ -31,7 +29,7 @@ const db = SQLite.openDatabase('lnreader.db'); const migrateNovelMetaDataQuery = 'UPDATE Novel SET cover = ?, summary = ?, author = ?, artist = ?, status = ?, genres = ?, inLibrary = 1 WHERE id = ?'; const migrateChapterQuery = - 'UPDATE Chapter SET bookmark = ?, unread = ?, readTime = ? WHERE id = ?'; + 'UPDATE Chapter SET bookmark = ?, unread = ?, readTime = ?, progress = ? WHERE id = ?'; const sleep = (time: number): any => new Promise(resolve => setTimeout(() => resolve(null), time)); @@ -56,7 +54,7 @@ const sortChaptersByNumber = (novelName: string, chapters: ChapterInfo[]) => { export const migrateNovel = async ( pluginId: string, fromNovel: NovelInfo, - toNovelUrl: string, + toNovelPath: string, ) => { const currentAction = MMKVStorage.getString(BACKGROUND_ACTION); if (currentAction) { @@ -64,21 +62,21 @@ export const migrateNovel = async ( return; } try { - let fromChapters = await getChapters(fromNovel.id, '', ''); - let toNovel = await getNovel(toNovelUrl); + let fromChapters = await getNovelChapters(fromNovel.id); + let toNovel = await getNovel(toNovelPath, pluginId); let toChapters: ChapterInfo[]; if (toNovel) { - toChapters = await getChapters(toNovel.id, '', ''); + toChapters = await getNovelChapters(toNovel.id); } else { - const fetchedNovel = await fetchNovel(pluginId, toNovelUrl).catch(e => { + const fetchedNovel = await fetchNovel(pluginId, toNovelPath).catch(e => { throw e; }); await insertNovelAndChapters(pluginId, fetchedNovel); - toNovel = await getNovel(toNovelUrl); + toNovel = await getNovel(toNovelPath, pluginId); if (!toNovel) { return; } - toChapters = await getChapters(toNovel.id, '', ''); + toChapters = await getNovelChapters(toNovel.id); } const options = { @@ -132,25 +130,23 @@ export const migrateNovel = async ( txnErrorCallback, ); }); - //settings + setMMKVObject( - NOVEL_SETTINSG_PREFIX + '_' + toNovel.url, - getMMKVObject(NOVEL_SETTINSG_PREFIX + '_' + fromNovel.url), + `${NOVEL_SETTINSG_PREFIX}_${toNovel.pluginId}_${toNovel.path}`, + getMMKVObject( + `${NOVEL_SETTINSG_PREFIX}_${fromNovel.pluginId}_${fromNovel.path}`, + ), ); - const fromProgress = - getMMKVObject(PROGRESS_PREFIX + '_' + fromNovel.url) || - {}; - const toProgresss: NovelProgress = {}; - const setProgress = (progress: NovelProgress) => { - setMMKVObject(PROGRESS_PREFIX + '_' + toNovel.url, progress); - }; const lastRead = getMMKVObject( - LAST_READ_PREFIX + '_' + fromNovel.url, + `${LAST_READ_PREFIX}_${fromNovel.pluginId}_${fromNovel.path}`, ); const setLastRead = (chapter: ChapterInfo) => { - setMMKVObject(LAST_READ_PREFIX + '_' + toNovel.url, chapter); + setMMKVObject( + `${LAST_READ_PREFIX}_${toNovel.pluginId}_${toNovel.path}`, + chapter, + ); }; fromChapters = sortChaptersByNumber(fromNovel.name, fromChapters); @@ -179,15 +175,12 @@ export const migrateNovel = async ( continue; } - if (fromProgress && fromProgress[fromChapter.id]) { - toProgresss[toChapter.id] = fromProgress[fromChapter.id]; - } - db.transaction(tx => tx.executeSql(migrateChapterQuery, [ Number(fromChapter.bookmark), Number(fromChapter.unread), fromChapter.readTime, + fromChapter.progress, toChapter.id, ]), ); @@ -197,7 +190,7 @@ export const migrateNovel = async ( pluginId, toNovel.id, toChapter.id, - toChapter.url, + toChapter.path, ); await sleep(taskData.delay || 1000); } @@ -218,7 +211,6 @@ export const migrateNovel = async ( fromChapters.length === fromPointer || toChapters.length === toPointer ) { - setProgress(toProgresss); Notifications.scheduleNotificationAsync({ content: { title: getString('browseScreen.migration.novelMigrated'), diff --git a/src/services/plugin/fetch.ts b/src/services/plugin/fetch.ts index 76f300100..49f5ae293 100644 --- a/src/services/plugin/fetch.ts +++ b/src/services/plugin/fetch.ts @@ -1,39 +1,59 @@ import { getPlugin } from '@plugins/pluginManager'; -export const fetchNovel = async (pluginId: string, novelUrl: string) => { +export const fetchNovel = async (pluginId: string, novelPath: string) => { const plugin = getPlugin(pluginId); - const res = await plugin.parseNovelAndChapters(novelUrl).catch(e => { + if (!plugin) { + throw new Error(`Unknown plugin: ${pluginId}`); + } + const res = await plugin.parseNovel(novelPath).catch(e => { throw e; }); return res; }; export const fetchImage = async (pluginId: string, imageUrl: string) => { - return getPlugin(pluginId) - .fetchImage(imageUrl) - .catch(e => { - throw e; - }); + const plugin = getPlugin(pluginId); + if (!plugin) { + throw new Error(`Unknown plugin: ${pluginId}`); + } + return plugin.fetchImage(imageUrl).catch(e => { + throw e; + }); }; -export const fetchChapter = async (pluginId: string, chapterUrl: string) => { +export const fetchChapter = async (pluginId: string, chapterPath: string) => { const plugin = getPlugin(pluginId); - let chapterText = `Not found plugin with id: ${pluginId}`; + let chapterText = `Unkown plugin: ${pluginId}`; if (plugin) { - chapterText = await plugin.parseChapter(chapterUrl).catch(e => { + chapterText = await plugin.parseChapter(chapterPath).catch(e => { throw e; }); } return chapterText; }; -export const fetchChapters = async (pluginId: string, novelUrl: string) => { +export const fetchChapters = async (pluginId: string, novelPath: string) => { const plugin = getPlugin(pluginId); - const res = await plugin.parseNovelAndChapters(novelUrl).catch(e => { + if (!plugin) { + throw new Error(`Unknown plugin: ${pluginId}`); + } + const res = await plugin.parseNovel(novelPath).catch(e => { throw e; }); + return res?.chapters; +}; - const chapters = res.chapters; - - return chapters; +export const fetchPage = async ( + pluginId: string, + novelPath: string, + page: string, +) => { + const plugin = getPlugin(pluginId); + if (!plugin || !plugin.parsePage) { + throw new Error('Cant parse page!'); + } + const res = await plugin.parsePage(novelPath, page).catch(e => { + throw e; + }); + return res; }; diff --git a/src/services/updates/LibraryUpdateQueries.ts b/src/services/updates/LibraryUpdateQueries.ts index b87bb1be4..b4802c39d 100644 --- a/src/services/updates/LibraryUpdateQueries.ts +++ b/src/services/updates/LibraryUpdateQueries.ts @@ -6,6 +6,8 @@ import { SourceNovel } from '@plugins/types'; import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager'; import { NovelDownloadFolder } from '@utils/constants/download'; import * as RNFS from 'react-native-fs'; +import { getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv'; +import { NOVEL_PAGE_UPDATES_PREFIX } from '@hooks/persisted/useNovel'; const db = SQLite.openDatabase('lnreader.db'); const updateNovelMetadata = async ( @@ -13,7 +15,8 @@ const updateNovelMetadata = async ( novelId: number, novel: SourceNovel, ) => { - let { name, cover, summary, author, artist, genres, status } = novel; + let { name, cover, summary, author, artist, genres, status, totalPages } = + novel; const novelDir = NovelDownloadFolder + '/' + pluginId + '/' + novelId; if (cover) { const novelCoverUri = 'file://' + novelDir + '/cover.png'; @@ -27,15 +30,20 @@ const updateNovelMetadata = async ( } db.transaction(tx => { tx.executeSql( - 'UPDATE Novel SET name = ?, cover = ?, summary = ?, author = ?, artist = ?, genres = ?, status = ? WHERE id = ?', + `UPDATE Novel SET + name = ?, cover = ?, summary = ?, author = ?, artist = ?, + genres = ?, status = ?, totalPages = ? + WHERE id = ? + `, [ name, cover || null, - summary || '', + summary || null, author || 'unknown', - artist || '', - genres || '', - status || '', + artist || null, + genres || null, + status || null, + totalPages || 0, novelId, ], ); @@ -49,7 +57,7 @@ export interface UpdateNovelOptions { const updateNovel = async ( pluginId: string, - novelUrl: string, + novelPath: string, novelId: number, options: UpdateNovelOptions, ) => { @@ -57,35 +65,56 @@ const updateNovel = async ( return; } const { downloadNewChapters, refreshNovelMetadata } = options; - - let novel = await fetchNovel(pluginId, novelUrl); - + const novel = await fetchNovel(pluginId, novelPath); if (refreshNovelMetadata) { updateNovelMetadata(pluginId, novelId, novel); } db.transaction(tx => { - novel.chapters?.forEach(chapter => { - const { name, url, releaseTime } = chapter; + novel.chapters.forEach(chapter => { + const { name, path, releaseTime, page } = chapter; tx.executeSql( ` - INSERT INTO Chapter (url, name, releaseTime, novelId, updatedTime) - VALUES (?, ?, ?, ?, datetime('now','localtime')) - ON CONFLICT(url) DO UPDATE SET - name=excluded.name, - releaseTime=excluded.releaseTime, - updatedTime=excluded.updatedTime - WHERE Chapter.name != excluded.name - OR Chapter.releaseTime != excluded.releaseTime; + INSERT INTO Chapter (path, name, releaseTime, novelId, updatedTime, page) + SELECT ?, ?, ?, ?, datetime('now','localtime'), ? + WHERE NOT EXISTS (SELECT id FROM Chapter WHERE path = ? AND novelId = ?); `, - [url, name, releaseTime || '', novelId], + [path, name, releaseTime || null, novelId, page || '1', path, novelId], (txObj, { insertId }) => { - if (insertId && downloadNewChapters) { - downloadChapter(pluginId, novelId, insertId, url); + if (insertId) { + if (downloadNewChapters) { + downloadChapter(pluginId, novelId, insertId, path); + } + } else { + tx.executeSql( + ` + UPDATE Chapter SET + name = ?, releaseTime = ?, updatedTime = datetime('now','localtime'), page = ? + WHERE path = ? AND novelId = ? AND (name != ? OR releaseTime != ? OR page != ?); + `, + [ + name, + releaseTime || null, + page || '1', + path, + novelId, + name, + releaseTime || null, + page || '1', + ], + ); } }, ); }); }); + const key = `${NOVEL_PAGE_UPDATES_PREFIX}_${novelId}`; + const hasUpdates = getMMKVObject(key); + if (hasUpdates) { + setMMKVObject( + key, + hasUpdates.map(() => true), + ); + } }; export { updateNovel }; diff --git a/src/services/updates/index.ts b/src/services/updates/index.ts index 9a1206b11..e322acd3e 100644 --- a/src/services/updates/index.ts +++ b/src/services/updates/index.ts @@ -65,7 +65,7 @@ const updateLibrary = async (categoryId?: number) => { if (BackgroundService.isRunning()) { await updateNovel( libraryNovels[i].pluginId, - libraryNovels[i].url, + libraryNovels[i].path, libraryNovels[i].id, options, ); diff --git a/src/utils/handleNavigateParams.ts b/src/utils/handleNavigateParams.ts deleted file mode 100644 index b2fce51f0..000000000 --- a/src/utils/handleNavigateParams.ts +++ /dev/null @@ -1,38 +0,0 @@ -interface oCProps { - novel: { - url: string; - pluginId: string; - name: string; - }; - chapter: { - id: number; - url: string; - novelId: number; - name: string; - bookmark: number; - }; -} -export type openChapterChapterTypes = oCProps['chapter']; -export type openChapterNovelTypes = oCProps['novel']; - -export function openChapter( - novel: openChapterNovelTypes, - chapter: openChapterChapterTypes, -) { - return { - ...novel, - ...chapter, - }; -} -export interface openNovelProps { - pluginId: string; - id?: number; - url: string; - name: string; - cover?: string; -} -export function openNovel(novel: openNovelProps): openNovelProps { - return { - ...novel, - }; -}