feat: save novel cover to download folder (if possible)
remove clear cached covers
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ export default function usePlugins() {
|
||||
}
|
||||
setMMKVObject(AVAILABLE_PLUGINS, availablePlugins);
|
||||
filterPlugins(languagesFilter);
|
||||
} else {
|
||||
throw new Error(`Failed to installed ${plugin.name}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { showToast } from '@utils/showToast';
|
||||
|
||||
const clearCoverCache = () => {
|
||||
showToast('Handle nothing.');
|
||||
};
|
||||
|
||||
export { clearCoverCache };
|
||||
@@ -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 '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user