diff --git a/android/app/src/main/java/com/rajarsheechatterjee/EpubUtil/EpubUtil.java b/android/app/src/main/java/com/rajarsheechatterjee/EpubUtil/EpubUtil.java new file mode 100644 index 000000000..ef58e25b2 --- /dev/null +++ b/android/app/src/main/java/com/rajarsheechatterjee/EpubUtil/EpubUtil.java @@ -0,0 +1,163 @@ +package com.rajarsheechatterjee.EpubUtil; + +import android.util.Xml; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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.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.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Objects; + +public class EpubUtil extends ReactContextBaseJavaModule { + EpubUtil(ReactApplicationContext context){super(context);} + + @NonNull + @Override + public String getName() { + return "EpubUtil"; + } + + private XmlPullParser initParse(File file) throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileInputStream(file), null); + parser.nextTag(); + return parser; + } + + private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { + String result = ""; + if (parser.next() == XmlPullParser.TEXT) { + result = parser.getText(); + parser.nextTag(); + } + return result; + } + + private String cleanUrl(String url){ + if(url != null){ + return url.replaceFirst("#[^.]+?$", ""); + } + return null; + } + + @ReactMethod + public void parseNovelAndChapters(String epubDirPath, Promise promise) { + try { + File containerFile = new File(epubDirPath, "META-INF/container.xml"); + File contentMetaFile = new File(epubDirPath, getContentMetaFilePath(containerFile)); + String contentDir = contentMetaFile.getParent(); + + ReadableMap novel = getNovelMetadata(contentMetaFile, contentDir); + promise.resolve(novel); + } catch (XmlPullParserException | IOException e) { + promise.reject(e.getCause()); + } + } + + private String getContentMetaFilePath(File file) throws XmlPullParserException, IOException { + XmlPullParser parser = initParse(file); + while (parser.next() != XmlPullParser.END_TAG){ + @Nullable String tag = parser.getName(); + if(tag != null && tag.equals("rootfile")) { + return parser.getAttributeValue(null, "full-path"); + } + } + return "OEBPS/content.opf"; // default + } + private ReadableMap getNovelMetadata(File file, String contentDir) throws XmlPullParserException, IOException { + WritableMap novel = new WritableNativeMap(); + WritableArray chapters = new WritableNativeArray(); + XmlPullParser parser = initParse(file); + HashMap refMap = new HashMap<>(); + HashMap tocMap = new HashMap<>(); + File tocFile = new File(contentDir, "toc.ncx"); + if(tocFile.exists()){ + XmlPullParser tocParser = initParse(tocFile); + String label = null; + while (tocParser.next() != XmlPullParser.END_DOCUMENT){ + String tag = tocParser.getName(); + if(tag != null){ + if(tag.equals("text")){ + label = readText(tocParser); + }else if(tag.equals("content")){ + String href = cleanUrl(tocParser.getAttributeValue(null, "src")); + if(href != null){ + tocMap.put(href, label); + } + } + } + } + } + String cover = null; + while (parser.next() != XmlPullParser.END_DOCUMENT){ + @Nullable String tag = parser.getName(); + if(tag != null){ + switch (tag) { + case "item": { + String id = parser.getAttributeValue(null, "id"); + String href = parser.getAttributeValue(null, "href"); + if (id != null) { + refMap.put(id, href); + } + break; + } + case "itemref": { + String idRef = parser.getAttributeValue(null, "idref"); + String href = refMap.get(idRef); + if (href != null) { + WritableMap chapter = new WritableNativeMap(); + chapter.putString("path", contentDir + "/" + href); + String name = tocMap.get(href); + chapter.putString("name", name == null ? href : name); + chapters.pushMap(chapter); + } + break; + } + case "title": + novel.putString("name", readText(parser)); + break; + case "creator": + novel.putString("author", readText(parser)); + break; + case "contributor": + novel.putString("artist", readText(parser)); + break; + case "description": + novel.putString("summary", readText(parser)); + break; + case "meta": + String metaName = parser.getAttributeValue(null, "name"); + if(metaName != null && metaName.equals("cover")){ + cover = parser.getAttributeValue(null, "content"); + } + break; + } + parser.next(); + } + } + if(cover != null){ + String coverPath = contentDir + "/" + refMap.get(cover); + novel.putString("cover", coverPath); + } + novel.putArray("chapters", chapters); + return novel; + } +} diff --git a/android/app/src/main/java/com/rajarsheechatterjee/EpubUtil/EpubUtilPackage.java b/android/app/src/main/java/com/rajarsheechatterjee/EpubUtil/EpubUtilPackage.java new file mode 100644 index 000000000..ba3b82b15 --- /dev/null +++ b/android/app/src/main/java/com/rajarsheechatterjee/EpubUtil/EpubUtilPackage.java @@ -0,0 +1,33 @@ +package com.rajarsheechatterjee.EpubUtil; + +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 com.rajarsheechatterjee.ZipArchive.ZipArchive; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class EpubUtilPackage implements ReactPackage { + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) { + List modules = new ArrayList<>(); + try { + modules.add(new EpubUtil(reactApplicationContext)); + } catch (Exception e) { + throw new RuntimeException(e); + } + return modules; + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) { + return Collections.emptyList(); + } +} diff --git a/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java index 9e320fb5b..1cca556d2 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java @@ -12,6 +12,7 @@ import com.facebook.react.defaults.DefaultReactNativeHost; import com.facebook.soloader.SoLoader; +import com.rajarsheechatterjee.EpubUtil.EpubUtilPackage; import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage; import com.rajarsheechatterjee.TextFile.TextFilePackage; import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage; @@ -38,6 +39,7 @@ protected List getPackages() { packages.add(new VolumeButtonListenerPackage()); packages.add(new ZipArchivePackage()); packages.add(new TextFilePackage()); + packages.add(new EpubUtilPackage()); return packages; } diff --git a/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorModule.java b/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorModule.java index 513a2b00c..a66d7e9d1 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorModule.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorModule.java @@ -23,9 +23,6 @@ public class NavigationBarColorModule extends ReactContextBaseJavaModule { public static final String REACT_CLASS = "NavigationBarColor"; private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity"; - private static final String ERROR_API_LEVEL = "API_LEVEl"; - private static final String ERROR_API_LEVEL_MESSAGE = "Only Android Oreo and above is supported"; - private static ReactApplicationContext reactContext = null; private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar @@ -33,7 +30,6 @@ public class NavigationBarColorModule extends ReactContextBaseJavaModule { public NavigationBarColorModule(ReactApplicationContext context) { super(context); - reactContext = context; } public void setNavigationBarTheme(Activity activity, Boolean light) { @@ -66,61 +62,56 @@ public Map getConstants() { @ReactMethod public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) { final WritableMap map = Arguments.createMap(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (getCurrentActivity() != null) { - try { - final Window window = getCurrentActivity().getWindow(); - runOnUiThread(new Runnable() { - @Override - public void run() { - if (color.equals("transparent") || color.equals("translucent")) { - window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - if (color.equals("transparent")) { - window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); - } else { - window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } - setNavigationBarTheme(getCurrentActivity(), light); - map.putBoolean("success", true); - promise.resolve(map); - return; - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } - if (animated) { - Integer colorFrom = window.getNavigationBarColor(); - Integer colorTo = Color.parseColor(String.valueOf(color)); - //window.setNavigationBarColor(colorTo); - ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animator) { - window.setNavigationBarColor((Integer) animator.getAnimatedValue()); - } - }); - colorAnimation.start(); + if (getCurrentActivity() != null) { + try { + final Window window = getCurrentActivity().getWindow(); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (color.equals("transparent") || color.equals("translucent")) { + window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + if (color.equals("transparent")) { + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } else { - window.setNavigationBarColor(Color.parseColor(String.valueOf(color))); + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); } setNavigationBarTheme(getCurrentActivity(), light); - WritableMap map = Arguments.createMap(); map.putBoolean("success", true); promise.resolve(map); + return; + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); } - }); - } catch (IllegalViewOperationException e) { - map.putBoolean("success", false); - promise.reject("error", e); - } - - } else { - promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE)); - + if (animated) { + Integer colorFrom = window.getNavigationBarColor(); + Integer colorTo = Color.parseColor(color); + //window.setNavigationBarColor(colorTo); + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + window.setNavigationBarColor((Integer) animator.getAnimatedValue()); + } + }); + colorAnimation.start(); + } else { + window.setNavigationBarColor(Color.parseColor(color)); + } + setNavigationBarTheme(getCurrentActivity(), light); + WritableMap map = Arguments.createMap(); + map.putBoolean("success", true); + promise.resolve(map); + } + }); + } catch (IllegalViewOperationException e) { + map.putBoolean("success", false); + promise.reject("error", e); } + } else { - promise.reject(ERROR_API_LEVEL, new Throwable(ERROR_API_LEVEL_MESSAGE)); + promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE)); } } diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts index 211dfed6c..6766cf69f 100644 --- a/src/hooks/persisted/useNovel.ts +++ b/src/hooks/persisted/useNovel.ts @@ -349,7 +349,11 @@ export const useNovel = (novelPath: string, pluginId: string) => { } else { pages = (await getCustomPages(novel.id)).map(c => c.page); } - setPages(pages); + if (pages.length) { + setPages(pages); + } else { + setPages(['1']); + } setNovel(novel); }, []); @@ -401,8 +405,8 @@ export const useNovel = (novelPath: string, pluginId: string) => { ); } setChapters(chapters); + setLoading(false); } - setLoading(false); }, [novel, novelSettings, pageIndex]); useEffect(() => { getNovel(); diff --git a/src/native/EpubUtil.ts b/src/native/EpubUtil.ts new file mode 100644 index 000000000..8e734ee58 --- /dev/null +++ b/src/native/EpubUtil.ts @@ -0,0 +1,8 @@ +import { SourceNovel } from '@plugins/types'; +import { NativeModules } from 'react-native'; +interface EpubUtilInterface { + parseNovelAndChapters: (epubDirPath: string) => Promise; +} +const { EpubUtil } = NativeModules; + +export default EpubUtil as EpubUtilInterface; diff --git a/src/services/epub/import.ts b/src/services/epub/import.ts index db9e5d278..5e3435dc9 100644 --- a/src/services/epub/import.ts +++ b/src/services/epub/import.ts @@ -16,10 +16,9 @@ import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager'; import { MMKVStorage } from '@utils/mmkv/mmkv'; import { BACKGROUND_ACTION, BackgoundAction } from '@services/constants'; import { getString } from '@strings/translations'; -import { ChapterItem, SourceNovel } from '@plugins/types'; -import { load as parseXML } from 'cheerio'; import { showToast } from '@utils/showToast'; import TextFile from '@native/TextFile'; +import EpubUtil from '@native/EpubUtil'; interface TaskData { delay: number; @@ -31,12 +30,17 @@ const insertLocalNovel = ( path: string, cover?: string, author?: string, + artist?: string, + summary?: string, ): Promise => { return new Promise((resolve, reject) => { db.transaction(tx => { tx.executeSql( - "INSERT INTO Novel(name, path, cover, author, pluginId, inLibrary, isLocal) VALUES(?, ?, ?, ?, 'local', 1, 1)", - [name, path, cover || null, author || null], + ` + INSERT INTO + Novel(name, path, pluginId, inLibrary, isLocal) + VALUES(?, ?, 'local', 1, 1)`, + [name, path], async (txObj, resultSet) => { if (resultSet.insertId) { await updateNovelCategoryById(resultSet.insertId, [2]); @@ -49,9 +53,11 @@ const insertLocalNovel = ( await RNFS.moveFile(cover, newCoverPath); } await updateNovelInfo({ - pluginId: LOCAL_PLUGIN_ID, id: resultSet.insertId, + pluginId: LOCAL_PLUGIN_ID, author: author, + artist: artist, + summary: summary, path: NovelDownloadFolder + '/local/' + resultSet.insertId, cover: newCoverPath, name: name, @@ -144,92 +150,6 @@ const insertLocalChapter = ( }); }; -const getParent = (filePath: string) => { - return filePath.replace(/[/\\][^/\\]+$/, ''); -}; - -const parseNovelAndChapters = async ( - epubDirPath: string, -): Promise => { - const containerText = await TextFile.readFile( - `${epubDirPath}/META-INF/container.xml`, - ); - const contentPath = - epubDirPath + '/' + parseXML(containerText)('rootfile').attr('full-path'); - const contentDir = getParent(contentPath); - const contentText = await TextFile.readFile(contentPath); - const parsedContent = parseXML(contentText); - const novelName = parsedContent('dc\\:title').text().trim(); - const author = parsedContent('dc\\:creator').text().trim(); - const description = parsedContent('dc\\:description').text().trim(); - const coverRef = parsedContent('meta[name="cover"]').attr('content'); - let cover = ''; - if (coverRef) { - cover = - contentDir + '/' + parsedContent(`item[id="${coverRef}"]`).attr('href'); - } - BackgroundService.updateNotification({ - progressBar: { - value: 1, - max: 4, - }, - }); - const tocPath = contentDir + '/' + 'toc.ncx'; - const tocMap: Record = {}; - if (await RNFS.exists(tocPath)) { - const tocText = await TextFile.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 = {}; - 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], - path: `${contentDir}/${href}`, - }; - }); - BackgroundService.updateNotification({ - progressBar: { - value: 4, - max: 4, - indeterminate: true, - }, - }); - const novel: SourceNovel = { - name: novelName, - author: author, - cover: cover, - path: contentDir + novelName, // temporary - chapters: chapters, - summary: description, - }; - return novel; -}; - const importEpubAction = async (taskData?: TaskData) => { try { if (!taskData) { @@ -241,25 +161,20 @@ const importEpubAction = async (taskData?: TaskData) => { if (await RNFS.exists(epubDirPath)) { await RNFS.unlink(epubDirPath); } - await RNFS.mkdir(epubDirPath).catch(e => { - throw e; - }); + await RNFS.mkdir(epubDirPath); MMKVStorage.set(BACKGROUND_ACTION, BackgoundAction.IMPORT_EPUB); - await ZipArchive.unzip(epubFilePath, epubDirPath).catch(e => { - throw e; - }); + await ZipArchive.unzip(epubFilePath, epubDirPath); await sleep(taskData.delay); - const novel = await parseNovelAndChapters(epubDirPath).catch(e => { - throw e; - }); + + const novel = await EpubUtil.parseNovelAndChapters(epubDirPath); const novelId = await insertLocalNovel( novel.name, - novel.path, + epubDirPath + novel.name, // temporary novel.cover, novel.author, - ).catch(e => { - throw e; - }); + novel.artist, + novel.summary, + ); const now = dayjs().toISOString(); const filePathSet = new Set(); if (novel.chapters) { @@ -296,7 +211,6 @@ const importEpubAction = async (taskData?: TaskData) => { } } await sleep(taskData.delay); - // move static files const novelDir = NovelDownloadFolder + '/local/' + novelId; BackgroundService.updateNotification({ taskTitle: getString('advancedSettingsScreen.importStaticFiles'),