diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts index 0b28035c..8f3b3961 100644 --- a/src/database/queries/NovelQueries.ts +++ b/src/database/queries/NovelQueries.ts @@ -4,7 +4,7 @@ const db = SQLite.openDatabase('lnreader.db'); import * as DocumentPicker from 'expo-document-picker'; import * as RNFS from 'react-native-fs'; -import { fetchChapters, fetchNovel } from '@services/plugin/fetch'; +import { fetchChapters, fetchImage, fetchNovel } from '@services/plugin/fetch'; import { insertChapters } from './ChapterQueries'; import { showToast } from '@utils/showToast'; @@ -15,13 +15,13 @@ import { BackupNovel, NovelInfo } from '../types'; import { SourceNovel } from '@plugins/types'; import { NovelDownloadFolder } from '@utils/constants/download'; -export const insertNovelAndChapters = ( +export const insertNovelAndChapters = async ( pluginId: string, sourceNovel: SourceNovel, -): Promise => { +): Promise => { const insertNovelQuery = 'INSERT INTO Novel (url, pluginId, name, cover, summary, author, artist, status, genres) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)'; - return new Promise(resolve => { + const novelId: number | undefined = await new Promise(resolve => { db.transaction(tx => { tx.executeSql( insertNovelQuery, @@ -36,17 +36,36 @@ export const insertNovelAndChapters = ( sourceNovel.status || null, sourceNovel.genres || null, ], - (txObj, resultSet) => { - if (resultSet.insertId) { - insertChapters(resultSet.insertId, sourceNovel.chapters).then(() => - resolve(resultSet.insertId || 1), - ); - } - }, + async (txObj, resultSet) => resolve(resultSet.insertId), txnErrorCallback, ); }); }); + if (novelId) { + const promises = [insertChapters(novelId, sourceNovel.chapters)]; + if (sourceNovel.cover) { + const novelDir = NovelDownloadFolder + '/' + pluginId + '/' + novelId; + const novelCoverUri = 'file://' + novelDir + '/cover.png'; + promises.push( + fetchImage(pluginId, sourceNovel.cover).then(base64 => { + if (base64) { + RNFS.mkdir(novelDir) + .then(() => RNFS.writeFile(novelCoverUri, base64, 'base64')) + .then(() => { + db.transaction(tx => { + tx.executeSql('UPDATE Novel SET cover = ? WHERE id = ?', [ + novelCoverUri, + novelId, + ]); + }); + }); + } + }), + ); + } + await Promise.all(promises); + } + return novelId; }; export const getAllNovels = async (): Promise => { @@ -59,18 +78,6 @@ export const getAllNovels = async (): Promise => { ); }; -export const getNovelsWithCustomCover = async (): Promise => { - return new Promise(resolve => { - db.transaction(tx => { - tx.executeSql( - 'SELECT id, pluginId, cover FROM Novel WHERE cover NOT LIKE "http%"', - [], - (txObj, { rows }) => resolve(rows._array), - ); - }); - }); -}; - export const getNovel = async (novelUrl: string): Promise => { return new Promise(resolve => db.transaction(tx => { @@ -122,20 +129,22 @@ export const switchNovelToLibrary = async ( } else { const sourceNovel = await fetchNovel(pluginId, novelUrl); const novelId = await insertNovelAndChapters(pluginId, sourceNovel); - db.transaction(tx => { - tx.executeSql( - 'UPDATE Novel SET inLibrary = 1 WHERE url = ?', - [novelUrl], - () => showToast(getString('browseScreen.addedToLibrary')), - txnErrorCallback, - ); - tx.executeSql( - 'INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, (SELECT DISTINCT id FROM Category WHERE sort = 1))', - [novelId], - noop, - txnErrorCallback, - ); - }); + if (novelId) { + db.transaction(tx => { + tx.executeSql( + 'UPDATE Novel SET inLibrary = 1 WHERE url = ?', + [novelUrl], + () => showToast(getString('browseScreen.addedToLibrary')), + txnErrorCallback, + ); + tx.executeSql( + 'INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, (SELECT DISTINCT id FROM Category WHERE sort = 1))', + [novelId], + noop, + txnErrorCallback, + ); + }); + } } }; @@ -248,21 +257,23 @@ export const updateNovelInfo = async (info: NovelInfo) => { }); }; -export const pickCustomNovelCover = async (novelId: number) => { +export const pickCustomNovelCover = async (novel: NovelInfo) => { const image = await DocumentPicker.getDocumentAsync({ type: 'image/*' }); - if (image.type === 'success' && image.uri) { - const uri = 'file://' + image.uri; - + const novelDir = + NovelDownloadFolder + '/' + novel.pluginId + '/' + novel.id; + let novelCoverUri = 'file://' + novelDir + '/cover.png'; + RNFS.copyFile(image.uri, novelCoverUri); + novelCoverUri += '?' + Date.now(); db.transaction(tx => { tx.executeSql( 'UPDATE Novel SET cover = ? WHERE id = ?', - [uri, novelId], + [novelCoverUri, novel.id], noop, txnErrorCallback, ); }); - return image.uri; + return novelCoverUri; } }; diff --git a/src/hooks/persisted/usePlugins.ts b/src/hooks/persisted/usePlugins.ts index ba995b69..6e3a24b0 100644 --- a/src/hooks/persisted/usePlugins.ts +++ b/src/hooks/persisted/usePlugins.ts @@ -118,6 +118,8 @@ export default function usePlugins() { } setMMKVObject(AVAILABLE_PLUGINS, availablePlugins); filterPlugins(languagesFilter); + } else { + throw new Error(`Failed to installed ${plugin.name}`); } }); }; diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 8cb6d6aa..17aeaf59 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -97,8 +97,7 @@ const installPlugin = async (url: string): Promise => { } }); } catch (e: any) { - showToast(e.message); - return undefined; + throw e; } }; @@ -135,7 +134,7 @@ const fetchPlugins = async () => { const githubBranch = 'plugins'; const availablePlugins: Record> = await fetch( - `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/${githubBranch}/.dist/${githubUsername}/plugins.json`, + `https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/${githubBranch}/.dist/${githubUsername}/plugins.min.json`, ) .then(res => res.json()) .catch(() => { diff --git a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts index 71bded1b..ce0c6376 100644 --- a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts +++ b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts @@ -38,7 +38,7 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { filteredInstalledPlugins.forEach(async plugin => { if (isMounted.current) { try { - const res = await getPlugin(plugin.id).searchNovels(searchText); + const res = await getPlugin(plugin.id).searchNovels(searchText, 1); setSearchResults(prevState => prevState.map(prevResult => diff --git a/src/screens/browse/components/PluginCard.tsx b/src/screens/browse/components/PluginCard.tsx index 20392dce..874304f3 100644 --- a/src/screens/browse/components/PluginCard.tsx +++ b/src/screens/browse/components/PluginCard.tsx @@ -98,6 +98,7 @@ const PluginCard: React.FC = ({ } else { installPlugin(plugin) .then(() => showToast(`Installed ${plugin.name}`)) + .catch((error: Error) => showToast(error.message)) .finally(() => setIsLoading(false)); } }} diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx index e6b00a93..329fe7e5 100644 --- a/src/screens/novel/NovelScreen.tsx +++ b/src/screens/novel/NovelScreen.tsx @@ -309,7 +309,7 @@ const Novel = ({ route, navigation }: NovelScreenProps) => { const setCustomNovelCover = async () => { showExtraMenu(false); - const newCover = await pickCustomNovelCover(novel.id); + const newCover = await pickCustomNovelCover(novel); if (newCover) { setNovel({ ...novel, diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx index 7c43d40c..f2b2ee9d 100644 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ b/src/screens/settings/SettingsAdvancedScreen.tsx @@ -6,7 +6,6 @@ import { useTheme } from '@hooks/persisted'; import { showToast } from '@utils/showToast'; import { deleteCachedNovels } from '@database/queries/NovelQueries'; -import { clearCoverCache } from '@services/utils/coverCache'; import { getString } from '@strings/translations'; import { useBoolean } from '@hooks'; import ConfirmationDialog from '@components/ConfirmationDialog/ConfirmationDialog'; @@ -64,12 +63,6 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => { onPress={showClearUpdatesDialog} theme={theme} /> - => { export const novelTask = async (parentId: string): Promise => { return getAllNovels().then(novels => { const subtasks = novels.map(novel => { - if (novel.cover && !novel.cover.startsWith('http')) { - novel.cover = `file:///${NovelDownloadFolder}/${novel.pluginId}/${novel.id}/cover.png`; - } return (): Promise => getChapters(novel.id).then(chapters => { return { @@ -61,27 +52,6 @@ export const novelTask = async (parentId: string): Promise => { }); }; -export const novelCoverTask = (parentId: string): Promise => { - return getNovelsWithCustomCover().then(novels => { - return { - taskType: TaskType.NOVEL_COVER, - subtasks: novels.map(novel => { - return async (): Promise => { - return { - folderTree: [parentId], - name: `${NovelDownloadFolder}/${novel.pluginId}/${novel.id}/cover.png`.replace( - AppDownloadFolder + '/', - '', - ), - mimeType: 'image/png', - content: novel.cover || '', - }; - }; - }), - }; - }); -}; - export const categoryTask = (parentId: string): Promise => { return getCategoriesFromDb().then(categories => { const task = async (): Promise => { diff --git a/src/services/backup/remote/backupTasks.ts b/src/services/backup/remote/backupTasks.ts index a77c69a9..444e6909 100644 --- a/src/services/backup/remote/backupTasks.ts +++ b/src/services/backup/remote/backupTasks.ts @@ -1,8 +1,5 @@ import { appVersion } from '@utils/versionUtils'; -import { - AppDownloadFolder, - NovelDownloadFolder, -} from '@utils/constants/download'; +import { AppDownloadFolder } from '@utils/constants/download'; import { walkDir, backupMMKVData } from '../utils'; import { BackupPackage, @@ -10,10 +7,7 @@ import { BackupDataFileName, TaskType, } from '../types'; -import { - getAllNovels, - getNovelsWithCustomCover, -} from '@database/queries/NovelQueries'; +import { getAllNovels } from '@database/queries/NovelQueries'; import { getChapters } from '@database/queries/ChapterQueries'; import { getAllNovelCategories, @@ -40,9 +34,6 @@ export const versionTask = async ( export const novelTask = async (folderTree: string[]): Promise => { return getAllNovels().then(novels => { const subtasks = novels.map(novel => { - if (novel.cover && !novel.cover.startsWith('http')) { - novel.cover = `file:///${NovelDownloadFolder}/${novel.pluginId}/${novel.id}/cover.png`; - } return (): Promise => getChapters(novel.id).then(chapters => { return { @@ -63,27 +54,6 @@ export const novelTask = async (folderTree: string[]): Promise => { }); }; -export const novelCoverTask = (folderTree: string[]): Promise => { - return getNovelsWithCustomCover().then(novels => { - return { - taskType: TaskType.NOVEL_COVER, - subtasks: novels.map(novel => { - return async (): Promise => { - return { - folderTree, - name: `${NovelDownloadFolder}/${novel.pluginId}/${novel.id}/cover.png`.replace( - AppDownloadFolder + '/', - '', - ), - mimeType: 'image/png', - content: novel.cover || '', - }; - }; - }), - }; - }); -}; - export const categoryTask = (folderTree: string[]): Promise => { return getCategoriesFromDb().then(categories => { const task = async (): Promise => { diff --git a/src/services/backup/types.ts b/src/services/backup/types.ts index d4fb273d..413684ff 100644 --- a/src/services/backup/types.ts +++ b/src/services/backup/types.ts @@ -15,7 +15,6 @@ export enum TaskType { VERSION = 'Version', CATEGORY = 'Category', NOVEL_AND_CHAPTERS = 'Novel and Chapters', - NOVEL_COVER = 'Novel Cover', DOWNLOAD = 'Download', SETTING = 'Setting', THEME = 'Theme', diff --git a/src/services/plugin/fetch.ts b/src/services/plugin/fetch.ts index 4c5eb433..ca0aaae3 100644 --- a/src/services/plugin/fetch.ts +++ b/src/services/plugin/fetch.ts @@ -6,6 +6,10 @@ export const fetchNovel = async (pluginId: string, novelUrl: string) => { return res; }; +export const fetchImage = async (pluginId: string, imageUrl: string) => { + return getPlugin(pluginId).fetchImage(imageUrl); +}; + export const fetchChapter = async (pluginId: string, chapterUrl: string) => { const plugin = getPlugin(pluginId); let chapterText = `Not found plugin with id: ${pluginId}`; diff --git a/src/services/updates/LibraryUpdateQueries.ts b/src/services/updates/LibraryUpdateQueries.ts index 2cb0fcae..ff5519d7 100644 --- a/src/services/updates/LibraryUpdateQueries.ts +++ b/src/services/updates/LibraryUpdateQueries.ts @@ -1,21 +1,37 @@ -import { fetchNovel } from '../plugin/fetch'; +import { fetchImage, fetchNovel } from '../plugin/fetch'; import { downloadChapter } from '../../database/queries/ChapterQueries'; import * as SQLite from 'expo-sqlite'; 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'; const db = SQLite.openDatabase('lnreader.db'); -const updateNovelMetadata = async (novelId: number, novel: SourceNovel) => { - const { name, cover, summary, author, artist, genres, status } = novel; - +const updateNovelMetadata = async ( + pluginId: string, + novelId: number, + novel: SourceNovel, +) => { + let { name, cover, summary, author, artist, genres, status } = novel; + const novelDir = NovelDownloadFolder + '/' + pluginId + '/' + novelId; + if (cover) { + const novelCoverUri = 'file://' + novelDir + '/cover.png'; + await fetchImage(pluginId, cover).then(base64 => { + if (base64) { + cover = novelCoverUri; + return RNFS.writeFile(novelCoverUri, base64, 'base64'); + } + }); + cover += '?' + Date.now(); + } db.transaction(tx => { tx.executeSql( 'UPDATE Novel SET name = ?, cover = ?, summary = ?, author = ?, artist = ?, genres = ?, status = ? WHERE id = ?', [ name, - cover || '', - summary || ' ', + cover || null, + summary || '', author || 'unknown', artist || '', genres || '', @@ -45,7 +61,7 @@ const updateNovel = async ( let novel = await fetchNovel(pluginId, novelUrl); if (refreshNovelMetadata) { - updateNovelMetadata(novelId, novel); + updateNovelMetadata(pluginId, novelId, novel); } db.transaction(tx => { novel.chapters?.forEach(chapter => { diff --git a/src/services/utils/coverCache.ts b/src/services/utils/coverCache.ts deleted file mode 100644 index a76bc5a6..00000000 --- a/src/services/utils/coverCache.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { showToast } from '@utils/showToast'; - -const clearCoverCache = () => { - showToast('Handle nothing.'); -}; - -export { clearCoverCache }; diff --git a/src/utils/fetch/fetch.ts b/src/utils/fetch/fetch.ts index e380f0bb..572a7eb0 100644 --- a/src/utils/fetch/fetch.ts +++ b/src/utils/fetch/fetch.ts @@ -36,7 +36,7 @@ export const fetchApi = async ( return await fetch(url, init); }; -export const fetchFile = async (url: string, init: any) => { +export const fetchFile = async (url: string, init: any): Promise => { if (!init) { init = {}; } @@ -49,23 +49,12 @@ export const fetchFile = async (url: string, init: any) => { return new Promise(resolve => { const fr = new FileReader(); fr.onloadend = () => { - if ( - !fr.result - .toString() - .startsWith('data:application/octet-stream;base64,') - ) { - return undefined; - } - resolve( - fr.result - .toString() - .substring('data:application/octet-stream;base64,'.length), - ); + resolve(fr.result.toString().replace(/data:.+\/.+;base64,/, '')); }; fr.readAsDataURL(blob); }); } catch (e) { - return undefined; + return ''; } };