From 7fbdd92117c19a512822522b0c87c3e776804036 Mon Sep 17 00:00:00 2001 From: nyagami Date: Fri, 10 Nov 2023 20:01:09 +0700 Subject: [PATCH] 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 --- .github/workflows/build.yml | 7 +- .../EpubParser/EpubParser.java | 162 +++++++++++ .../EpubParser/EpubParserPackage.java | 32 +++ .../LNReader/MainActivity.java | 1 - .../LNReader/MainApplication.java | 18 +- .../NavigationBarColorModule.java | 5 +- .../NavigationBarColorPackage.java | 2 +- babel.config.js | 1 + src/database/db.ts | 4 +- src/database/queries/CategoryQueries.ts | 2 +- src/database/queries/LibraryQueries.ts | 2 +- src/database/queries/NovelQueries.ts | 41 ++- src/database/tables/CategoryTable.ts | 7 +- src/database/tables/NovelTable.ts | 3 +- src/database/types/index.ts | 1 + src/native/epubParser.ts | 8 + src/plugins/pluginManager.ts | 2 +- src/redux/novel/novel.actions.js | 4 +- src/screens/novel/NovelScreen.js | 270 +++++++++--------- src/screens/novel/components/ChapterItem.tsx | 25 +- .../NovelScreenButtonGroup.tsx | 40 +-- .../settings/SettingsAdvancedScreen.js | 6 +- src/services/epub/import.ts | 268 +++++++++++++++++ src/services/migrate/migrateNovel.ts | 4 +- src/services/updates/updates.js | 4 +- tsconfig.json | 1 + 26 files changed, 715 insertions(+), 205 deletions(-) create mode 100644 android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParser.java create mode 100644 android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParserPackage.java create mode 100644 src/native/epubParser.ts create mode 100644 src/services/epub/import.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 551e99ab..ad8dff9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParser.java b/android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParser.java new file mode 100644 index 00000000..08240570 --- /dev/null +++ b/android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParser.java @@ -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; + } +} diff --git a/android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParserPackage.java b/android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParserPackage.java new file mode 100644 index 00000000..8bc80e3e --- /dev/null +++ b/android/app/src/main/java/com/rajarsheechatterjee/EpubParser/EpubParserPackage.java @@ -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 createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) { + List modules = new ArrayList<>(); + try { + modules.add(new EpubParser(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/MainActivity.java b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainActivity.java index 67e2386f..55aad11d 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainActivity.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainActivity.java @@ -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; 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 dd907439..f3b3b54f 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.java @@ -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 getPackages() { - @SuppressWarnings("UnnecessaryLocalVariable") List 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) { 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 6fcd4e12..513a2b00 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorModule.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorModule.java @@ -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; diff --git a/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorPackage.java b/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorPackage.java index 89cc14d5..ff4738f9 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorPackage.java +++ b/android/app/src/main/java/com/rajarsheechatterjee/NavigationBarColor/NavigationBarColorPackage.java @@ -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; diff --git a/babel.config.js b/babel.config.js index d13ea66e..75f3b991 100644 --- a/babel.config.js +++ b/babel.config.js @@ -18,6 +18,7 @@ module.exports = function (api) { '@utils': './src/utils', '@theme': './src/theme', '@navigators': './src/navigators', + '@native': './src/native', }, }, ], diff --git a/src/database/db.ts b/src/database/db.ts index 43250bf3..b4056b04 100644 --- a/src/database/db.ts +++ b/src/database/db.ts @@ -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, [], () => { diff --git a/src/database/queries/CategoryQueries.ts b/src/database/queries/CategoryQueries.ts index 47afcc06..055b6c93 100644 --- a/src/database/queries/CategoryQueries.ts +++ b/src/database/queries/CategoryQueries.ts @@ -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 => { diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index 3f99212e..67f6f979 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -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 => { diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts index 53344b12..c72045a5 100644 --- a/src/database/queries/NovelQueries.ts +++ b/src/database/queries/NovelQueries.ts @@ -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 => { @@ -51,7 +53,7 @@ export const getNovel = async (novelUrl: string): Promise => { 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) => { db.transaction(tx => { tx.executeSql( @@ -124,7 +130,27 @@ export const removeNovelsFromLibrary = (novelIds: Array) => { showToast('Removed from Library'); }; -export const deleteNovelCache = () => { +export const getCachedNovels = (): Promise => { + 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 => { 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 => diff --git a/src/database/tables/CategoryTable.ts b/src/database/tables/CategoryTable.ts index b42689da..27e6ddfe 100644 --- a/src/database/tables/CategoryTable.ts +++ b/src/database/tables/CategoryTable.ts @@ -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)'; diff --git a/src/database/tables/NovelTable.ts b/src/database/tables/NovelTable.ts index ba28a32e..c3aceaad 100644 --- a/src/database/tables/NovelTable.ts +++ b/src/database/tables/NovelTable.ts @@ -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 ); `; diff --git a/src/database/types/index.ts b/src/database/types/index.ts index 08519f52..c13cc1b3 100644 --- a/src/database/types/index.ts +++ b/src/database/types/index.ts @@ -11,6 +11,7 @@ export interface NovelInfo { status?: NovelStatus; genres?: string; inLibrary: number; + isLocal: number; } export interface LibraryNovelInfo extends NovelInfo { diff --git a/src/native/epubParser.ts b/src/native/epubParser.ts new file mode 100644 index 00000000..ee416d1a --- /dev/null +++ b/src/native/epubParser.ts @@ -0,0 +1,8 @@ +import { SourceNovel } from '@plugins/types'; +import { NativeModules } from 'react-native'; +interface EpubParserInterface { + parse: (epubFilePath: string, epubDirPath: string) => Promise; +} +const { EpubParser } = NativeModules; + +export default EpubParser as EpubParserInterface; diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index db6fcb14..7626459b 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -149,7 +149,7 @@ const fetchPlugins = async () => { const githubBranch = 'plugins'; const availablePlugins: Record> = 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; }; diff --git a/src/redux/novel/novel.actions.js b/src/redux/novel/novel.actions.js index 09fe1f47..4e634082 100644 --- a/src/redux/novel/novel.actions.js +++ b/src/redux/novel/novel.actions.js @@ -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. */ diff --git a/src/screens/novel/NovelScreen.js b/src/screens/novel/NovelScreen.js index ee1bd5bc..fd2ee979 100644 --- a/src/screens/novel/NovelScreen.js +++ b/src/screens/novel/NovelScreen.js @@ -294,6 +294,7 @@ const Novel = ({ route, navigation }) => { const renderItem = ({ item: it, index }) => ( { }} onPress={() => showJumpToChapterModal(true)} /> - - showDownloadMenu(false)} - anchor={ - showDownloadMenu(true)} - /> - } - contentStyle={{ backgroundColor: theme.surface2 }} - > - { - dispatch( - downloadAllChaptersAction(novel.pluginId, novel.url, [ - chapters.find( - chapter => - !!chapter.unread === true && - !!chapter.isDownloaded === false, - ), - ]), - ); - showDownloadMenu(false); - }} - /> - { - dispatch( - downloadAllChaptersAction( - novel.pluginId, - novel.url, - chapters - .filter( - chapter => - !!chapter.unread === true && - !!chapter.idDownloaded === false, - ) - .slice(0, 5), - ), - ); - showDownloadMenu(false); - }} - /> - { - dispatch( - downloadAllChaptersAction( - novel.pluginId, - novel.url, - chapters - .filter( - chapter => - !!chapter.unread === true && - !!chapter.Downloaded === false, - ) - .slice(0, 10), - ), - ); - showDownloadMenu(false); - }} - /> - { - downloadCustomChapterModal.setTrue(); - showDownloadMenu(false); - }} - /> - { - dispatch( - downloadAllChaptersAction( - novel.pluginId, - novel.url, - chapters.filter(chapter => !!chapter.unread === true), - ), - ); - showDownloadMenu(false); - }} - /> - { - dispatch( - downloadAllChaptersAction( - novel.pluginId, - novel.url, - chapters, - ), - ); - showDownloadMenu(false); - }} - /> - - dispatch(deleteAllChaptersAction(pluginId, chapters)) + {!novel.isLocal && ( + showDownloadMenu(false)} + anchor={ + showDownloadMenu(true)} + /> } - /> - + contentStyle={{ backgroundColor: theme.surface2 }} + > + { + dispatch( + downloadAllChaptersAction(novel.pluginId, novel.url, [ + chapters.find( + chapter => + !!chapter.unread === true && + !!chapter.isDownloaded === false, + ), + ]), + ); + showDownloadMenu(false); + }} + /> + { + dispatch( + downloadAllChaptersAction( + novel.pluginId, + novel.url, + chapters + .filter( + chapter => + !!chapter.unread === true && + !!chapter.idDownloaded === false, + ) + .slice(0, 5), + ), + ); + showDownloadMenu(false); + }} + /> + { + dispatch( + downloadAllChaptersAction( + novel.pluginId, + novel.url, + chapters + .filter( + chapter => + !!chapter.unread === true && + !!chapter.Downloaded === false, + ) + .slice(0, 10), + ), + ); + showDownloadMenu(false); + }} + /> + { + downloadCustomChapterModal.setTrue(); + showDownloadMenu(false); + }} + /> + { + dispatch( + downloadAllChaptersAction( + novel.pluginId, + novel.url, + chapters.filter( + chapter => !!chapter.unread === true, + ), + ), + ); + showDownloadMenu(false); + }} + /> + { + dispatch( + downloadAllChaptersAction( + novel.pluginId, + novel.url, + chapters, + ), + ); + showDownloadMenu(false); + }} + /> + + dispatch(deleteAllChaptersAction(pluginId, chapters)) + } + /> + + )} void; showProgressPercentage?: (chapter: ChapterInfo) => any; left?: ReactNode; - + isLocal: boolean; isUpdateCard?: boolean; novelName: string; } @@ -43,6 +43,7 @@ const ChapterItem: React.FC = ({ onSelectLongPress, navigateToChapter, showProgressPercentage, + isLocal, left, isUpdateCard, novelName, @@ -124,16 +125,18 @@ const ChapterItem: React.FC = ({ - + {!isLocal && ( + + )} ); }; diff --git a/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx b/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx index 888a6b81..4295f1ad 100644 --- a/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx +++ b/src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx @@ -23,7 +23,7 @@ const NovelScreenButtonGroup: React.FC = ({ 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 = ({ ) : null} - {inLibrary ? ( + {inLibrary && !isLocal && ( = ({ - ) : null} - - - - - WebView - - - + )} + {!isLocal && ( + + + + + WebView + + + + )} { const theme = useTheme(); @@ -72,6 +73,7 @@ const AdvancedSettings = ({ navigation }) => { theme={theme} /> + {