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:
nyagami
2023-11-10 20:01:09 +07:00
committed by GitHub
parent 5c818be5ab
commit 7fbdd92117
26 changed files with 715 additions and 205 deletions
@@ -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,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,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;