feat: save novel cover to download folder (if possible)

remove clear cached covers
This commit is contained in:
Nyagami
2024-01-18 00:18:57 +07:00
parent c2caee6ef9
commit c71dda77d4
14 changed files with 95 additions and 148 deletions
+54 -43
View File
@@ -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<number> => {
): Promise<number | undefined> => {
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<NovelInfo[]> => {
@@ -59,18 +78,6 @@ export const getAllNovels = async (): Promise<NovelInfo[]> => {
);
};
export const getNovelsWithCustomCover = async (): Promise<NovelInfo[]> => {
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<NovelInfo | null> => {
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;
}
};
+2
View File
@@ -118,6 +118,8 @@ export default function usePlugins() {
}
setMMKVObject(AVAILABLE_PLUGINS, availablePlugins);
filterPlugins(languagesFilter);
} else {
throw new Error(`Failed to installed ${plugin.name}`);
}
});
};
+2 -3
View File
@@ -97,8 +97,7 @@ const installPlugin = async (url: string): Promise<Plugin | undefined> => {
}
});
} catch (e: any) {
showToast(e.message);
return undefined;
throw e;
}
};
@@ -135,7 +134,7 @@ const fetchPlugins = async () => {
const githubBranch = 'plugins';
const availablePlugins: Record<Language, Array<PluginItem>> = 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(() => {
@@ -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 =>
@@ -98,6 +98,7 @@ const PluginCard: React.FC<Props> = ({
} else {
installPlugin(plugin)
.then(() => showToast(`Installed ${plugin.name}`))
.catch((error: Error) => showToast(error.message))
.finally(() => setIsLoading(false));
}
}}
+1 -1
View File
@@ -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,
@@ -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}
/>
<List.Item
title="Clean up cached covers"
description="Delete all covers cached"
onPress={clearCoverCache}
theme={theme}
/>
<List.Item
title={getString('advancedSettings.deleteReadChapters')}
onPress={showDeleteReadChaptersDialog}
+2 -32
View File
@@ -5,14 +5,8 @@ import {
TaskType,
} from '../types';
import { appVersion } from '@utils/versionUtils';
import {
getAllNovels,
getNovelsWithCustomCover,
} from '@database/queries/NovelQueries';
import {
AppDownloadFolder,
NovelDownloadFolder,
} from '@utils/constants/download';
import { getAllNovels } from '@database/queries/NovelQueries';
import { AppDownloadFolder } from '@utils/constants/download';
import { getChapters } from '@database/queries/ChapterQueries';
import {
getCategoriesFromDb,
@@ -38,9 +32,6 @@ export const versionTask = async (parentId: string): Promise<BackupTask> => {
export const novelTask = async (parentId: string): Promise<BackupTask> => {
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<BackupPackage> =>
getChapters(novel.id).then(chapters => {
return {
@@ -61,27 +52,6 @@ export const novelTask = async (parentId: string): Promise<BackupTask> => {
});
};
export const novelCoverTask = (parentId: string): Promise<BackupTask> => {
return getNovelsWithCustomCover().then(novels => {
return {
taskType: TaskType.NOVEL_COVER,
subtasks: novels.map(novel => {
return async (): Promise<BackupPackage> => {
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<BackupTask> => {
return getCategoriesFromDb().then(categories => {
const task = async (): Promise<BackupPackage> => {
+2 -32
View File
@@ -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<BackupTask> => {
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<BackupPackage> =>
getChapters(novel.id).then(chapters => {
return {
@@ -63,27 +54,6 @@ export const novelTask = async (folderTree: string[]): Promise<BackupTask> => {
});
};
export const novelCoverTask = (folderTree: string[]): Promise<BackupTask> => {
return getNovelsWithCustomCover().then(novels => {
return {
taskType: TaskType.NOVEL_COVER,
subtasks: novels.map(novel => {
return async (): Promise<BackupPackage> => {
return {
folderTree,
name: `${NovelDownloadFolder}/${novel.pluginId}/${novel.id}/cover.png`.replace(
AppDownloadFolder + '/',
'',
),
mimeType: 'image/png',
content: novel.cover || '',
};
};
}),
};
});
};
export const categoryTask = (folderTree: string[]): Promise<BackupTask> => {
return getCategoriesFromDb().then(categories => {
const task = async (): Promise<BackupPackage> => {
-1
View File
@@ -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',
+4
View File
@@ -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}`;
+23 -7
View File
@@ -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 => {
-7
View File
@@ -1,7 +0,0 @@
import { showToast } from '@utils/showToast';
const clearCoverCache = () => {
showToast('Handle nothing.');
};
export { clearCoverCache };
+3 -14
View File
@@ -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<string> => {
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 '';
}
};