update: use cheerio to parse local novel
This commit is contained in:
@@ -1,174 +0,0 @@
|
||||
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 escapeFilePath(String filePath){
|
||||
return filePath.replaceAll(":", "\uA789");
|
||||
}
|
||||
|
||||
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) {
|
||||
String escapedFilePath = escapeFilePath(zipEntry.getName());
|
||||
if(Pattern.compile(".*cover.*\\.(png|jpeg|jpg)", Pattern.CASE_INSENSITIVE).matcher(escapedFilePath).matches()){
|
||||
novelCover = escapedFilePath;
|
||||
}
|
||||
File newFile = new File(epubDirPath, escapedFilePath);
|
||||
if(newFile.exists()) continue;
|
||||
newFile.getParentFile().mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
int writeCount = 0;
|
||||
while ((len = zis.read(buffer)) > 0) {
|
||||
writeCount += 1;
|
||||
fos.write(buffer, 0, len);
|
||||
}
|
||||
fos.close();
|
||||
if(writeCount == 0){
|
||||
newFile.delete();
|
||||
}
|
||||
}
|
||||
zis.closeEntry();
|
||||
zis.close();
|
||||
return novelCover;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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 java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class ZipArchive extends ReactContextBaseJavaModule {
|
||||
ZipArchive(ReactApplicationContext context) throws Exception {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ZipArchive";
|
||||
}
|
||||
public String escapeFilePath(String filePath){
|
||||
return filePath.replaceAll(":", "\uA789");
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unzip(String epubFilePath, String epubDirPath, Promise promise) {
|
||||
try{
|
||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(epubFilePath));
|
||||
ZipEntry zipEntry;
|
||||
int len;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||
String escapedFilePath = this.escapeFilePath(zipEntry.getName());
|
||||
File newFile = new File(epubDirPath, 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);
|
||||
}
|
||||
fos.close();
|
||||
if(!isWritten) newFile.delete();
|
||||
}
|
||||
zis.closeEntry();
|
||||
zis.close();
|
||||
promise.resolve(null);
|
||||
} catch (Exception e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -11,13 +11,13 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class EpubParserPackage implements ReactPackage {
|
||||
public class ZipArchivePackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new EpubParser(reactApplicationContext));
|
||||
modules.add(new ZipArchive(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
import com.rajarsheechatterjee.EpubParser.EpubParserPackage;
|
||||
import com.rajarsheechatterjee.EpubParser.ZipArchivePackage;
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
@@ -35,7 +35,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
packages.add(new EpubParserPackage());
|
||||
packages.add(new ZipArchivePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import { NativeModules } from 'react-native';
|
||||
interface ZipArchiveInterface {
|
||||
unzip: (epubFilePath: string, epubDirPath: string) => Promise<SourceNovel>;
|
||||
}
|
||||
const { ZipArchive } = NativeModules;
|
||||
|
||||
export default ZipArchive as ZipArchiveInterface;
|
||||
@@ -1,8 +0,0 @@
|
||||
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;
|
||||
+118
-42
@@ -1,12 +1,11 @@
|
||||
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 ZipArchive from '@native/ZipArchive';
|
||||
import { NovelDownloadFolder } from '@utils/constants/download';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@@ -16,11 +15,12 @@ import {
|
||||
import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { BACKGROUND_ACTION, BackgoundAction } from '@services/constants';
|
||||
import { ChapterItem, SourceNovel } from '@plugins/types';
|
||||
import { load as parseXML } from 'cheerio';
|
||||
|
||||
interface TaskData {
|
||||
delay: number;
|
||||
epubFilePath: string;
|
||||
epubDirPath: string; // export dir
|
||||
sourceUri: string;
|
||||
}
|
||||
|
||||
const insertLocalNovel = (
|
||||
@@ -33,7 +33,7 @@ const insertLocalNovel = (
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
"INSERT INTO Novel(name, url, cover, author, pluginId, inLibrary, isLocal) VALUES(?, ?, ?, ?, 'local', 1, 1)",
|
||||
[name, url, cover || '', author || 'unknown'],
|
||||
[name, url, cover || null, author || null],
|
||||
async (txObj, resultSet) => {
|
||||
if (resultSet.insertId) {
|
||||
await updateNovelCategoryById(resultSet.insertId, [2]);
|
||||
@@ -48,6 +48,7 @@ const insertLocalNovel = (
|
||||
await updateNovelInfo({
|
||||
pluginId: LOCAL_PLUGIN_ID,
|
||||
id: resultSet.insertId,
|
||||
author: author,
|
||||
url: NovelDownloadFolder + '/local/' + resultSet.insertId,
|
||||
cover: newCoverPath,
|
||||
name: name,
|
||||
@@ -81,7 +82,7 @@ const insertLocalChapter = (
|
||||
'INSERT INTO Chapter(novelId, name, url, releaseTime) VALUES(?, ?, ?, ?)',
|
||||
[
|
||||
novelId,
|
||||
decodeURI(name),
|
||||
name,
|
||||
// use fakeid just for make the url is unique :D
|
||||
NovelDownloadFolder + '/local/' + novelId + '/' + fakeId,
|
||||
releaseTime,
|
||||
@@ -95,20 +96,12 @@ const insertLocalChapter = (
|
||||
chapterText = chapterText.replace(
|
||||
/(href|src)=(["'])(.*?)\2/g,
|
||||
($0, $1, $2, $3: string) => {
|
||||
const escapedFilePath = $3.replace(/:/g, '\uA789');
|
||||
if ($3) {
|
||||
staticPaths.push(epubContentDir + '/' + escapedFilePath);
|
||||
staticPaths.push(epubContentDir + '/' + $3);
|
||||
}
|
||||
return (
|
||||
$1 +
|
||||
'=' +
|
||||
$2 +
|
||||
'file://' +
|
||||
novelDir +
|
||||
'/' +
|
||||
escapedFilePath.split(/[\\\/]/)?.pop() +
|
||||
$2
|
||||
);
|
||||
return `${$1}="file://${novelDir}/${$3
|
||||
.split(/[\\\/]/)
|
||||
?.pop()}"`;
|
||||
},
|
||||
);
|
||||
await RNFS.mkdir(novelDir + '/' + resultSet.insertId);
|
||||
@@ -129,29 +122,122 @@ const insertLocalChapter = (
|
||||
});
|
||||
});
|
||||
};
|
||||
const importEpubAction = async (taskData?: TaskData) => {
|
||||
if (!taskData) {
|
||||
throw new Error('No data provided');
|
||||
|
||||
const getParent = (filePath: string) => {
|
||||
return filePath.replace(/[/\\][^/\\]+$/, '');
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (
|
||||
epubDirPath: string,
|
||||
): Promise<SourceNovel> => {
|
||||
const containerText = await RNFS.readFile(
|
||||
`${epubDirPath}/META-INF/container.xml`,
|
||||
);
|
||||
const contentPath =
|
||||
epubDirPath + '/' + parseXML(containerText)('rootfile').attr('full-path');
|
||||
const contentDir = getParent(contentPath);
|
||||
const contentText = await RNFS.readFile(contentPath);
|
||||
const parsedContent = parseXML(contentText);
|
||||
const novelName = parsedContent('dc\\:title').text().trim();
|
||||
const author = parsedContent('dc\\:creator').text().trim();
|
||||
const coverRef = parsedContent('meta[name="cover"]').attr('content');
|
||||
let cover = '';
|
||||
if (coverRef) {
|
||||
cover =
|
||||
contentDir + '/' + parsedContent(`item[id="${coverRef}"]`).attr('href');
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 1,
|
||||
max: 4,
|
||||
},
|
||||
});
|
||||
const tocPath = contentDir + '/' + 'toc.ncx';
|
||||
const tocMap: Record<string, string> = {};
|
||||
if (await RNFS.exists(tocPath)) {
|
||||
const tocText = await RNFS.readFile(tocPath);
|
||||
const toc = parseXML(tocText);
|
||||
toc('navPoint').each(function () {
|
||||
const href = toc(this).find('content').attr('src');
|
||||
const name = toc(this).find('text').text().trim();
|
||||
if (href && name) {
|
||||
tocMap[href] = name;
|
||||
}
|
||||
});
|
||||
}
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 2,
|
||||
max: 4,
|
||||
},
|
||||
});
|
||||
const itemMap: Record<string, string> = {};
|
||||
parsedContent('item').each(function () {
|
||||
itemMap[this.attribs.id] = this.attribs.href;
|
||||
});
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 3,
|
||||
max: 4,
|
||||
},
|
||||
});
|
||||
const chapters: ChapterItem[] = parsedContent('itemref')
|
||||
.toArray()
|
||||
.map(ele => {
|
||||
const href = itemMap[ele.attribs.idref];
|
||||
return {
|
||||
name: tocMap[href],
|
||||
url: `${contentDir}/${href}`,
|
||||
};
|
||||
});
|
||||
BackgroundService.updateNotification({
|
||||
progressBar: {
|
||||
value: 4,
|
||||
max: 4,
|
||||
indeterminate: true,
|
||||
},
|
||||
});
|
||||
const novel: SourceNovel = {
|
||||
name: novelName,
|
||||
author: author,
|
||||
cover: cover,
|
||||
url: contentDir + novelName, // temporary
|
||||
chapters: chapters,
|
||||
};
|
||||
return novel;
|
||||
};
|
||||
|
||||
const importEpubAction = async (taskData?: TaskData) => {
|
||||
try {
|
||||
if (!taskData) {
|
||||
throw new Error('No data provided');
|
||||
}
|
||||
const epubFilePath = RNFS.ExternalCachesDirectoryPath + '/novel.epub';
|
||||
await RNFS.copyFile(taskData.sourceUri, epubFilePath);
|
||||
const epubDirPath = RNFS.ExternalCachesDirectoryPath + '/epub';
|
||||
if (await RNFS.exists(epubDirPath)) {
|
||||
await RNFS.unlink(epubDirPath);
|
||||
}
|
||||
await RNFS.mkdir(epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.IMPORT_EPUB);
|
||||
const novel: SourceNovel = await EpubParser.parse(
|
||||
taskData.epubFilePath,
|
||||
taskData.epubDirPath,
|
||||
).catch(e => {
|
||||
await ZipArchive.unzip(epubFilePath, epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
await sleep(taskData.delay);
|
||||
const novel = await parseNovelAndChapters(epubDirPath).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
const novelId = await insertLocalNovel(
|
||||
novel.name,
|
||||
novel.url,
|
||||
novel.cover,
|
||||
novel.artist,
|
||||
novel.author,
|
||||
).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
const now = dayjs().format('L');
|
||||
const now = dayjs().toISOString();
|
||||
const filePathSet = new Set<string>();
|
||||
if (novel.chapters) {
|
||||
BackgroundService.updateNotification({
|
||||
@@ -171,7 +257,7 @@ const importEpubAction = async (taskData?: TaskData) => {
|
||||
},
|
||||
});
|
||||
const chapter = novel.chapters[i];
|
||||
if (chapter.name == null) {
|
||||
if (!chapter.name) {
|
||||
chapter.name = chapter.url.split(/[\\\/]/).pop() || 'unknown';
|
||||
}
|
||||
const filePaths = await insertLocalChapter(
|
||||
@@ -239,29 +325,19 @@ export const importEpub = async () => {
|
||||
try {
|
||||
const epubFile = await DocumentPicker.getDocumentAsync({
|
||||
type: 'application/epub+zip',
|
||||
copyToCacheDirectory: true,
|
||||
copyToCacheDirectory: false,
|
||||
});
|
||||
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';
|
||||
if (await RNFS.exists(epubDirPath)) {
|
||||
await RNFS.unlink(epubDirPath);
|
||||
}
|
||||
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 },
|
||||
parameters: { delay: 500, sourceUri: epubFile.uri },
|
||||
progressBar: { value: 0, max: 4 },
|
||||
});
|
||||
} catch (e: any) {
|
||||
// BackgroundService.catch cant catch Error from importEpubAction
|
||||
|
||||
Reference in New Issue
Block a user