feat: Import EPUB (#833)
* implement classes for plugins * feat: plugin manages UA, cookie itself * fix: repo path * safer regexString * feat: EpubParser native module * add reject to parser * local category, better insert category trigger * add isLocal field to Novel * category update logic for local novels * feat: import epub * delete downloaded folder when delete novel * local novel screen * update build.yml * fix: novel without cover
This commit is contained in:
@@ -8,13 +8,16 @@ jobs:
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Clone Repository (Latest)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install --legacy-peer-deps
|
||||
npm install
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v3
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.rajarsheechatterjee.EpubParser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
public class EpubParser extends ReactContextBaseJavaModule {
|
||||
String rootPath;
|
||||
String contentDirPath;
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
|
||||
EpubParser(ReactApplicationContext context) throws Exception {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "EpubParser";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void parse(String epubFilePath, String epubDirPath, Promise promise) {
|
||||
try{
|
||||
this.rootPath = epubDirPath;
|
||||
String novelCover = this.unzip(epubFilePath, epubDirPath);
|
||||
File contentFile = this.parseContainer(epubDirPath);
|
||||
this.contentDirPath = contentFile.getParent();
|
||||
WritableMap novel = (WritableMap) this.parseContent(contentFile);
|
||||
if(novelCover != null){
|
||||
novel.putString("cover", new File(epubDirPath, novelCover).getAbsolutePath());
|
||||
}
|
||||
promise.resolve(novel);
|
||||
} catch (Exception e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
public File parseContainer(String epubDir) throws Exception {
|
||||
Document doc = this.parserDocument(new File(epubDir, "META-INF/container.xml"));
|
||||
return new File(this.rootPath,
|
||||
((Element) doc.getElementsByTagName("rootfile").item(0)).getAttribute("full-path"));
|
||||
}
|
||||
|
||||
public ReadableMap parseContent(File contentFile) throws Exception {
|
||||
Document content = this.parserDocument(contentFile);
|
||||
WritableMap novel = new WritableNativeMap();
|
||||
novel.putString("name", this.getFirstElementByTag(content, "dc:title").getTextContent().trim());
|
||||
novel.putString("url", contentFile.getParent());
|
||||
novel.putString("author", this.getFirstElementByTag(content, "dc:creator").getTextContent().trim());
|
||||
novel.putArray("chapters", this.getChapters(content));
|
||||
return novel;
|
||||
}
|
||||
|
||||
public ReadableArray getChapters(Document doc) throws Exception {
|
||||
WritableArray chapters = new WritableNativeArray();
|
||||
Document toc = null;
|
||||
if (this.getFirstElementByTag(doc, "spine").getAttribute("toc") != null) {
|
||||
toc = this.parserDocument(new File(this.contentDirPath, "toc.ncx"));
|
||||
}
|
||||
NodeList itemrefs = this.getNodeListByTag(doc, "itemref");
|
||||
for (int i = 0; i < itemrefs.getLength(); i++) {
|
||||
WritableMap chapter = new WritableNativeMap();
|
||||
Element itemref = (Element) itemrefs.item(i);
|
||||
chapter.putString("name", itemref.getAttribute("idref"));
|
||||
Element item = this.getElementById(doc, chapter.getString("name"));
|
||||
chapter.putString("url", new File(this.contentDirPath, item.getAttribute("href")).getAbsolutePath());
|
||||
if(toc != null){
|
||||
chapter.putString("name", this.getChapterName(toc, item.getAttribute(("href"))));
|
||||
if(chapter.getString("name") == null) continue;
|
||||
}
|
||||
chapters.pushMap(chapter);
|
||||
}
|
||||
return chapters;
|
||||
}
|
||||
|
||||
public String getChapterName(Document doc, String href) throws Exception {
|
||||
String path = String.format("//*[starts-with(@src,'%1$s')]", href);
|
||||
Node node = ((NodeList) this.xPath.compile(path).evaluate(doc, XPathConstants.NODESET)).item(0);
|
||||
if(node != null){
|
||||
String text = node.getPreviousSibling().getTextContent().trim();
|
||||
if(text.isEmpty()) text = node.getPreviousSibling().getPreviousSibling().getTextContent().trim();
|
||||
return !text.isEmpty() ? text : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Element getElementById(Document doc, String id) throws Exception {
|
||||
return (Element) ((NodeList) this.xPath
|
||||
.compile(String.format("//*[@id='%1$s']", id))
|
||||
.evaluate(doc, XPathConstants.NODESET))
|
||||
.item(0);
|
||||
}
|
||||
|
||||
public NodeList getNodeListByTag(Document doc, String tag) throws Exception {
|
||||
return (NodeList)
|
||||
(
|
||||
this.xPath.compile(String.format("//*[contains(name(), '%1$s')]", tag))
|
||||
.evaluate(doc, XPathConstants.NODESET)
|
||||
);
|
||||
}
|
||||
|
||||
public Element getFirstElementByTag(Document doc, String tag) throws Exception {
|
||||
return (Element) this.getNodeListByTag(doc, tag).item(0);
|
||||
}
|
||||
|
||||
public Document parserDocument(File docFile) throws Exception {
|
||||
Document doc = this.db.parse(docFile);
|
||||
doc.getDocumentElement().normalize();
|
||||
return doc;
|
||||
}
|
||||
|
||||
public String unzip(String epubFilePath, String epubDirPath) throws Exception {
|
||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(epubFilePath));
|
||||
ZipEntry zipEntry;
|
||||
int len;
|
||||
String novelCover = null;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||
if(Pattern.compile(".*cover.*\\.(png|jpeg|jpg)", Pattern.CASE_INSENSITIVE).matcher(zipEntry.getName()).matches()){
|
||||
novelCover = zipEntry.getName();
|
||||
}
|
||||
File newFile = new File(epubDirPath, zipEntry.getName());
|
||||
newFile.getParentFile().mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
while ((len = zis.read(buffer)) > 0)
|
||||
fos.write(buffer, 0, len);
|
||||
fos.close();
|
||||
}
|
||||
zis.closeEntry();
|
||||
zis.close();
|
||||
return novelCover;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.rajarsheechatterjee.EpubParser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class EpubParserPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new EpubParser(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
import com.rajarsheechatterjee.EpubParser.EpubParserPackage;
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
@@ -21,10 +23,6 @@ import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreenReactPackage;
|
||||
import com.rnfs.RNFSPackage;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
@@ -36,10 +34,10 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new com.rajarsheechatterjee.LNReader.NavigationBarColorPackage());
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
packages.add(new EpubParserPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@@ -69,14 +67,6 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method
|
||||
* with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*
|
||||
* @param context
|
||||
* @param reactInstanceManager
|
||||
*/
|
||||
private static void initializeFlipper(
|
||||
Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
|
||||
+1
-4
@@ -1,21 +1,18 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
package com.rajarsheechatterjee.NavigationBarColor;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import androidx.annotation.UiThread;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
package com.rajarsheechatterjee.NavigationBarColor;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
@@ -18,6 +18,7 @@ module.exports = function (api) {
|
||||
'@utils': './src/utils',
|
||||
'@theme': './src/theme',
|
||||
'@navigators': './src/navigators',
|
||||
'@native': './src/native',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ import * as SQLite from 'expo-sqlite';
|
||||
import {
|
||||
createCategoriesTableQuery,
|
||||
createCategoryDefaultQuery,
|
||||
createCategoryLocalQuery,
|
||||
createCategoryTriggerQuery,
|
||||
} from './tables/CategoryTable';
|
||||
import { createNovelTableQuery } from './tables/NovelTable';
|
||||
@@ -22,8 +23,9 @@ export const createTables = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createNovelTableQuery);
|
||||
tx.executeSql(createCategoriesTableQuery, [], () => {
|
||||
tx.executeSql(createCategoryTriggerQuery);
|
||||
tx.executeSql(createCategoryDefaultQuery);
|
||||
tx.executeSql(createCategoryLocalQuery);
|
||||
tx.executeSql(createCategoryTriggerQuery);
|
||||
});
|
||||
tx.executeSql(createNovelCategoryTableQuery);
|
||||
tx.executeSql(createChapterTableQuery, [], () => {
|
||||
|
||||
@@ -41,7 +41,7 @@ const beforeDeleteCategoryQuery = `
|
||||
const deleteCategoryQuery = 'DELETE FROM Category WHERE id = ?';
|
||||
|
||||
export const deleteCategoryById = (category: Category): void => {
|
||||
if (category.sort === 1) {
|
||||
if (category.sort <= 2) {
|
||||
return showToast('You cant delete default category');
|
||||
}
|
||||
db.transaction(tx => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { txnErrorCallback } from '../utils/helpers';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
export const getNovelsWithCatogory = (
|
||||
export const getNovelsWithCategory = (
|
||||
categoryId: number,
|
||||
onlyOngoingNovels?: boolean,
|
||||
): Promise<NovelInfo[]> => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as SQLite from 'expo-sqlite';
|
||||
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 { insertChapters } from './ChapterQueries';
|
||||
@@ -12,8 +13,9 @@ import { noop } from 'lodash-es';
|
||||
import { getString } from '@strings/translations';
|
||||
import { NovelInfo } from '../types';
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import { NovelDownloadFolder } from '@utils/constants/download';
|
||||
|
||||
export const insertNovelandChapters = (
|
||||
export const insertNovelAndChapters = (
|
||||
pluginId: string,
|
||||
sourceNovel: SourceNovel,
|
||||
): Promise<number> => {
|
||||
@@ -51,7 +53,7 @@ export const getNovel = async (novelUrl: string): Promise<NovelInfo> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'SELECT id, * FROM Novel WHERE url = ?',
|
||||
'SELECT * FROM Novel WHERE url = ?',
|
||||
[novelUrl],
|
||||
(txObj, { rows }) => resolve(rows.item(0)),
|
||||
txnErrorCallback,
|
||||
@@ -69,6 +71,9 @@ export const switchNovelToLibrary = async (
|
||||
) => {
|
||||
const novel = await getNovel(novelUrl);
|
||||
if (novel) {
|
||||
if (novel.inLibrary) {
|
||||
return;
|
||||
}
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE Novel SET inLibrary = ? WHERE id = ?',
|
||||
@@ -94,7 +99,7 @@ export const switchNovelToLibrary = async (
|
||||
});
|
||||
} else {
|
||||
const sourceNovel = await fetchNovel(pluginId, novelUrl);
|
||||
const novelId = await insertNovelandChapters(pluginId, sourceNovel);
|
||||
const novelId = await insertNovelAndChapters(pluginId, sourceNovel);
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE Novel SET inLibrary = 1 WHERE url = ?',
|
||||
@@ -112,6 +117,7 @@ export const switchNovelToLibrary = async (
|
||||
}
|
||||
};
|
||||
|
||||
// allow to delete local novels
|
||||
export const removeNovelsFromLibrary = (novelIds: Array<number>) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
@@ -124,7 +130,27 @@ export const removeNovelsFromLibrary = (novelIds: Array<number>) => {
|
||||
showToast('Removed from Library');
|
||||
};
|
||||
|
||||
export const deleteNovelCache = () => {
|
||||
export const getCachedNovels = (): Promise<NovelInfo[]> => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'SELECT * FROM Novel WHERE inLibrary = 0',
|
||||
[],
|
||||
(txObj, { rows }) => resolve(rows._array as NovelInfo[]),
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
export const deleteCachedNovels = async () => {
|
||||
const cachedNovels = await getCachedNovels();
|
||||
for (let novel of cachedNovels) {
|
||||
const novelDir =
|
||||
NovelDownloadFolder + '/' + novel.pluginId + '/' + novel.id;
|
||||
if (await RNFS.exists(novelDir)) {
|
||||
await RNFS.unlink(novelDir);
|
||||
}
|
||||
}
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'DELETE FROM Novel WHERE inLibrary = 0',
|
||||
@@ -181,14 +207,17 @@ export const restoreLibrary = async (novel: NovelInfo) => {
|
||||
export const updateNovelInfo = async (info: NovelInfo) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE Novel SET name = ?, summary = ?, author = ?, artist = ?, genres = ?, status = ? WHERE id = ?',
|
||||
'UPDATE Novel SET name = ?, cover = ?, url = ? , summary = ?, author = ?, artist = ?, genres = ?, status = ?, isLocal = ? WHERE id = ?',
|
||||
[
|
||||
info.name,
|
||||
info.cover || '',
|
||||
info.url,
|
||||
info.summary || '',
|
||||
info.author || '',
|
||||
info.artist || '',
|
||||
info.genres || '',
|
||||
info.status || '',
|
||||
info.isLocal,
|
||||
info.id,
|
||||
],
|
||||
noop,
|
||||
@@ -237,6 +266,8 @@ export const updateNovelCategories = async (
|
||||
option: string,
|
||||
): Promise<void> => {
|
||||
let queries: string[] = [];
|
||||
// allow local novels have other categories, but not the revesre
|
||||
categoryIds = categoryIds.filter(id => id !== 2);
|
||||
if (option === 'KEEP_OLD') {
|
||||
novelIds.forEach(novelId => {
|
||||
categoryIds.forEach(categoryId =>
|
||||
|
||||
@@ -9,9 +9,12 @@ export const createCategoriesTableQuery = `
|
||||
export const createCategoryTriggerQuery = `
|
||||
CREATE TRIGGER IF NOT EXISTS add_category AFTER INSERT ON Category
|
||||
BEGIN
|
||||
UPDATE Category SET sort = (SELECT MAX(sort) FROM Category) + 1 WHERE id = new.id;
|
||||
UPDATE Category SET sort = (SELECT IFNULL(sort, new.id)) WHERE id = new.id;
|
||||
END;
|
||||
`;
|
||||
|
||||
export const createCategoryDefaultQuery =
|
||||
'INSERT OR IGNORE INTO Category (id, name, sort) VALUES (IFNULL((SELECT MAX(id) FROM Category), 1), "Default", 0)';
|
||||
'INSERT OR IGNORE INTO Category (id, name, sort) VALUES (1, "Default", 1)';
|
||||
|
||||
export const createCategoryLocalQuery =
|
||||
'INSERT OR IGNORE INTO Category (id, name, sort) VALUES (2, "Local", 2)';
|
||||
|
||||
@@ -10,6 +10,7 @@ export const createNovelTableQuery = `
|
||||
artist TEXT,
|
||||
status TEXT Default 'Unknown',
|
||||
genres TEXT,
|
||||
inLibrary INTEGER DEFAULT 0
|
||||
inLibrary INTEGER DEFAULT 0,
|
||||
isLocal INTERGER DEFAULT 0
|
||||
);
|
||||
`;
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface NovelInfo {
|
||||
status?: NovelStatus;
|
||||
genres?: string;
|
||||
inLibrary: number;
|
||||
isLocal: number;
|
||||
}
|
||||
|
||||
export interface LibraryNovelInfo extends NovelInfo {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import { NativeModules } from 'react-native';
|
||||
interface EpubParserInterface {
|
||||
parse: (epubFilePath: string, epubDirPath: string) => Promise<SourceNovel>;
|
||||
}
|
||||
const { EpubParser } = NativeModules;
|
||||
|
||||
export default EpubParser as EpubParserInterface;
|
||||
@@ -149,7 +149,7 @@ const fetchPlugins = async () => {
|
||||
const githubBranch = 'plugins';
|
||||
|
||||
const availablePlugins: Record<Languages, Array<PluginItem>> = await fetch(
|
||||
`https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/${githubBranch}/dist/${githubUsername}/plugins.min.json`,
|
||||
`https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/${githubBranch}/.dist/${githubUsername}/plugins.min.json`,
|
||||
).then(res => res.json());
|
||||
return availablePlugins;
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { fetchNovel } from '@services/plugin/fetch';
|
||||
import {
|
||||
switchNovelToLibrary,
|
||||
insertNovelandChapters,
|
||||
insertNovelAndChapters,
|
||||
getNovel,
|
||||
} from '@database/queries/NovelQueries';
|
||||
import { insertChapters } from '@database/queries/ChapterQueries';
|
||||
@@ -85,7 +85,7 @@ export const getNovelAction =
|
||||
/**
|
||||
* Insert novel in db.
|
||||
*/
|
||||
await insertNovelandChapters(pluginId, fetchedNovel);
|
||||
await insertNovelAndChapters(pluginId, fetchedNovel);
|
||||
/**
|
||||
* Get novel from db.
|
||||
*/
|
||||
|
||||
+137
-133
@@ -294,6 +294,7 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const renderItem = ({ item: it, index }) => (
|
||||
<ChapterItem
|
||||
isLocal={novel.isLocal}
|
||||
theme={theme}
|
||||
chapter={it}
|
||||
showChapterTitles={showChapterTitles}
|
||||
@@ -359,140 +360,143 @@ const Novel = ({ route, navigation }) => {
|
||||
}}
|
||||
onPress={() => showJumpToChapterModal(true)}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
visible={downloadMenu}
|
||||
onDismiss={() => showDownloadMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="download-outline"
|
||||
iconColor={theme.onBackground}
|
||||
size={24}
|
||||
style={{
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
}}
|
||||
onPress={() => showDownloadMenu(true)}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.surface2 }}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Next chapter"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{ color: theme.onSurface }}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(novel.pluginId, novel.url, [
|
||||
chapters.find(
|
||||
chapter =>
|
||||
!!chapter.unread === true &&
|
||||
!!chapter.isDownloaded === false,
|
||||
),
|
||||
]),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Next 5 chapter"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.unread === true &&
|
||||
!!chapter.idDownloaded === false,
|
||||
)
|
||||
.slice(0, 5),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Next 10 chapter"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.unread === true &&
|
||||
!!chapter.Downloaded === false,
|
||||
)
|
||||
.slice(0, 10),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Custom"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{ color: theme.onSurface }}
|
||||
onPress={() => {
|
||||
downloadCustomChapterModal.setTrue();
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Unread"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters.filter(chapter => !!chapter.unread === true),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="All"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters,
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Delete downloads"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(deleteAllChaptersAction(pluginId, chapters))
|
||||
{!novel.isLocal && (
|
||||
<Menu
|
||||
visible={downloadMenu}
|
||||
onDismiss={() => showDownloadMenu(false)}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="download-outline"
|
||||
iconColor={theme.onBackground}
|
||||
size={24}
|
||||
style={{
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
}}
|
||||
onPress={() => showDownloadMenu(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Menu>
|
||||
contentStyle={{ backgroundColor: theme.surface2 }}
|
||||
>
|
||||
<Menu.Item
|
||||
title="Next chapter"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{ color: theme.onSurface }}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(novel.pluginId, novel.url, [
|
||||
chapters.find(
|
||||
chapter =>
|
||||
!!chapter.unread === true &&
|
||||
!!chapter.isDownloaded === false,
|
||||
),
|
||||
]),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Next 5 chapter"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.unread === true &&
|
||||
!!chapter.idDownloaded === false,
|
||||
)
|
||||
.slice(0, 5),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Next 10 chapter"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.unread === true &&
|
||||
!!chapter.Downloaded === false,
|
||||
)
|
||||
.slice(0, 10),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Custom"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{ color: theme.onSurface }}
|
||||
onPress={() => {
|
||||
downloadCustomChapterModal.setTrue();
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Unread"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters.filter(
|
||||
chapter => !!chapter.unread === true,
|
||||
),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="All"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.pluginId,
|
||||
novel.url,
|
||||
chapters,
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Delete downloads"
|
||||
style={{ backgroundColor: theme.surface2 }}
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(deleteAllChaptersAction(pluginId, chapters))
|
||||
}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
visible={extraMenu}
|
||||
|
||||
@@ -26,7 +26,7 @@ interface ChapterItemProps {
|
||||
navigateToChapter: (chapter: ChapterInfo) => void;
|
||||
showProgressPercentage?: (chapter: ChapterInfo) => any;
|
||||
left?: ReactNode;
|
||||
|
||||
isLocal: boolean;
|
||||
isUpdateCard?: boolean;
|
||||
novelName: string;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
onSelectLongPress,
|
||||
navigateToChapter,
|
||||
showProgressPercentage,
|
||||
isLocal,
|
||||
left,
|
||||
isUpdateCard,
|
||||
novelName,
|
||||
@@ -124,16 +125,18 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
</View>
|
||||
</View>
|
||||
</Row>
|
||||
<DownloadButton
|
||||
downloadQueue={downloadQueue}
|
||||
chapter={chapter}
|
||||
theme={theme}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
hideDeleteChapterMenu={hideDeleteChapterMenu}
|
||||
showDeleteChapterMenu={showDeleteChapterMenu}
|
||||
deleteChapterMenuVisible={deleteChapterMenuVisible}
|
||||
/>
|
||||
{!isLocal && (
|
||||
<DownloadButton
|
||||
downloadQueue={downloadQueue}
|
||||
chapter={chapter}
|
||||
theme={theme}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
hideDeleteChapterMenu={hideDeleteChapterMenu}
|
||||
showDeleteChapterMenu={showDeleteChapterMenu}
|
||||
deleteChapterMenuVisible={deleteChapterMenuVisible}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ const NovelScreenButtonGroup: React.FC<NovelScreenButtonGroupProps> = ({
|
||||
handleFollowNovel,
|
||||
theme,
|
||||
}) => {
|
||||
const { inLibrary } = novel;
|
||||
const { inLibrary, isLocal } = novel;
|
||||
const { navigate } = useNavigation();
|
||||
const followButtonColor = inLibrary ? theme.primary : theme.outline;
|
||||
|
||||
@@ -98,7 +98,7 @@ const NovelScreenButtonGroup: React.FC<NovelScreenButtonGroupProps> = ({
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
{inLibrary ? (
|
||||
{inLibrary && !isLocal && (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
@@ -115,23 +115,25 @@ const NovelScreenButtonGroup: React.FC<NovelScreenButtonGroupProps> = ({
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.buttonContainer}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={handleOpenWebView}
|
||||
style={styles.button}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="earth"
|
||||
color={theme.outline}
|
||||
size={24}
|
||||
/>
|
||||
<Text style={[styles.buttonLabel, { color: theme.outline }]}>
|
||||
WebView
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
{!isLocal && (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={handleOpenWebView}
|
||||
style={styles.button}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="earth"
|
||||
color={theme.outline}
|
||||
size={24}
|
||||
/>
|
||||
<Text style={[styles.buttonLabel, { color: theme.outline }]}>
|
||||
WebView
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Portal>
|
||||
<SetCategoryModal
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dialog, Portal } from 'react-native-paper';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
|
||||
import { deleteNovelCache } from '@database/queries/NovelQueries';
|
||||
import { deleteCachedNovels } from '@database/queries/NovelQueries';
|
||||
import { clearCoverCache } from '@services/utils/coverCache';
|
||||
import { getString } from '@strings/translations';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
import { Appbar, Button, List } from '@components';
|
||||
import useSourceStorage from '@hooks/useSourceStorage';
|
||||
import { importEpub } from '@services/epub/import';
|
||||
|
||||
const AdvancedSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -72,6 +73,7 @@ const AdvancedSettings = ({ navigation }) => {
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item title="Clear cookies" onPress={clearCookies} theme={theme} />
|
||||
<List.Item title="Import Epub" onPress={importEpub} theme={theme} />
|
||||
</List.Section>
|
||||
<Portal>
|
||||
<ConfirmationDialog
|
||||
@@ -106,7 +108,7 @@ const AdvancedSettings = ({ navigation }) => {
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
deleteNovelCache();
|
||||
deleteCachedNovels();
|
||||
hideClearDatabaseDialog();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as RNFS from 'react-native-fs';
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { sleep } from '@utils/sleep';
|
||||
import EpubParser from '@native/epubParser';
|
||||
import { NovelDownloadFolder } from '@utils/constants/download';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
updateNovelCategoryById,
|
||||
updateNovelInfo,
|
||||
} from '@database/queries/NovelQueries';
|
||||
|
||||
interface TaskData {
|
||||
delay: number;
|
||||
epubFilePath: string;
|
||||
epubDirPath: string; // export dir
|
||||
}
|
||||
|
||||
const insertLocalNovel = (
|
||||
name: string,
|
||||
url: string,
|
||||
cover?: string,
|
||||
author?: string,
|
||||
): Promise<number> => {
|
||||
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 || '', author || 'unknown'],
|
||||
async (txObj, resultSet) => {
|
||||
if (resultSet.insertId) {
|
||||
await updateNovelCategoryById(resultSet.insertId, [2]);
|
||||
const novelDir =
|
||||
NovelDownloadFolder + '/local/' + resultSet.insertId;
|
||||
await RNFS.mkdir(novelDir);
|
||||
const newCoverPath =
|
||||
'file://' + novelDir + '/' + cover?.split(/[\/\\]/).pop();
|
||||
if (cover && (await RNFS.exists(cover))) {
|
||||
await RNFS.moveFile(cover, newCoverPath);
|
||||
}
|
||||
await updateNovelInfo({
|
||||
pluginId: 'local',
|
||||
id: resultSet.insertId,
|
||||
url: NovelDownloadFolder + '/local/' + resultSet.insertId,
|
||||
cover: newCoverPath,
|
||||
name: name,
|
||||
inLibrary: 1,
|
||||
isLocal: 1,
|
||||
});
|
||||
resolve(resultSet.insertId);
|
||||
} else {
|
||||
reject(new Error('novel insert failed'));
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
reject(error);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const insertLocalChapter = (
|
||||
novelId: number,
|
||||
fakeId: number,
|
||||
name: string,
|
||||
url: string,
|
||||
releaseTime: string,
|
||||
): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'INSERT INTO Chapter(novelId, name, url, releaseTime) VALUES(?, ?, ?, ?)',
|
||||
[
|
||||
novelId,
|
||||
name,
|
||||
// use fakeid just for make the url is unique :D
|
||||
NovelDownloadFolder + '/local/' + novelId + '/' + fakeId,
|
||||
releaseTime,
|
||||
],
|
||||
async (txObj, resultSet) => {
|
||||
if (resultSet.insertId) {
|
||||
let chapterText = await RNFS.readFile(url);
|
||||
const staticPaths: string[] = [];
|
||||
const novelDir = NovelDownloadFolder + '/local/' + novelId;
|
||||
const epubContentDir = url.replace(/[^\\\/]+$/, '');
|
||||
chapterText = chapterText.replace(
|
||||
/(href|src)=(["'])(.*?)\2/g,
|
||||
($0, $1, $2, $3) => {
|
||||
if ($3) {
|
||||
staticPaths.push(epubContentDir + '/' + $3);
|
||||
}
|
||||
return (
|
||||
$1 +
|
||||
'=' +
|
||||
$2 +
|
||||
'file://' +
|
||||
novelDir +
|
||||
'/' +
|
||||
$3.split(/[\\\/]/)?.pop() +
|
||||
$2
|
||||
);
|
||||
},
|
||||
);
|
||||
await RNFS.mkdir(novelDir + '/' + resultSet.insertId);
|
||||
await RNFS.writeFile(
|
||||
novelDir + '/' + resultSet.insertId + '/index.html',
|
||||
chapterText,
|
||||
);
|
||||
resolve(staticPaths);
|
||||
} else {
|
||||
reject(new Error('chapter insert failed'));
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
reject(error);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
const importEpubAction = async (taskData?: TaskData) => {
|
||||
if (!taskData) {
|
||||
throw new Error('No data provided');
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
const novel: SourceNovel = await EpubParser.parse(
|
||||
taskData.epubFilePath,
|
||||
taskData.epubDirPath,
|
||||
);
|
||||
await sleep(taskData.delay);
|
||||
try {
|
||||
const novelId = await insertLocalNovel(
|
||||
novel.name,
|
||||
novel.url,
|
||||
novel.cover,
|
||||
novel.artist,
|
||||
).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
const now = dayjs().format('L');
|
||||
const filePathSet = new Set<string>();
|
||||
if (novel.chapters) {
|
||||
BackgroundService.updateNotification({
|
||||
taskTitle: 'Import Novel',
|
||||
taskDesc: '0/' + novel.chapters.length,
|
||||
progressBar: {
|
||||
value: 0,
|
||||
max: novel.chapters.length,
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < novel.chapters?.length; i++) {
|
||||
BackgroundService.updateNotification({
|
||||
taskDesc: i + 1 + '/' + novel.chapters.length,
|
||||
progressBar: {
|
||||
value: i + 1,
|
||||
max: novel.chapters.length,
|
||||
},
|
||||
});
|
||||
const chapter = novel.chapters[i];
|
||||
if (chapter.name == null) {
|
||||
chapter.name = chapter.url.split(/[\\\/]/).pop() || 'unknown';
|
||||
}
|
||||
const filePaths = await insertLocalChapter(
|
||||
novelId,
|
||||
i,
|
||||
chapter.name,
|
||||
chapter.url,
|
||||
now,
|
||||
).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
filePaths.forEach(filePath => filePathSet.add(filePath));
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
// move static files
|
||||
const novelDir = NovelDownloadFolder + '/local/' + novelId;
|
||||
BackgroundService.updateNotification({
|
||||
taskTitle: 'Import static files',
|
||||
taskDesc: '0/' + filePathSet.size,
|
||||
progressBar: {
|
||||
value: 0,
|
||||
max: filePathSet.size,
|
||||
},
|
||||
});
|
||||
let cnt = 1;
|
||||
for (let filePath of filePathSet) {
|
||||
BackgroundService.updateNotification({
|
||||
taskDesc: cnt + '/' + filePathSet.size,
|
||||
progressBar: {
|
||||
value: cnt,
|
||||
max: filePathSet.size,
|
||||
},
|
||||
});
|
||||
if (await RNFS.exists(filePath)) {
|
||||
await RNFS.moveFile(
|
||||
filePath,
|
||||
novelDir + '/' + filePath.split(/[\\\/]/).pop(),
|
||||
);
|
||||
}
|
||||
cnt += 1;
|
||||
}
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Import Epub',
|
||||
body: 'Done',
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
} catch (e: any) {
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Import error',
|
||||
body: e.message,
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
await BackgroundService.stop();
|
||||
}
|
||||
};
|
||||
|
||||
export const importEpub = async () => {
|
||||
try {
|
||||
const epubFile = await DocumentPicker.getDocumentAsync({
|
||||
type: 'application/epub+zip',
|
||||
copyToCacheDirectory: true,
|
||||
});
|
||||
if (epubFile.type === 'cancel') {
|
||||
throw new Error('Cancel');
|
||||
}
|
||||
const epubFilePath = RNFS.ExternalCachesDirectoryPath + '/novel.epub';
|
||||
await RNFS.moveFile(epubFile.uri, epubFilePath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
const epubDirPath = RNFS.ExternalCachesDirectoryPath + '/epub';
|
||||
await RNFS.mkdir(epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
await BackgroundService.start<TaskData>(importEpubAction, {
|
||||
taskName: 'Import Epub',
|
||||
taskTitle: 'Parse Epub',
|
||||
taskDesc: 'Parsing',
|
||||
taskIcon: { name: 'notification_icon', type: 'drawable' },
|
||||
color: '#00adb5',
|
||||
parameters: { delay: 500, epubFilePath, epubDirPath },
|
||||
});
|
||||
} catch (e: any) {
|
||||
// BackgroundService.catch cant catch Error from importEpubAction
|
||||
// this catch the aboves
|
||||
// importEpubAction catches itself
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Import error',
|
||||
body: e.message,
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
await BackgroundService.stop();
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { store } from '@redux/store';
|
||||
import { NovelInfo, ChapterInfo } from '@database/types';
|
||||
import {
|
||||
getNovel,
|
||||
insertNovelandChapters,
|
||||
insertNovelAndChapters,
|
||||
} from '@database/queries/NovelQueries';
|
||||
import { getChapters } from '@database/queries/ChapterQueries';
|
||||
import { downloadChapter } from '@database/queries/ChapterQueries';
|
||||
@@ -64,7 +64,7 @@ export const migrateNovel = async (
|
||||
toChapters = await getChapters(toNovel.id, '', '');
|
||||
} else {
|
||||
const fetchedNovel = await fetchNovel(pluginId, toNovelUrl);
|
||||
await insertNovelandChapters(pluginId, fetchedNovel);
|
||||
await insertNovelAndChapters(pluginId, fetchedNovel);
|
||||
toNovel = await getNovel(toNovelUrl);
|
||||
toChapters = await getChapters(toNovel.id, '', '');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Notifications from 'expo-notifications';
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
|
||||
import {
|
||||
getNovelsWithCatogory,
|
||||
getLibraryWithCategory,
|
||||
getLibraryNovelsFromDb,
|
||||
} from '../../database/queries/LibraryQueries';
|
||||
|
||||
@@ -17,7 +17,7 @@ const updateLibrary = async options => {
|
||||
let libraryNovels = [];
|
||||
|
||||
if (categoryId) {
|
||||
libraryNovels = await getNovelsWithCatogory(
|
||||
libraryNovels = await getLibraryWithCategory(
|
||||
categoryId,
|
||||
onlyUpdateOngoingNovels,
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@plugins/*": ["src/plugins/*"],
|
||||
"@services/*": ["src/services/*"],
|
||||
"@navigators/*": ["src/navigators/*"],
|
||||
"@native/*": ["src/native/*"],
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
Reference in New Issue
Block a user