Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64ef4bae1f | |||
| 88a70cbcea | |||
| cfcc4bd875 | |||
| 02b058dac2 | |||
| 36cb7fc391 | |||
| 8613a088d2 | |||
| e600828bc2 | |||
| e4364aea7c | |||
| df8da48c32 | |||
| a83b4b1a32 | |||
| 8a85f4eaf1 | |||
| 2c1cce7fbe | |||
| e4ca751fb0 | |||
| c3543f8186 | |||
| 9253249ef6 | |||
| e0e593bf9f | |||
| a298ed81d7 | |||
| 60cf358e57 | |||
| 4548cdf8db | |||
| ee9391bd9f | |||
| f069a67660 | |||
| 7ee843f7b8 | |||
| 757c1eae5f | |||
| e8fe0fdc83 | |||
| 5df5a2ccd2 | |||
| 3f6bcb2a8f | |||
| f8fef4cb2c | |||
| c15144f633 | |||
| a0096cdacb | |||
| 7afe230ec3 | |||
| 4a49f32823 | |||
| 925855a0dd | |||
| 6f4fdd22dd | |||
| 918edc4362 | |||
| e955069190 | |||
| 5037fa693a | |||
| 527289372b | |||
| c1cc32bcf1 | |||
| 3b2ee3d78b | |||
| 929b183478 | |||
| f53b0eac80 | |||
| 8258059adb | |||
| 66c9b80374 | |||
| 490c9ee495 | |||
| 4cb6251409 | |||
| 6349c26a49 | |||
| f7f02dc179 | |||
| 28b65e35d2 | |||
| be2cca88af | |||
| 0c42e93238 | |||
| 895f7c7fbf | |||
| de816e5de4 | |||
| fbc128c786 | |||
| 8e0ec4ccf8 | |||
| e092ad9ba8 | |||
| 5b8cfebe3d | |||
| e760bd1a9b | |||
| d260eb1b5e | |||
| d59f95f13a | |||
| 0d9da26eff | |||
| a769110c70 | |||
| 71ea318503 | |||
| 59d5f08c83 | |||
| 36230cebea | |||
| cbee29ea85 | |||
| 9b5a44fe59 | |||
| 9f87edaa7c | |||
| e976c9793c |
@@ -45,7 +45,7 @@ body:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.1.15"
|
||||
Example: "1.1.18"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -88,7 +88,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.15](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.1.18](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -34,7 +34,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.14](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.1.18](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -8,7 +8,10 @@ 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'
|
||||
|
||||
@@ -12,13 +12,14 @@ import { Provider as PaperProvider } from 'react-native-paper';
|
||||
import { Provider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/lib/integration/react';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
|
||||
import { createDatabase } from '@database/db';
|
||||
import { persistor, store } from '@redux/store';
|
||||
import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import NewUpdateDialog from '@components/NewUpdateDialog';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
|
||||
// Initialize trackers
|
||||
import './src/services/Trackers/index';
|
||||
@@ -49,6 +50,7 @@ const App = () => {
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<NewUpdateDialog />
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Common Problems with First Setup
|
||||
|
||||
### SDK Location not found. Define location with an ANDROID_SDK_ROOT environment variable. (expo modules error)
|
||||
|
||||
If you're on windows, you'll need to edit your system (system wide)/user (just you) environment variables. You'll want to add an environment variable named **ANDROID_SDK_ROOT** (legacy env variable name) and **ANDROID_HOME** that points to your android studio's sdk location. It'll most likely look something like this:
|
||||
|
||||
C:\Users\\**Username**\AppData\Local\Android\Sdk (copy paste this into the box)
|
||||
|
||||
Delete your node_modules folder, invalidate caches and restart in android studio and restart your machine. Follow the steps again in the [setup section](./CONTRIBUTING.md#setup).
|
||||
|
||||
### '.' is not recognized as an internal or external command, operable program or batch file. (npm run buildRelease error)
|
||||
|
||||
Run this line in the terminal instead: ```cd android ; ./gradlew clean ; ./gradlew assembleRelease```
|
||||
+9
-4
@@ -8,9 +8,10 @@ After forking to your own github org or account, do the following steps to get s
|
||||
|
||||
```bash
|
||||
# prerequisites
|
||||
node --version <= 16.13.1 (for version management, get nvm [recommended])
|
||||
java sdk --version <= 11 (for version management, get jenv [optional])
|
||||
android sdk (https://developer.android.com/studio)
|
||||
node --version <= 16.13.1 (for version management, get nvm [recommended])
|
||||
java sdk --version <= 17 (for version management, get jenv [optional])
|
||||
android sdk (https://developer.android.com/studio)
|
||||
python --version <= 3.11
|
||||
|
||||
# clone your fork to your local machine
|
||||
git clone https://github.com/<your-account-name>/lnreader.git
|
||||
@@ -25,6 +26,10 @@ npm install
|
||||
npm run buildRelease
|
||||
```
|
||||
|
||||
### Common Problems with First Setup
|
||||
|
||||
Please refer to this [file](./COMMON_ERRORS.md)
|
||||
|
||||
### Developing on Android
|
||||
|
||||
You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
|
||||
@@ -56,4 +61,4 @@ codebase, however you can always check to see if the source code is compliant by
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
```
|
||||
@@ -1,4 +1,5 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.chaquo.python"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
@@ -133,7 +134,7 @@ def reactNativeArchitectures() {
|
||||
ext {
|
||||
versionMajor = 1
|
||||
versionMinor = 1
|
||||
versionPatch = 15
|
||||
versionPatch = 18
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -146,9 +147,23 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Generated version code. Supports versions up to 1024.1024.2048
|
||||
versionCode ((((versionMajor << 10) | versionMinor) << 11) | versionPatch)
|
||||
versionCode((((versionMajor << 10) | versionMinor) << 11) | versionPatch)
|
||||
versionName "$versionMajor.$versionMinor.$versionPatch"
|
||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
||||
}
|
||||
python {
|
||||
//buildPython "C:/Users/kijij/AppData/Local/Programs/Python/Python39/python.exe" //don't believe you need this
|
||||
pip {
|
||||
install "-r", "./requirements.txt"
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
python.srcDirs = ["./src/main/python"]
|
||||
}
|
||||
}
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
lxml==4.6.3
|
||||
@@ -12,7 +12,7 @@ import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
|
||||
import com.rajarsheechatterjee.EpubParser.EpubParserPackage;
|
||||
import expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new com.rajarsheechatterjee.LNReader.NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
packages.add(new EpubParserPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.rajarsheechatterjee.EpubParser;
|
||||
|
||||
import com.chaquo.python.PyObject;
|
||||
import com.chaquo.python.Python;
|
||||
import com.chaquo.python.android.AndroidPlatform;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.BaseActivityEventListener;
|
||||
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.modules.core.DeviceEventManagerModule;
|
||||
import java.io.File;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class EpubParser extends ReactContextBaseJavaModule implements ActivityEventListener{
|
||||
private static final int REQUEST_DIRECTORY=100; //constant request code dunno if will use it
|
||||
private static String epubPath;
|
||||
private static Promise promise;
|
||||
private final EpubParserActivityEventListener activityEventListener;
|
||||
private static ReactApplicationContext appContext = null;
|
||||
|
||||
public EpubParser(ReactApplicationContext context){
|
||||
super(context);
|
||||
appContext = context;
|
||||
activityEventListener = new EpubParserActivityEventListener(context, this);
|
||||
context.addActivityEventListener(activityEventListener);
|
||||
if (!Python.isStarted()) {
|
||||
// Initialize Python with the Chaquopy SDK
|
||||
Python.start(new AndroidPlatform(context));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
if (activity != null && requestCode == REQUEST_DIRECTORY && resultCode == Activity.RESULT_OK) {
|
||||
Uri uri = data.getData();
|
||||
String epubPath = getFilePathFromUri(uri);
|
||||
|
||||
Python py = Python.getInstance();
|
||||
PyObject epubParser = py.getModule("epubParser");
|
||||
|
||||
if (epubPath != null && epubPath.toLowerCase().endsWith(".epub")) {
|
||||
PyObject savePath = epubParser.callAttr("parseEpub", epubPath, "/data/data/com.rajarsheechatterjee.LNReader/files/");
|
||||
promise.resolve(savePath.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(){
|
||||
return "EpubParser";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void openDirectory(Promise promise){
|
||||
this.promise = promise;
|
||||
try{
|
||||
Intent intent=new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*"); //there's an extra backslash in there
|
||||
final Activity activity = getCurrentActivity();
|
||||
activity.startActivityForResult(intent,REQUEST_DIRECTORY);
|
||||
} catch (Exception e){
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getFilePathFromUri(Uri uri){
|
||||
String filePath=null;
|
||||
if(uri!=null){
|
||||
ContentResolver resolver = getCurrentActivity().getContentResolver();
|
||||
Cursor cursor=resolver.query(uri,null,null,null,null);
|
||||
if(cursor!=null&&cursor.moveToFirst()){
|
||||
int index=cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
String fileName=cursor.getString(index);
|
||||
cursor.close();
|
||||
|
||||
File file=new File(getReactApplicationContext().getCacheDir(),fileName);
|
||||
filePath=file.getAbsolutePath();
|
||||
|
||||
try{
|
||||
InputStream inputStream=resolver.openInputStream(uri);
|
||||
OutputStream outputStream=new FileOutputStream(file);
|
||||
byte[]buffer=new byte[4096];
|
||||
int bytesRead;
|
||||
while((bytesRead=inputStream.read(buffer))!=-1){
|
||||
outputStream.write(buffer,0,bytesRead);
|
||||
}
|
||||
outputStream.close();
|
||||
inputStream.close();
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
// Handle new intents if needed
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.rajarsheechatterjee.EpubParser;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.BaseActivityEventListener;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
public class EpubParserActivityEventListener extends BaseActivityEventListener {
|
||||
private final ReactApplicationContext reactContext;
|
||||
private final EpubParser epubParser;
|
||||
|
||||
public EpubParserActivityEventListener(ReactApplicationContext reactContext, EpubParser epubParser) {
|
||||
this.reactContext = reactContext;
|
||||
this.epubParser = epubParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
epubParser.onActivityResult(activity, requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.rajarsheechatterjee.EpubParser;
|
||||
|
||||
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 {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<NativeModule>();
|
||||
|
||||
modules.add(new EpubParser(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import json
|
||||
import os.path
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
from lxml import etree
|
||||
|
||||
# import shutil
|
||||
# from PIL import Image #not necessary only for debugging
|
||||
|
||||
'''export interface SourceNovel {
|
||||
url: string; //must be absoulute
|
||||
name: string;
|
||||
cover?: string;
|
||||
genres?: string;
|
||||
summary?: string;
|
||||
author?: string;
|
||||
artist?: string;
|
||||
status?: string;
|
||||
chapters?: ChapterItem[];
|
||||
}
|
||||
export interface ChapterItem {
|
||||
name: string;
|
||||
url: string; //must be absoulute
|
||||
releaseTime?: string;
|
||||
}'''
|
||||
|
||||
|
||||
class ChapterItem:
|
||||
def __init__(self, name, url, releaseTime):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.releaseTime = releaseTime # perhaps epub creation date?
|
||||
|
||||
|
||||
namespaces = {
|
||||
'calibre': 'http://calibre.kovidgoyal.net/2009/metadata',
|
||||
'dc': 'http://purl.org/dc/elements/1.1/',
|
||||
'dcterms': 'http://purl.org/dc/terms/',
|
||||
'opf': 'http://www.idpf.org/2007/opf',
|
||||
'ncx': 'http://www.daisy.org/z3986/2005/ncx/',
|
||||
'u': 'urn:oasis:names:tc:opendocument:xmlns:container',
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'xhtml': 'http://www.w3.org/1999/xhtml'
|
||||
}
|
||||
|
||||
|
||||
def getContentOPF(epub_path):
|
||||
with zipfile.ZipFile(epub_path) as z:
|
||||
text = z.read('META-INF/container.xml') # get container xml to get content
|
||||
tree = etree.fromstring(text)
|
||||
rootfile_path = tree.xpath('/u:container/u:rootfiles/u:rootfile',
|
||||
namespaces=namespaces)[0].get('full-path')
|
||||
tree = etree.fromstring(z.read(rootfile_path))
|
||||
return tree
|
||||
|
||||
|
||||
def cleanTitle(dir_name):
|
||||
# cleans a string so that it is valid for directories
|
||||
pattern = r'[<>:"/\\|?*\x00-\x1F]'
|
||||
# Remove any disallowed characters from the directory name string
|
||||
clean_dir_name = re.sub(pattern, '', dir_name)
|
||||
return clean_dir_name
|
||||
|
||||
|
||||
def getContent(epub_path, dest_dir):
|
||||
extensions = ['.html', '.htm', '.xhtml', '.css', '.png', '.jpeg', '.jpg', '.gif', '.ncx',
|
||||
'.opf'] # still need the content.opf file
|
||||
with zipfile.ZipFile(epub_path) as z:
|
||||
for filename in z.namelist():
|
||||
if (filename.endswith('opf')):
|
||||
contentOPF = filename # search for opf file
|
||||
break
|
||||
|
||||
tree = etree.XML(z.read(contentOPF))
|
||||
title = tree.find('.//dc:title', namespaces=namespaces).text
|
||||
cleanedTitle = cleanTitle(title)
|
||||
dir = dest_dir + 'convertedEpubs/' + cleanedTitle # temporary directory for saving
|
||||
|
||||
for fileInfo in z.infolist():
|
||||
if any(fileInfo.filename.endswith(ext) for ext in extensions):
|
||||
z.extract(fileInfo, path=dir)
|
||||
|
||||
return dir
|
||||
|
||||
|
||||
def treeFindsAll(queryString, XMLtree):
|
||||
list = XMLtree.findall(queryString, namespaces=namespaces)
|
||||
query = ''
|
||||
if len(list) == 0:
|
||||
query = 'N/A'
|
||||
return query
|
||||
for item in list:
|
||||
query += (item.text + ' ')
|
||||
return query
|
||||
|
||||
|
||||
class SourceNovel:
|
||||
def __init__(self, url, name, cover, genres, summary, author, artist, chapters):
|
||||
self.url = url # file location?
|
||||
self.name = name
|
||||
self.cover = cover
|
||||
self.genres = genres
|
||||
self.summary = summary
|
||||
self.author = author
|
||||
self.artist = artist
|
||||
self.status = 'local'
|
||||
self.chapters = chapters
|
||||
|
||||
|
||||
def getChapters(z: zipfile.ZipFile, opf_tree, dir_path):
|
||||
isEPUB2 = False
|
||||
chapters = []
|
||||
|
||||
for filename in z.namelist():
|
||||
if filename.endswith('toc.ncx'):
|
||||
isEPUB2 = True
|
||||
chpts_data_filename = filename
|
||||
break
|
||||
|
||||
if isEPUB2:
|
||||
name_by_path = {}
|
||||
tree = etree.XML(z.read(chpts_data_filename))
|
||||
|
||||
for nav_point in tree.xpath('.//ncx:navPoint', namespaces=namespaces):
|
||||
name = nav_point.find('.//ncx:text', namespaces=namespaces).text
|
||||
path = nav_point.find('.//ncx:content', namespaces=namespaces).attrib['src']
|
||||
|
||||
chapter_path = path[:path.index('#')] if '#' in path else path
|
||||
if chapter_path not in name_by_path:
|
||||
name_by_path[chapter_path] = name
|
||||
|
||||
chapters_elements = opf_tree.xpath('//opf:spine//opf:itemref', namespaces=namespaces)
|
||||
for i, chapter_el in enumerate(chapters_elements):
|
||||
id = chapter_el.get('idref')
|
||||
item = opf_tree.find(f".//opf:manifest/opf:item[@id='{id}']", namespaces=namespaces)
|
||||
path = item.attrib['href']
|
||||
chapter_has_name = path in name_by_path
|
||||
print(chapter_has_name)
|
||||
if chapter_has_name:
|
||||
chapter_name = name_by_path[path]
|
||||
if chapter_name.isnumeric():
|
||||
chapterName = f'Chapter {chapter_name}'
|
||||
else:
|
||||
chapterName = chapter_name
|
||||
else:
|
||||
chapterName = id.split('_')[0]
|
||||
|
||||
chapters.append({
|
||||
'name': chapterName,
|
||||
'path': dir_path + '/' + path if len(dir_path) > 0 else path,
|
||||
})
|
||||
else:
|
||||
# EPUB3 uses nav.xhtml instead but I don't have an example rn
|
||||
# For now it will list all files without their corresponding chapter names
|
||||
chapters_elements = opf_tree.xpath('//opf:spine//opf:itemref', namespaces=namespaces)
|
||||
for i, chapter_el in enumerate(chapters_elements):
|
||||
id = chapter_el.get('idref')
|
||||
item = opf_tree.find(f".//opf:manifest/opf:item[@id='{id}']", namespaces=namespaces)
|
||||
path = item.attrib['href']
|
||||
|
||||
chapters.append({
|
||||
'name': f'EPUB3 - Chapter {i + 1}',
|
||||
'path': dir_path + '/' + path if len(dir_path) > 0 else path,
|
||||
})
|
||||
|
||||
return chapters
|
||||
|
||||
|
||||
def getMetadata(epub_path, save_path):
|
||||
with zipfile.ZipFile(epub_path) as z:
|
||||
for filename in z.namelist():
|
||||
if (filename.endswith('opf')):
|
||||
contentOPF = filename # search for opf file
|
||||
break
|
||||
print('content.opf', contentOPF)
|
||||
tree = etree.XML(z.read(contentOPF))
|
||||
|
||||
title = tree.find('.//dc:title', namespaces=namespaces).text
|
||||
# print('title', title)
|
||||
|
||||
cover = getCover(epub_path)
|
||||
# print('cover', cover)
|
||||
|
||||
authors = ''
|
||||
for author in tree.findall('.//dc:creator', namespaces=namespaces):
|
||||
authors += (author.text + ' ')
|
||||
# print('authors', authors)
|
||||
|
||||
genre = treeFindsAll('.//dc:subject', tree)
|
||||
# print('genre', genre)
|
||||
|
||||
summary = treeFindsAll('.//dc:description', tree)
|
||||
# print('summary', summary)
|
||||
|
||||
artist = '' # this will be included with author as epub files do not need to have an artist role and distinction is probably not necessary
|
||||
chapters = getChapters(z, tree, os.path.dirname(contentOPF))
|
||||
print('chapters', chapters)
|
||||
|
||||
return {
|
||||
'url': save_path,
|
||||
'title': title,
|
||||
'cover': cover,
|
||||
'genre': genre,
|
||||
'summary': summary,
|
||||
'authors': authors.strip(),
|
||||
'artist': artist,
|
||||
'chapters': chapters,
|
||||
}
|
||||
|
||||
|
||||
def getCover(epub_path):
|
||||
with zipfile.ZipFile(epub_path) as z:
|
||||
for filename in z.namelist():
|
||||
if (filename.endswith('opf')):
|
||||
contentOPF = filename # search for opf file
|
||||
|
||||
tree = etree.XML(z.read(contentOPF))
|
||||
coverHREF = None
|
||||
|
||||
try:
|
||||
coverID = tree.xpath("//opf:metadata/opf:meta[@name='cover']", namespaces=namespaces)[0].get('content')
|
||||
# print('coverID 2', coverID) # now we know where the cover image is located
|
||||
coverHREF = tree.xpath("//opf:manifest/opf:item[@id='" + coverID + "']", namespaces=namespaces)[0].get(
|
||||
'href')
|
||||
|
||||
except IndexError: # not an EPUB 2.0
|
||||
# print('EPUB 2 failure')
|
||||
pass
|
||||
|
||||
if not coverHREF: # try EPUB 3.0
|
||||
try:
|
||||
coverHREF = tree.xpath("//opf:manifest/opf:item[@properties='cover-image']", namespaces=namespaces)[
|
||||
0].get('href')
|
||||
except IndexError:
|
||||
# print('EPUB 3 failure')
|
||||
pass
|
||||
elif not coverHREF: # some EPUBs don't explicitly declare cover images
|
||||
try:
|
||||
coverID = tree.xpath("//opf:spine/open:itemref[@idref='cover']", namespaces=namespaces)[0].get('idref')
|
||||
temp = tree.xpath("//opf:manifest/opf:item[@id='" + coverID + "']", namespaces=namespaces)[0].get(
|
||||
'href')
|
||||
|
||||
tree = etree.fromstring(z.read(temp))
|
||||
coverHREF = tree.xpath('//xhtml:img', namespaces=namespaces)[0].get('src')
|
||||
except IndexError:
|
||||
print('Edge case failure')
|
||||
elif not coverHREF:
|
||||
# print('No cover found')
|
||||
return None
|
||||
|
||||
coverPath = coverHREF.replace('\\', '/')
|
||||
# print('coverPath', coverPath)
|
||||
|
||||
return coverPath if os.path.dirname(contentOPF) == 0 else os.path.dirname(contentOPF) + '/' + coverPath
|
||||
|
||||
|
||||
def dumpMetaData(metaData, dest_dir):
|
||||
dir = dest_dir + 'convertedEpubs/' + cleanTitle(metaData.get('title')) + '/'
|
||||
with open(dir + 'metadata.json', 'w') as fileout:
|
||||
json.dump(metaData, fileout)
|
||||
|
||||
|
||||
def parseEpub(epub_path, dest_dir):
|
||||
save_path = getContent(epub_path, dest_dir) # simultaneously writes and saves content to a variable
|
||||
metaData = getMetadata(epub_path, save_path)
|
||||
dumpMetaData(metaData, dest_dir)
|
||||
return save_path
|
||||
@@ -20,12 +20,13 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://chaquo.com/maven" }
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.1.2")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
|
||||
classpath ("com.chaquo.python:gradle:14.0.2")
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
||||
Generated
+718
-709
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.15",
|
||||
"version": "1.1.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error: string;
|
||||
onRetry?: () => void;
|
||||
retryIconColor?: string;
|
||||
actions?: Array<{
|
||||
iconName: string;
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error }) => {
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, actions }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.icon, { color: theme.outline }]}>ಥ_ಥ</Text>
|
||||
<Text style={[styles.error, { color: theme.outline }]}>{error}</Text>
|
||||
{actions?.length ? (
|
||||
<View style={styles.actionsCtn}>
|
||||
{actions.map(action => (
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={action.onPress}
|
||||
style={styles.buttonCtn}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={action.iconName}
|
||||
size={24}
|
||||
color={theme.outline}
|
||||
/>
|
||||
<Text style={{ color: theme.outline }}>{action.title}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -36,4 +60,21 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
buttonWrapper: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 4,
|
||||
flexDirection: 'row',
|
||||
flex: 1 / 3,
|
||||
},
|
||||
buttonCtn: {
|
||||
flex: 1,
|
||||
paddingVertical: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionsCtn: {
|
||||
marginTop: 20,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ const InfoItem = ({
|
||||
theme,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
icon?: string;
|
||||
theme: ThemeColors;
|
||||
}) => (
|
||||
<View style={styles.infoCtn}>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { StyleSheet, View, Text, Pressable } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
import color from 'color';
|
||||
|
||||
const ListView = ({
|
||||
@@ -31,6 +33,7 @@ const ListView = ({
|
||||
<FastImage
|
||||
source={{
|
||||
uri: item.novelCover,
|
||||
headers: { 'User-Agent': defaultUserAgentString },
|
||||
}}
|
||||
style={[styles.extensionIcon, inLibraryBadge && { opacity: 0.5 }]}
|
||||
/>
|
||||
|
||||
@@ -1,52 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View, Text, Dimensions } from 'react-native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { ScrollView, StyleSheet, View, Text, Dimensions } from 'react-native';
|
||||
import { Portal, Modal } from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import Button from './Button/Button';
|
||||
|
||||
import { Button } from '@components';
|
||||
|
||||
import { useBoolean, useReleaseChecker, useTheme } from '@hooks';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
interface NewUpdateDialogProps {
|
||||
newVersion: {
|
||||
tag_name: string;
|
||||
body: string;
|
||||
downloadUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
|
||||
const NewUpdateDialog: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const newRelease = useReleaseChecker();
|
||||
|
||||
const {
|
||||
value: isVisible,
|
||||
setFalse: hideModal,
|
||||
setValue: setModalVisible,
|
||||
} = useBoolean();
|
||||
|
||||
useEffect(() => {
|
||||
setModalVisible(Boolean(newRelease));
|
||||
}, [newRelease]);
|
||||
|
||||
const modalHeight = Dimensions.get('window').height / 2;
|
||||
|
||||
if (!newRelease) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={newUpdateDialog}
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
visible={isVisible}
|
||||
onDismiss={hideModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.overlay3 },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
|
||||
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
|
||||
{`${getString('common.newUpdateAvailable')} ${newRelease?.tagName}`}
|
||||
</Text>
|
||||
<ScrollView style={{ height: modalHeight }}>
|
||||
{newVersion.body}
|
||||
<Text style={{ color: theme.onSurface }}>
|
||||
{newRelease?.changelog}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
<View style={styles.buttonCtn}>
|
||||
<Button
|
||||
title={getString('common.cancel')}
|
||||
onPress={() => showNewUpdateDialog(false)}
|
||||
/>
|
||||
<Button title={getString('common.cancel')} onPress={hideModal} />
|
||||
<Button
|
||||
title={getString('common.install')}
|
||||
onPress={() => Linking.openURL(newVersion.downloadUrl)}
|
||||
onPress={() => Linking.openURL(newRelease?.downloadUrl)}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
@@ -67,7 +72,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
body: {},
|
||||
buttonCtn: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 16,
|
||||
|
||||
@@ -20,6 +20,8 @@ import { SourceNovelItem } from 'src/sources/types';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
|
||||
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
interface NovelCoverProps {
|
||||
item: LibraryNovelInfo;
|
||||
onPress: () => void;
|
||||
@@ -75,9 +77,9 @@ const NovelCover: React.FC<NovelCoverProps> = ({
|
||||
},
|
||||
styles.standardNovelCover,
|
||||
isSelected && {
|
||||
backgroundColor: theme.primary,
|
||||
} &&
|
||||
styles.selectedNovelCover,
|
||||
backgroundColor: theme.primary,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
@@ -108,7 +110,10 @@ const NovelCover: React.FC<NovelCoverProps> = ({
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{ uri }}
|
||||
source={{
|
||||
uri,
|
||||
headers: { 'User-Agent': defaultUserAgentString },
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
height: coverHeight,
|
||||
@@ -348,9 +353,6 @@ const styles = StyleSheet.create({
|
||||
overflow: 'hidden',
|
||||
margin: 2,
|
||||
},
|
||||
selectedNovelCover: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
compactTitle: {
|
||||
color: 'rgba(255,255,255,1)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
|
||||
@@ -17,6 +17,7 @@ export type NovelListRenderItem = ListRenderItem<
|
||||
|
||||
interface NovelListProps
|
||||
extends FlatListProps<LibraryNovelInfo | NovelInfo | SourceNovelItem> {
|
||||
isFetchingNextPage?: boolean;
|
||||
inSource?: boolean;
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ const NovelList: React.FC<NovelListProps> = props => {
|
||||
);
|
||||
var extendedNovelList: Array<SourceNovelItem | LibraryNovelInfo> =
|
||||
props?.data as Array<LibraryNovelInfo>;
|
||||
if (props.data?.length && props.inSource) {
|
||||
if (props.data?.length && props.inSource && props.isFetchingNextPage) {
|
||||
let remainder = numColumns - (props.data?.length % numColumns);
|
||||
let extension = [];
|
||||
if (remainder !== 0 && remainder !== numColumns) {
|
||||
|
||||
@@ -20,30 +20,6 @@ const SwitchSetting: React.FC<SwitchProps> = ({
|
||||
icon,
|
||||
value,
|
||||
}) => {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
icon: {
|
||||
margin: 0,
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: icon && 16,
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
switch: { marginLeft: 8 },
|
||||
});
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
@@ -56,7 +32,7 @@ const SwitchSetting: React.FC<SwitchProps> = ({
|
||||
{icon && (
|
||||
<List.Icon color={theme.primary} icon={icon} style={styles.icon} />
|
||||
)}
|
||||
<View style={styles.textContainer}>
|
||||
<View>
|
||||
<Text style={[styles.text, { color: theme.onSurface }]}>{label}</Text>
|
||||
{description && (
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>{description}</Text>
|
||||
@@ -74,3 +50,27 @@ const SwitchSetting: React.FC<SwitchProps> = ({
|
||||
};
|
||||
|
||||
export default SwitchSetting;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
icon: {
|
||||
margin: 0,
|
||||
marginRight: 16,
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
switch: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,3 +13,4 @@ export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPrefe
|
||||
export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
|
||||
export { Checkbox } from './Checkbox/Checkbox';
|
||||
export { RadioButton } from './RadioButton/RadioButton';
|
||||
export { default as ConfirmationDialog } from './ConfirmationDialog/ConfirmationDialog';
|
||||
|
||||
@@ -132,9 +132,9 @@ export const restoreLibrary = async novel => {
|
||||
|
||||
if (chapters.length) {
|
||||
await insertChapters(insertId, chapters);
|
||||
|
||||
resolve();
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
(txObj, error) => {
|
||||
resolve();
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { appVersion } from '../utils/versionUtils';
|
||||
|
||||
export const useGithubUpdateChecker = () => {
|
||||
const latestReleaseUrl =
|
||||
'https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest';
|
||||
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [latestRelease, setLatestRelease] = useState();
|
||||
|
||||
const checkForRelease = async () => {
|
||||
let res = await fetch(latestReleaseUrl);
|
||||
res = await res.json();
|
||||
|
||||
const release = {
|
||||
tag_name: res.tag_name,
|
||||
body: res.body,
|
||||
downloadUrl: res.assets[0].browser_download_url,
|
||||
};
|
||||
|
||||
setLatestRelease(release);
|
||||
setChecking(false);
|
||||
};
|
||||
|
||||
const isNewVersion = versionTag => {
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
const currentVersion = appVersion;
|
||||
const newVersion = versionTag.replace(regex, '');
|
||||
|
||||
const newSemVer = newVersion.split('.');
|
||||
const oldSemVer = currentVersion.split('.');
|
||||
|
||||
oldSemVer?.map((version, index) => {
|
||||
if (newSemVer[index] > version) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkForRelease();
|
||||
// .catch(e => {
|
||||
// showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
|
||||
// // console.error(e);
|
||||
// });
|
||||
}, []);
|
||||
|
||||
if (!checking) {
|
||||
const data = {
|
||||
latestRelease,
|
||||
isNewVersion: isNewVersion(latestRelease.tag_name),
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
@@ -10,3 +10,4 @@ export { useTheme } from './useTheme';
|
||||
export { default as useCategories } from './useCategories';
|
||||
export { useCategorySettings, useLibrarySettings } from './useSettings';
|
||||
export { usePreviousRouteName } from './usePreviousRouteName';
|
||||
export { useReleaseChecker } from './useReleaseChecker';
|
||||
|
||||
@@ -12,12 +12,6 @@ const useNovel = () => {
|
||||
return novel;
|
||||
};
|
||||
|
||||
const useChapter = () => {
|
||||
const chapter = useSelector(state => state.chapterReducer);
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const useFindNovel = novelId => {
|
||||
let novel = useSelector(state => state.preferenceReducer.novelSettings);
|
||||
novel = novel[novelId];
|
||||
@@ -90,7 +84,6 @@ export {
|
||||
usePreferences,
|
||||
useContinueReading,
|
||||
useNovel,
|
||||
useChapter,
|
||||
useSavedSettings,
|
||||
usePosition,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMMKVNumber } from 'react-native-mmkv';
|
||||
|
||||
import { appVersion } from '../utils/versionUtils';
|
||||
import { showToast } from './showToast';
|
||||
|
||||
const LATEST_RELEASE_URL =
|
||||
'https://api.github.com/repos/LNReader/lnreader/releases/latest';
|
||||
|
||||
export interface NewRelease {
|
||||
tagName: string;
|
||||
changelog: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
const isNewVersion = (versionTag: string) => {
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
const currentVersion = appVersion;
|
||||
const newVersion = versionTag.replace(regex, '');
|
||||
|
||||
const newSemVer = newVersion.split('.');
|
||||
const oldSemVer = currentVersion.split('.');
|
||||
|
||||
return oldSemVer?.some((version, index) => {
|
||||
if (newSemVer[index] > version) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useReleaseChecker = () => {
|
||||
const [lastChecked, setLastChecked] = useMMKVNumber('last_app_update_check');
|
||||
const [newRelease, setNewRelease] = useState<NewRelease>();
|
||||
|
||||
const getNewRelease = async () => {
|
||||
try {
|
||||
if (lastChecked && dayjs(lastChecked).diff(undefined, 'hours') < 24) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newReleaseRes = await fetch(LATEST_RELEASE_URL);
|
||||
const newReleaseJSON = await newReleaseRes.json();
|
||||
|
||||
if (isNewVersion(newReleaseJSON.tag_name)) {
|
||||
setNewRelease({
|
||||
changelog: newReleaseJSON.body,
|
||||
downloadUrl: newReleaseJSON.assets[0].browser_download_url,
|
||||
tagName: newReleaseJSON.tag_name,
|
||||
});
|
||||
}
|
||||
|
||||
setLastChecked(+Date.now());
|
||||
} catch (error: any) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getNewRelease();
|
||||
}, []);
|
||||
|
||||
return newRelease;
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { showToast } from './showToast';
|
||||
|
||||
export const SOURCE_STORAGE = 'SOURCE_STORAGE';
|
||||
|
||||
interface SourceStorage {
|
||||
cookies?: any;
|
||||
}
|
||||
|
||||
export type SourceStorageMap = Record<number, Partial<SourceStorage>>;
|
||||
|
||||
const useSourceStorage = ({ sourceId = -1 }: { sourceId?: number }) => {
|
||||
const [values, setValues] = useMMKVObject<SourceStorageMap>(
|
||||
SOURCE_STORAGE,
|
||||
MMKVStorage,
|
||||
);
|
||||
|
||||
const setSourceStorage = (key: keyof SourceStorage, value: any): void => {
|
||||
setValues({
|
||||
...values,
|
||||
[sourceId]: {
|
||||
...values?.[sourceId],
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clearCookies = (): void => {
|
||||
// let tempStorage = {};
|
||||
|
||||
// if (values) {
|
||||
// tempStorage = Object.fromEntries(
|
||||
// Object.entries(values).map(([id, sourceData]) => {
|
||||
// delete sourceData.cookies;
|
||||
|
||||
// return [id, sourceData];
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
CookieManager.clearAll();
|
||||
CookieManager.clearAll(true); //clears cookies in webkit
|
||||
setValues({});
|
||||
showToast('Cookies cleared');
|
||||
};
|
||||
|
||||
const sourceStorage = values?.[sourceId];
|
||||
|
||||
return {
|
||||
...sourceStorage,
|
||||
setSourceStorage,
|
||||
clearCookies,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSourceStorage;
|
||||
|
||||
export const getSourceStorage = (sourceId: number) => {
|
||||
const rawSettings = MMKVStorage.getString(SOURCE_STORAGE) || '{}';
|
||||
const parsedSettings: Partial<SourceStorageMap> = JSON.parse(rawSettings);
|
||||
|
||||
return parsedSettings[sourceId] || {};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getNovel } from '@database/queries/NovelQueries';
|
||||
import { insertNovelInLibrary } from '@database/queries/NovelQueriesV2';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const { EpubParser } = NativeModules;
|
||||
const epubSourceId = 0;
|
||||
|
||||
interface EpubParserInterface {
|
||||
openDirectory(): Promise<string>;
|
||||
}
|
||||
|
||||
export async function openDirectory() {
|
||||
const ParserInterface = EpubParser as EpubParserInterface;
|
||||
const epubPath = await ParserInterface.openDirectory();
|
||||
|
||||
let dbNovel = await getNovel(epubSourceId, epubPath);
|
||||
if (dbNovel === undefined || dbNovel.followed === 0) {
|
||||
// Insert novel if it doesn't exist in the database
|
||||
await insertNovelInLibrary(epubSourceId, epubPath, false, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import Color from 'color';
|
||||
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
|
||||
|
||||
import Library from '../screens/library/LibraryScreen';
|
||||
@@ -5,7 +5,6 @@ import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
import { setBarColor } from '../theme/utils/setBarColor';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useGithubUpdateChecker } from '../hooks/githubUpdateChecker';
|
||||
|
||||
/**
|
||||
* Navigators
|
||||
@@ -26,7 +25,6 @@ import MigrateNovel from '../screens/browse/migration/MigrationNovels';
|
||||
|
||||
import MalTopNovels from '../screens/browse/discover/MalTopNovels';
|
||||
import AniListTopNovels from '../screens/browse/discover/AniListTopNovels';
|
||||
import NewUpdateDialog from '../components/NewUpdateDialog';
|
||||
import BrowseSettings from '../screens/browse/BrowseSettings';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import { updateLibraryAction } from '@redux/updates/updates.actions';
|
||||
@@ -56,11 +54,8 @@ const MainNavigator = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
|
||||
|
||||
return (
|
||||
<NavigationContainer theme={{ colors: theme }}>
|
||||
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="BottomNavigator" component={BottomNavigator} />
|
||||
<Stack.Screen name="Novel" component={Novel} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
CHAPTER_DOWNLOADING,
|
||||
CHAPTER_DOWNLOADED,
|
||||
CHAPTER_DELETED,
|
||||
ALL_CHAPTER_DELETED,
|
||||
GET_CHAPTERS,
|
||||
LOADING_NOVEL,
|
||||
GET_NOVEL,
|
||||
@@ -117,7 +118,7 @@ const novelReducer = (state = initialState, action) => {
|
||||
: chapter,
|
||||
),
|
||||
};
|
||||
case CHAPTER_DELETED:
|
||||
case ALL_CHAPTER_DELETED:
|
||||
return {
|
||||
...state,
|
||||
chapters: state.chapters.map(chapter => ({
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { SET_READER_SETTINGS, SET_APP_SETTINGS } from './settings.types';
|
||||
import {
|
||||
SET_READER_SETTINGS,
|
||||
SET_APP_SETTINGS,
|
||||
SET_MULTIPLE_APP_SETTINGS,
|
||||
} from './settings.types';
|
||||
|
||||
export const setAppSettings = (key, val) => dispatch => {
|
||||
dispatch({
|
||||
@@ -13,3 +17,10 @@ export const setReaderSettings = (key, val) => dispatch => {
|
||||
payload: { key, val },
|
||||
});
|
||||
};
|
||||
|
||||
export const setMultipleAppSettings = newSettings => dispatch => {
|
||||
dispatch({
|
||||
type: SET_MULTIPLE_APP_SETTINGS,
|
||||
payload: newSettings,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Dimensions } from 'react-native';
|
||||
import { SET_READER_SETTINGS, SET_APP_SETTINGS } from './settings.types';
|
||||
import {
|
||||
SET_READER_SETTINGS,
|
||||
SET_APP_SETTINGS,
|
||||
SET_MULTIPLE_APP_SETTINGS,
|
||||
} from './settings.types';
|
||||
|
||||
/**
|
||||
* Display Mode
|
||||
@@ -63,6 +67,7 @@ export const initialState = {
|
||||
|
||||
fullScreenMode: true,
|
||||
swipeGestures: false,
|
||||
readerPages: false,
|
||||
showScrollPercentage: true,
|
||||
useVolumeButtons: false,
|
||||
scrollAmount: Math.round(Dimensions.get('window').height),
|
||||
@@ -105,6 +110,11 @@ const settingsReducer = (state = initialState, action) => {
|
||||
[payload.key]: payload.val,
|
||||
},
|
||||
};
|
||||
case SET_MULTIPLE_APP_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const SET_APP_SETTINGS = 'SET_APP_SETTINGS';
|
||||
export const SET_READER_SETTINGS = 'SET_READER_SETTINGS';
|
||||
export const SET_MULTIPLE_APP_SETTINGS = 'SET_MULTIPLE_APP_SETTINGS';
|
||||
|
||||
@@ -52,6 +52,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
filterValues,
|
||||
setFilters,
|
||||
clearFilters,
|
||||
refetchNovels,
|
||||
} = useBrowseSource(sourceId, showLatestNovels);
|
||||
|
||||
const { defaultCategoryId = 1 } = useCategorySettings();
|
||||
@@ -126,10 +127,18 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
) : errorMessage || novelList.length === 0 ? (
|
||||
<ErrorScreenV2
|
||||
error={errorMessage || getString('sourceScreen.noResultsFound')}
|
||||
actions={[
|
||||
{
|
||||
iconName: 'refresh',
|
||||
title: getString('common.retry'),
|
||||
onPress: refetchNovels,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<NovelList
|
||||
data={novelList}
|
||||
isFetchingNextPage={hasNextPage && !searchText}
|
||||
inSource
|
||||
renderItem={({ item }) => {
|
||||
const inLibrary = novelInLibrary(item.novelUrl);
|
||||
|
||||
@@ -55,6 +55,9 @@ export const useBrowseSource = (
|
||||
}, [fetchNovels, currentPage]);
|
||||
|
||||
const refetchNovels = () => {
|
||||
isScreenMounted.current = true;
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
setCurrentPage(1);
|
||||
fetchNovels(1);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ import { SourceNovelItem } from '../../../sources/types';
|
||||
import { ThemeColors } from '../../../theme/types';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
interface Props {
|
||||
novel: SourceNovelItem;
|
||||
navigateToNovel: (novel: SourceNovelItem) => void;
|
||||
@@ -41,7 +43,10 @@ const GlobalSearchNovelItem: React.FC<Props> = ({
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
source={{
|
||||
uri: novel.novelCover,
|
||||
headers: { 'User-Agent': defaultUserAgentString },
|
||||
}}
|
||||
style={[styles.novelCover, { ...novelItemDimensions }]}
|
||||
/>
|
||||
{inLibrary ? (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { getString } from '@strings/translations';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
import { Appbar, ErrorScreenV2, LoadingScreenV2 } from '@components';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { getString } from '@strings/translations';
|
||||
import { LibraryStats } from '@database/types';
|
||||
import {
|
||||
getChaptersDownloadedCountFromDb,
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
getNovelGenresFromDb,
|
||||
getNovelStatusFromDb,
|
||||
} from '@database/queries/StatsQueries';
|
||||
import { Row } from '@components/Common';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
const StatsScreen = () => {
|
||||
const theme = useTheme();
|
||||
@@ -87,7 +85,7 @@ const StatsScreen = () => {
|
||||
<Text style={[styles.header, { color: theme.onSurfaceVariant }]}>
|
||||
{getString('moreScreen.settingsScreen.generalSettings')}
|
||||
</Text>
|
||||
<Row style={styles.statsRow}>
|
||||
<View style={styles.statsGroupCtn}>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.titlesInLibrary')}
|
||||
value={stats.novelsCount}
|
||||
@@ -100,8 +98,6 @@ const StatsScreen = () => {
|
||||
label={getString('statsScreen.totalChapters')}
|
||||
value={stats.chaptersCount}
|
||||
/>
|
||||
</Row>
|
||||
<Row style={styles.statsRow}>
|
||||
<StatsCard
|
||||
label={getString('statsScreen.unreadChapters')}
|
||||
value={stats.chaptersUnread}
|
||||
@@ -110,26 +106,25 @@ const StatsScreen = () => {
|
||||
label={getString('statsScreen.downloadedChapters')}
|
||||
value={stats.chaptersDownloaded}
|
||||
/>
|
||||
</Row>
|
||||
<Row style={styles.statsRow}>
|
||||
<StatsCard label="Sources" value={stats.sourcesCount} />
|
||||
</Row>
|
||||
</View>
|
||||
<Text style={[styles.header, { color: theme.onSurfaceVariant }]}>
|
||||
{getString('statsScreen.genreDistribution')}
|
||||
</Text>
|
||||
<Row style={[styles.statsRow, styles.genreRow]}>
|
||||
|
||||
<View style={styles.statsGroupCtn}>
|
||||
{Object.entries(stats.genres || {}).map(item => (
|
||||
<StatsCard key={item[0]} label={item[0]} value={item[1]} />
|
||||
))}
|
||||
</Row>
|
||||
</View>
|
||||
<Text style={[styles.header, { color: theme.onSurfaceVariant }]}>
|
||||
{getString('statsScreen.statusDistribution')}
|
||||
</Text>
|
||||
<Row style={[styles.statsRow, styles.genreRow]}>
|
||||
<View style={styles.statsGroupCtn}>
|
||||
{Object.entries(stats.status || {}).map(item => (
|
||||
<StatsCard key={item[0]} label={item[0]} value={item[1]} />
|
||||
))}
|
||||
</Row>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
@@ -165,9 +160,17 @@ export const StatsCard: React.FC<{ label: string; value?: number }> = ({
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
statsGroupCtn: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
statsCardCtn: {
|
||||
elevation: 1,
|
||||
margin: 4,
|
||||
marginBottom: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
@@ -184,10 +187,7 @@ const styles = StyleSheet.create({
|
||||
contentCtn: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
statsRow: {
|
||||
marginBottom: 8,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
genreRow: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||
import WebView from 'react-native-webview';
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
|
||||
import { Appbar } from '@components';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import useSourceStorage from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
type ReaderScreenRouteProps = RouteProp<{
|
||||
@@ -21,19 +20,12 @@ const WebviewScreen = () => {
|
||||
const { goBack } = useNavigation();
|
||||
|
||||
const {
|
||||
params: { name, sourceId, url },
|
||||
params: { name, url },
|
||||
} = useRoute<ReaderScreenRouteProps>();
|
||||
const { setSourceStorage } = useSourceStorage({ sourceId });
|
||||
|
||||
useEffect(() => {
|
||||
CookieManager.get(url, true).then(cookies => {
|
||||
const cloudflareCookie = cookies?.cf_clearance;
|
||||
if (cloudflareCookie) {
|
||||
const cloudflareCookieString = `${cloudflareCookie.name}=${cloudflareCookie.value}`;
|
||||
setSourceStorage('cookies', cloudflareCookieString);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const syncCookies = () => {
|
||||
CookieManager.flush();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -42,6 +34,7 @@ const WebviewScreen = () => {
|
||||
startInLoadingState
|
||||
userAgent={defaultUserAgentString}
|
||||
source={{ uri: url }}
|
||||
onNavigationStateChange={syncCookies}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { showToast } from '../../../hooks/showToast';
|
||||
|
||||
import { Button } from '@components';
|
||||
import { getString } from '@strings/translations';
|
||||
import { openNovel } from '@utils/handleNavigateParams';
|
||||
import { getNovelScreenRouteParams } from '@utils/NavigationUtils';
|
||||
|
||||
const MigrationNovelList = ({ data, theme, library, navigation }) => {
|
||||
const [selectedNovel, setSelectedNovel] = useState(false);
|
||||
@@ -30,7 +30,7 @@ const MigrationNovelList = ({ data, theme, library, navigation }) => {
|
||||
onLongPress={() =>
|
||||
navigation.navigate(
|
||||
'Novel',
|
||||
openNovel({
|
||||
getNovelScreenRouteParams({
|
||||
...item,
|
||||
sourceId: item.sourceId,
|
||||
}),
|
||||
|
||||
@@ -16,13 +16,12 @@ import { History } from '../../database/types';
|
||||
import { getString } from '@strings/translations';
|
||||
import ClearHistoryDialog from './components/ClearHistoryDialog';
|
||||
import {
|
||||
openChapter,
|
||||
openChapterChapterTypes,
|
||||
openChapterNovelTypes,
|
||||
openChapterFunctionTypes,
|
||||
openNovel,
|
||||
openNovelProps,
|
||||
} from '@utils/handleNavigateParams';
|
||||
ChapterRouteParams,
|
||||
NovelRouteParams,
|
||||
getNovelScreenRouteParams,
|
||||
getChapterScreenRouteParams,
|
||||
NovelScreenRouteParams,
|
||||
} from '@utils/NavigationUtils';
|
||||
import HistorySkeletonLoading from './components/HistorySkeletonLoading';
|
||||
|
||||
const HistoryScreen = () => {
|
||||
@@ -76,16 +75,16 @@ const HistoryScreen = () => {
|
||||
};
|
||||
|
||||
const handleNavigateToChapter = (
|
||||
novel: openChapterNovelTypes,
|
||||
chapter: openChapterChapterTypes,
|
||||
novel: NovelRouteParams,
|
||||
chapter: ChapterRouteParams,
|
||||
) =>
|
||||
navigate(
|
||||
'Chapter' as never,
|
||||
openChapter(novel, chapter) as openChapterFunctionTypes as never,
|
||||
getChapterScreenRouteParams(novel, chapter) as never,
|
||||
);
|
||||
|
||||
const handleNavigateToNovel = (novel: openNovelProps) =>
|
||||
navigate('Novel' as never, openNovel(novel) as openNovelProps as never);
|
||||
const handleNavigateToNovel = (novel: NovelScreenRouteParams) =>
|
||||
navigate('Novel' as never, getNovelScreenRouteParams(novel) as never);
|
||||
|
||||
const {
|
||||
value: clearHistoryDialogVisible,
|
||||
|
||||
@@ -11,19 +11,21 @@ import { History } from '@database/types';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import { coverPlaceholderColor } from '@theme/colors';
|
||||
import {
|
||||
openChapterChapterTypes,
|
||||
openChapterNovelTypes,
|
||||
openNovelProps,
|
||||
} from '@utils/handleNavigateParams';
|
||||
ChapterRouteParams,
|
||||
NovelRouteParams,
|
||||
NovelScreenRouteParams,
|
||||
} from '@utils/NavigationUtils';
|
||||
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
interface HistoryCardProps {
|
||||
history: History;
|
||||
handleNavigateToChapter: (
|
||||
novel: openChapterNovelTypes,
|
||||
chapter: openChapterChapterTypes,
|
||||
novel: NovelRouteParams,
|
||||
chapter: ChapterRouteParams,
|
||||
) => void;
|
||||
handleRemoveFromHistory: (historyId: number) => void;
|
||||
handleNavigateToNovel: (novel: openNovelProps) => void;
|
||||
handleNavigateToNovel: (novel: NovelScreenRouteParams) => void;
|
||||
theme: ThemeColors;
|
||||
}
|
||||
|
||||
@@ -85,7 +87,15 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
})
|
||||
}
|
||||
>
|
||||
<FastImage source={{ uri: novelCover }} style={styles.cover} />
|
||||
<FastImage
|
||||
source={{
|
||||
uri: novelCover,
|
||||
headers: {
|
||||
'User-Agent': defaultUserAgentString,
|
||||
},
|
||||
}}
|
||||
style={styles.cover}
|
||||
/>
|
||||
</Pressable>
|
||||
<View style={styles.detailsContainer}>
|
||||
<Text
|
||||
|
||||
@@ -31,7 +31,7 @@ import SetCategoryModal from '@screens/novel/components/SetCategoriesModal';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { debounce, intersection } from 'lodash-es';
|
||||
import { useBackHandler } from '@hooks/useBackHandler';
|
||||
import { openChapter } from '@utils/handleNavigateParams';
|
||||
import { getChapterScreenRouteParams } from '@utils/NavigationUtils';
|
||||
import useHistory from '@hooks/useHistory';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
@@ -259,7 +259,7 @@ const LibraryScreen = () => {
|
||||
onPress={() => {
|
||||
navigate(
|
||||
'Chapter' as never,
|
||||
openChapter(history[0], history[0]) as never,
|
||||
getChapterScreenRouteParams(history[0], history[0]) as never,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getString } from '@strings/translations';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { updateLibraryAction } from '@redux/updates/updates.actions';
|
||||
import { openNovel } from '@utils/handleNavigateParams';
|
||||
import { getNovelScreenRouteParams } from '@utils/NavigationUtils';
|
||||
|
||||
interface Props {
|
||||
categoryId: number;
|
||||
@@ -46,7 +46,7 @@ export const LibraryView: React.FC<Props> = ({
|
||||
if (selectedNovelIds.length) {
|
||||
setSelectedNovelIds(xor(selectedNovelIds, [item.novelId]));
|
||||
} else {
|
||||
navigate('Novel' as never, openNovel(item) as never);
|
||||
navigate('Novel' as never, getNovelScreenRouteParams(item) as never);
|
||||
dispatch(setNovel(item));
|
||||
}
|
||||
}}
|
||||
|
||||
+133
-135
@@ -31,144 +31,142 @@ const MoreScreen = ({ navigation }) => {
|
||||
setLibrarySettings({ incognitoMode: !incognitoMode });
|
||||
|
||||
return (
|
||||
<>
|
||||
<MoreHeader title="More" navigation={navigation} theme={theme} />
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<List.Section>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableDownloadedOnlyMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="cloud-off-outline" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Downloaded only
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
Filters all novels in your library
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView>
|
||||
<MoreHeader navigation={navigation} theme={theme} />
|
||||
<List.Section>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableDownloadedOnlyMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="cloud-off-outline" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Downloaded only
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
Filters all novels in your library
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={downloadedOnlyMode}
|
||||
onValueChange={enableDownloadedOnlyMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="incognito" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Incognito mode
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
Pauses reading history
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={downloadedOnlyMode}
|
||||
onValueChange={enableDownloadedOnlyMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="incognito" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Incognito mode
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
Pauses reading history
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Download queue"
|
||||
description={
|
||||
downloadQueue.length > 0 && downloadQueue.length + ' remaining'
|
||||
}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'DownloadQueue',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<List.Item
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Downloads',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.categories')}
|
||||
icon="label-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Categories',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('statsScreen.title')}
|
||||
icon="chart-line"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Statistics',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('moreScreen.settings')}
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'SettingsStack',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'About',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
</>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Download queue"
|
||||
description={
|
||||
downloadQueue.length > 0 && downloadQueue.length + ' remaining'
|
||||
}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'DownloadQueue',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Downloads',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.categories')}
|
||||
icon="label-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Categories',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('statsScreen.title')}
|
||||
icon="chart-line"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Statistics',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('moreScreen.settings')}
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'SettingsStack',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'About',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,14 @@ import {
|
||||
Text,
|
||||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
interpolateColor,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import {
|
||||
Provider,
|
||||
@@ -16,8 +23,8 @@ import {
|
||||
Appbar,
|
||||
IconButton,
|
||||
Menu,
|
||||
FAB,
|
||||
Snackbar,
|
||||
AnimatedFAB,
|
||||
} from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
@@ -49,18 +56,19 @@ import NovelInfoHeader from './components/Info/NovelInfoHeader';
|
||||
import NovelBottomSheet from './components/NovelBottomSheet';
|
||||
import TrackSheet from './components/Tracker/TrackSheet';
|
||||
import { Row } from '../../components/Common';
|
||||
import JumpToChapterModal from './components/Modal/JumpToChapterModal';
|
||||
import JumpToChapterModal from './components/JumpToChapterModal';
|
||||
import { Actionbar } from '../../components/Actionbar/Actionbar';
|
||||
import EditInfoModal from './components/Modal/EditInfoModal';
|
||||
import EditInfoModal from './components/EditInfoModal';
|
||||
import { pickCustomNovelCover } from '../../database/queries/NovelQueries';
|
||||
import DownloadCustomChapterModal from './components/Modal/DownloadCustomChapterModal';
|
||||
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useCategorySettings } from '@hooks/useSettings';
|
||||
import { openChapter } from '../../utils/handleNavigateParams';
|
||||
import { getChapterScreenRouteParams } from '../../utils/NavigationUtils';
|
||||
import NovelScreenLoading from './components/LoadingAnimation/NovelScreenLoading';
|
||||
import { useTrackerReducer } from '@redux/hooks';
|
||||
import EpubIconButton from './components/EpubIconButton';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -78,6 +86,7 @@ const Novel = ({ route, navigation }) => {
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [downloadMenu, showDownloadMenu] = useState(false);
|
||||
const [extraMenu, showExtraMenu] = useState(false);
|
||||
const [isFabExtended, setIsFabExtended] = useState(true);
|
||||
|
||||
let flatlistRef = useRef(null);
|
||||
let novelBottomSheetRef = useRef(null);
|
||||
@@ -89,6 +98,7 @@ const Novel = ({ route, navigation }) => {
|
||||
useFabForContinueReading = false,
|
||||
defaultChapterSort = 'ORDER BY chapterId ASC',
|
||||
disableHapticFeedback = false,
|
||||
jumpToLastReadChapter = false,
|
||||
} = useSettings();
|
||||
|
||||
const {
|
||||
@@ -104,6 +114,25 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const { defaultCategoryId = 1 } = useCategorySettings();
|
||||
|
||||
const headerOpacity = useSharedValue(0);
|
||||
const headerOpacityStyle = useAnimatedStyle(() => {
|
||||
const backgroundColor = interpolateColor(
|
||||
headerOpacity.value,
|
||||
[0, 1],
|
||||
['transparent', theme.surface],
|
||||
);
|
||||
return {
|
||||
backgroundColor,
|
||||
};
|
||||
});
|
||||
const onPageScroll = event => {
|
||||
const y = event.nativeEvent.contentOffset.y;
|
||||
headerOpacity.value = withTiming(y > 10 ? 1 : 0, { duration: 200 });
|
||||
|
||||
const currentScrollPosition = Math.floor(y) ?? 0;
|
||||
setIsFabExtended(currentScrollPosition <= 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getNovelAction(
|
||||
@@ -129,8 +158,8 @@ const Novel = ({ route, navigation }) => {
|
||||
progressViewOffset={progressViewOffset}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={updating}
|
||||
colors={[theme.primary]}
|
||||
progressBackgroundColor={theme.onPrimary}
|
||||
colors={[theme.onPrimary]}
|
||||
progressBackgroundColor={theme.primary}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -267,25 +296,37 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
if (lastSelectedChapter.chapterId !== chapter.chapterId) {
|
||||
if (lastSelectedChapter.chapterId > chapter.chapterId) {
|
||||
setSelected(sel => [
|
||||
...sel,
|
||||
chapter,
|
||||
...chapters.filter(
|
||||
chap =>
|
||||
(chap.chapterId <= chapter.chapterId ||
|
||||
chap.chapterId >= lastSelectedChapter.chapterId) === false,
|
||||
setSelected(sel =>
|
||||
uniqBy(
|
||||
[
|
||||
...sel,
|
||||
chapter,
|
||||
...chapters.filter(
|
||||
chap =>
|
||||
(chap.chapterId <= chapter.chapterId ||
|
||||
chap.chapterId >= lastSelectedChapter.chapterId) ===
|
||||
false,
|
||||
),
|
||||
],
|
||||
'chapterId',
|
||||
),
|
||||
]);
|
||||
);
|
||||
} else {
|
||||
setSelected(sel => [
|
||||
...sel,
|
||||
chapter,
|
||||
...chapters.filter(
|
||||
chap =>
|
||||
(chap.chapterId >= chapter.chapterId ||
|
||||
chap.chapterId <= lastSelectedChapter.chapterId) === false,
|
||||
setSelected(sel =>
|
||||
uniqBy(
|
||||
[
|
||||
...sel,
|
||||
chapter,
|
||||
...chapters.filter(
|
||||
chap =>
|
||||
(chap.chapterId >= chapter.chapterId ||
|
||||
chap.chapterId <= lastSelectedChapter.chapterId) ===
|
||||
false,
|
||||
),
|
||||
],
|
||||
'chapterId',
|
||||
),
|
||||
]);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +335,7 @@ const Novel = ({ route, navigation }) => {
|
||||
const navigateToChapter = chapter => {
|
||||
navigation.navigate(
|
||||
'Chapter',
|
||||
openChapter({ sourceId, novelUrl, novelName }, chapter),
|
||||
getChapterScreenRouteParams({ sourceId, novelUrl, novelName }, chapter),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -340,7 +381,6 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const renderItem = ({ item: it, index }) => (
|
||||
<ChapterItem
|
||||
theme={theme}
|
||||
chapter={it}
|
||||
index={index}
|
||||
showChapterTitles={showChapterTitles}
|
||||
@@ -367,7 +407,7 @@ const Novel = ({ route, navigation }) => {
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Portal>
|
||||
{selected.length === 0 ? (
|
||||
<View style={styles.row}>
|
||||
<Animated.View style={[styles.row, headerOpacityStyle]}>
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
iconColor={theme.onBackground}
|
||||
@@ -398,7 +438,11 @@ const Novel = ({ route, navigation }) => {
|
||||
}}
|
||||
onPress={() => showJumpToChapterModal(true)}
|
||||
/>
|
||||
|
||||
<EpubIconButton
|
||||
theme={theme}
|
||||
style={{ marginTop: StatusBar.currentHeight + 8 }}
|
||||
novel={novel}
|
||||
/>
|
||||
<Menu
|
||||
visible={downloadMenu}
|
||||
onDismiss={() => showDownloadMenu(false)}
|
||||
@@ -531,16 +575,13 @@ const Novel = ({ route, navigation }) => {
|
||||
titleStyle={{
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(deleteAllChaptersAction(sourceId, chapters))
|
||||
}
|
||||
onPress={() => {
|
||||
dispatch(deleteAllChaptersAction(sourceId, chapters));
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
<EpubIconButton
|
||||
theme={theme}
|
||||
style={{ marginTop: StatusBar.currentHeight + 8 }}
|
||||
novel={novel}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
visible={extraMenu}
|
||||
onDismiss={() => showExtraMenu(false)}
|
||||
@@ -578,7 +619,7 @@ const Novel = ({ route, navigation }) => {
|
||||
/>
|
||||
</Menu>
|
||||
</Row>
|
||||
</View>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(150)}
|
||||
@@ -611,6 +652,18 @@ const Novel = ({ route, navigation }) => {
|
||||
</Portal>
|
||||
<View style={styles.flashlistContainer}>
|
||||
<FlashList
|
||||
onLayout={() => {
|
||||
if (jumpToLastReadChapter) {
|
||||
const lastReadChapterIdx = chapters.findIndex(
|
||||
chapter => chapter.chapterId === lastReadChapter.chapterId,
|
||||
);
|
||||
|
||||
flatlistRef.current.scrollToIndex({
|
||||
animated: true,
|
||||
index: lastReadChapterIdx,
|
||||
});
|
||||
}
|
||||
}}
|
||||
ref={flatlistRef}
|
||||
estimatedItemSize={64}
|
||||
data={chapters}
|
||||
@@ -638,16 +691,17 @@ const Novel = ({ route, navigation }) => {
|
||||
deleteDownloadsSnackbar={deleteDownloadsSnackbar}
|
||||
/>
|
||||
}
|
||||
onScroll={onPageScroll}
|
||||
refreshControl={refreshControl()}
|
||||
/>
|
||||
</View>
|
||||
{useFabForContinueReading && chapters.length > 0 && lastReadChapter && (
|
||||
<FAB
|
||||
<AnimatedFAB
|
||||
style={[
|
||||
styles.fab,
|
||||
{ backgroundColor: theme.primary, marginBottom: bottomInset },
|
||||
]}
|
||||
small
|
||||
extended={isFabExtended}
|
||||
color={theme.onPrimary}
|
||||
uppercase={false}
|
||||
label={novel.unread ? 'Start' : 'Resume'}
|
||||
@@ -655,7 +709,7 @@ const Novel = ({ route, navigation }) => {
|
||||
onPress={() => {
|
||||
navigation.navigate(
|
||||
'Chapter',
|
||||
openChapter(novel, lastReadChapter),
|
||||
getChapterScreenRouteParams(novel, lastReadChapter),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChapterItemExtended } from '@database/types';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
|
||||
import { IconButton, Menu, overlay } from 'react-native-paper';
|
||||
@@ -27,6 +27,9 @@ export const DownloadButton: React.FC<DownloadButtonProps> = ({
|
||||
deleteChapterMenuVisible,
|
||||
}) => {
|
||||
const [downloaded, setDownloaded] = useState(chapter.downloaded);
|
||||
// React to external changes of chapter download state
|
||||
useEffect(() => setDownloaded(chapter.downloaded ? 1 : 0), [chapter]);
|
||||
|
||||
if (downloadQueue.some(chap => chap.chapterId === chapter.chapterId)) {
|
||||
return <ChapterDownloadingButton theme={theme} />;
|
||||
} else if (downloaded === 0) {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React, { memo, ReactNode, useState } from 'react';
|
||||
import { View, StyleSheet, Pressable } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import color from 'color';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { Row } from '../../../components/Common';
|
||||
import { Row } from '@components/Common';
|
||||
|
||||
import { useBoolean, useTheme } from '@hooks';
|
||||
import { ChapterItemExtended } from '@database/types';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
import {
|
||||
ChapterBookmarkButton,
|
||||
DownloadButton,
|
||||
} from './Chapter/ChapterDownloadButtons';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import { ChapterItemExtended } from '@database/types';
|
||||
|
||||
interface ChapterItemProps {
|
||||
chapter: ChapterItemExtended;
|
||||
theme: ThemeColors;
|
||||
index?: number;
|
||||
downloadQueue: any;
|
||||
showChapterTitles: boolean;
|
||||
@@ -35,7 +35,6 @@ interface ChapterItemProps {
|
||||
|
||||
const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
chapter,
|
||||
theme,
|
||||
index,
|
||||
showChapterTitles,
|
||||
downloadQueue,
|
||||
@@ -50,16 +49,20 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
isUpdateCard,
|
||||
novelName,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { chapterId, chapterName, read, releaseDate, bookmark } = chapter;
|
||||
const [deleteChapterMenuVisible, setDeleteChapterMenuVisible] =
|
||||
useState(false);
|
||||
const showDeleteChapterMenu = () => setDeleteChapterMenuVisible(true);
|
||||
const hideDeleteChapterMenu = () => setDeleteChapterMenuVisible(false);
|
||||
|
||||
const {
|
||||
value: isMenuVisible,
|
||||
setTrue: showMenu,
|
||||
setFalse: hideMenu,
|
||||
} = useBoolean();
|
||||
|
||||
const chapterNumber = parseChapterNumber(chapterName);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={chapterId.toString()}
|
||||
style={[
|
||||
styles.chapterCardContainer,
|
||||
isSelected?.(chapterId) && {
|
||||
@@ -74,10 +77,10 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
onLongPress={() => onSelectLongPress?.(chapter)}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
{left && <View style={styles.flex}>{left}</View>}
|
||||
<Row style={styles.row}>
|
||||
{left}
|
||||
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
|
||||
<View style={styles.textContainer}>
|
||||
<View style={{ flex: 1 }}>
|
||||
{isUpdateCard && (
|
||||
<Text
|
||||
style={[
|
||||
@@ -91,25 +94,33 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
{novelName}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
<View style={styles.row}>
|
||||
{!chapter.read ? (
|
||||
<MaterialCommunityIcons
|
||||
name="circle"
|
||||
color={theme.primary}
|
||||
size={8}
|
||||
style={styles.unreadIcon}
|
||||
/>
|
||||
) : null}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: isUpdateCard ? 12 : 14,
|
||||
color: read
|
||||
? theme.outline
|
||||
: bookmark
|
||||
? theme.primary
|
||||
: theme.onSurfaceVariant,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{showChapterTitles
|
||||
? chapterNumber
|
||||
? 'Chapter ' + chapterNumber
|
||||
: 'Chapter ' + index
|
||||
: chapterName}
|
||||
</Text>
|
||||
: theme.onSurface,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{showChapterTitles
|
||||
? chapterNumber
|
||||
? 'Chapter ' + chapterNumber
|
||||
: 'Chapter ' + index
|
||||
: chapterName}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.textRow}>
|
||||
{releaseDate && !isUpdateCard ? (
|
||||
<Text
|
||||
@@ -132,23 +143,21 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
</View>
|
||||
</View>
|
||||
</Row>
|
||||
<View style={styles.flex}>
|
||||
<DownloadButton
|
||||
downloadQueue={downloadQueue}
|
||||
chapter={chapter}
|
||||
theme={theme}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
hideDeleteChapterMenu={hideDeleteChapterMenu}
|
||||
showDeleteChapterMenu={showDeleteChapterMenu}
|
||||
deleteChapterMenuVisible={deleteChapterMenuVisible}
|
||||
/>
|
||||
</View>
|
||||
<DownloadButton
|
||||
downloadQueue={downloadQueue}
|
||||
chapter={chapter}
|
||||
theme={theme}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
hideDeleteChapterMenu={hideMenu}
|
||||
showDeleteChapterMenu={showMenu}
|
||||
deleteChapterMenuVisible={isMenuVisible}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChapterItem);
|
||||
export default memo(ChapterItem, isEqual);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chapterCardContainer: {
|
||||
@@ -158,28 +167,20 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
columnGab: 8,
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
},
|
||||
textRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
flex: {
|
||||
flexBasis: 'auto',
|
||||
flexShrink: 9,
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
marginVertical: '10%',
|
||||
height: '80%',
|
||||
justifyContent: 'space-evenly',
|
||||
marginTop: 5,
|
||||
},
|
||||
row: {
|
||||
flexBasis: 10,
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
unreadIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
|
||||
+48
-60
@@ -1,29 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { getString } from '@strings/translations';
|
||||
import { Button } from '@components';
|
||||
|
||||
import { Modal, TextInput, Text } from 'react-native-paper';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
import { openDocumentTree } from 'react-native-saf-x';
|
||||
import SwitchSetting from '@components/Switch/Switch';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setAppSettings } from '@redux/settings/settings.actions';
|
||||
import { openDocumentTree } from 'react-native-saf-x';
|
||||
|
||||
import { Button, List, SwitchItem } from '@components';
|
||||
|
||||
import { useTheme, useBoolean } from '@hooks';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { getString } from '@strings/translations';
|
||||
import { setMultipleAppSettings } from '@redux/settings/settings.actions';
|
||||
|
||||
interface ChooseEpubLocationModalProps {
|
||||
hideModal: () => void;
|
||||
modalVisible: boolean;
|
||||
isVisible: boolean;
|
||||
onSubmit?: (uri: string) => void;
|
||||
dispatchConfig?: boolean;
|
||||
hideModal: () => void;
|
||||
}
|
||||
|
||||
const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
hideModal,
|
||||
modalVisible,
|
||||
isVisible,
|
||||
onSubmit: onSubmitProp,
|
||||
dispatchConfig,
|
||||
hideModal,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
@@ -32,28 +30,28 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
epubUseCustomCSS = false,
|
||||
epubUseCustomJS = false,
|
||||
} = useSettings();
|
||||
const saveConfig = dispatchConfig || epubLocation === '';
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [uri, setUri] = useState(epubLocation);
|
||||
const useAppTheme = useBoolean(epubUseAppTheme);
|
||||
const useCustomCSS = useBoolean(epubUseCustomCSS);
|
||||
const useCustomJS = useBoolean(epubUseCustomJS);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const onDismiss = () => {
|
||||
hideModal();
|
||||
setError('');
|
||||
setUri(epubLocation);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (saveConfig) {
|
||||
dispatch(setAppSettings('epubLocation', uri));
|
||||
dispatch(setAppSettings('epubUseAppTheme', useAppTheme.value));
|
||||
dispatch(setAppSettings('epubUseCustomCSS', useCustomCSS.value));
|
||||
dispatch(setAppSettings('epubUseCustomJS', useCustomJS.value));
|
||||
}
|
||||
dispatch(
|
||||
setMultipleAppSettings({
|
||||
'epubLocation': uri,
|
||||
'epubUseAppTheme': useAppTheme.value,
|
||||
'epubUseCustomCSS': useCustomCSS.value,
|
||||
'epubUseCustomJS': useCustomJS.value,
|
||||
}),
|
||||
);
|
||||
|
||||
onSubmitProp?.(uri);
|
||||
hideModal();
|
||||
};
|
||||
@@ -64,14 +62,14 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
if (resultUri) {
|
||||
setUri(resultUri.uri);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} catch (error: any) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
visible={isVisible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
@@ -82,43 +80,37 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
|
||||
{getString('novelScreen.convertToEpubModal.chooseLocation')}
|
||||
</Text>
|
||||
<View style={styles.row}>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
onChangeText={setUri}
|
||||
value={uri}
|
||||
placeholder={getString(
|
||||
'novelScreen.convertToEpubModal.pathToFolder',
|
||||
)}
|
||||
onSubmitEditing={onSubmit}
|
||||
mode="outlined"
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.outline}
|
||||
dense
|
||||
error={error ? true : false}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon="folder-edit-outline"
|
||||
onPress={openFolderPicker}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<TextInput
|
||||
onChangeText={setUri}
|
||||
value={uri}
|
||||
placeholder={getString('novelScreen.convertToEpubModal.pathToFolder')}
|
||||
onSubmitEditing={onSubmit}
|
||||
mode="outlined"
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.outline}
|
||||
dense
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon="folder-edit-outline"
|
||||
onPress={openFolderPicker}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settings}>
|
||||
<SwitchSetting
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.convertToEpubModal.useReaderTheme')}
|
||||
value={useAppTheme.value}
|
||||
onPress={useAppTheme.toggle}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.convertToEpubModal.useCustomCSS')}
|
||||
value={useCustomCSS.value}
|
||||
onPress={useCustomCSS.toggle}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.convertToEpubModal.useCustomJS')}
|
||||
description={getString(
|
||||
'novelScreen.convertToEpubModal.useCustomJSWarning',
|
||||
@@ -128,6 +120,10 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<List.InfoItem
|
||||
title={getString('novelScreen.convertToEpubModal.chaptersWarning')}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={styles.modalFooterCtn}>
|
||||
<Button title={getString('common.submit')} onPress={onSubmit} />
|
||||
<Button title={getString('common.cancel')} onPress={hideModal} />
|
||||
@@ -145,10 +141,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
settings: {
|
||||
marginTop: 12,
|
||||
paddingLeft: 5,
|
||||
},
|
||||
icon: { marginVertical: 0, marginRight: 0 },
|
||||
textInput: { flex: 1 },
|
||||
modalHeaderCtn: {
|
||||
padding: 20,
|
||||
paddingTop: 32,
|
||||
@@ -164,9 +157,4 @@ const styles = StyleSheet.create({
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { StyleSheet, Text, View, TextInput } from 'react-native';
|
||||
|
||||
import { Button, IconButton, Modal, Portal } from 'react-native-paper';
|
||||
import { downloadAllChaptersAction } from '../../../../redux/novel/novel.actions';
|
||||
import { downloadAllChaptersAction } from '../../../redux/novel/novel.actions';
|
||||
|
||||
const DownloadCustomChapterModal = ({
|
||||
theme,
|
||||
+2
-2
@@ -10,8 +10,8 @@ import {
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { Modal, Portal, TextInput } from 'react-native-paper';
|
||||
import { setNovel } from '../../../../redux/novel/novel.actions';
|
||||
import { updateNovelInfo } from '../../../../database/queries/NovelQueries';
|
||||
import { setNovel } from '../../../redux/novel/novel.actions';
|
||||
import { updateNovelInfo } from '../../../database/queries/NovelQueries';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { Button } from '@components';
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { IconButton, Portal } from 'react-native-paper';
|
||||
import ChooseEpubLocationModal from './Modal/ChooseEpubLocationModal';
|
||||
import ChooseEpubLocationModal from './ChooseEpubLocationModal';
|
||||
import { StatusBar, StyleProp } from 'react-native';
|
||||
import { IconProps } from 'react-native-paper/lib/typescript/src/components/MaterialCommunityIcon';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
@@ -27,7 +27,11 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
|
||||
style,
|
||||
novel,
|
||||
}) => {
|
||||
const chooseEpubLocationModal = useBoolean(false);
|
||||
const {
|
||||
value: isVisible,
|
||||
setTrue: showModal,
|
||||
setFalse: hideModal,
|
||||
} = useBoolean(false);
|
||||
const readerSettings = useReaderSettings();
|
||||
const {
|
||||
epubUseAppTheme = false,
|
||||
@@ -35,56 +39,73 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
|
||||
epubUseCustomJS = false,
|
||||
} = useSettings();
|
||||
|
||||
const epubStyle = `${
|
||||
epubUseAppTheme
|
||||
? `html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
padding-bottom: 40px;
|
||||
font-size: ${readerSettings.textSize}px;
|
||||
color: ${readerSettings.textColor};
|
||||
text-align: ${readerSettings.textAlign};
|
||||
line-height: ${readerSettings.lineHeight};
|
||||
font-family: "${readerSettings.fontFamily}";
|
||||
background-color: "${readerSettings.theme}";
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.primary};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
epubUseCustomCSS
|
||||
? readerSettings.customCSS
|
||||
.replace(RegExp(`\#sourceId-${novel.sourceId}\\s*\\{`, 'g'), 'body {')
|
||||
.replace(RegExp(`\#sourceId-${novel.sourceId}[^\.\#A-Z]*`, 'gi'), '')
|
||||
: ''
|
||||
}`;
|
||||
const epubStyle = useMemo(
|
||||
() =>
|
||||
`${
|
||||
epubUseAppTheme
|
||||
? `
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
padding-bottom: 40px;
|
||||
font-size: ${readerSettings.textSize}px;
|
||||
color: ${readerSettings.textColor};
|
||||
text-align: ${readerSettings.textAlign};
|
||||
line-height: ${readerSettings.lineHeight};
|
||||
font-family: "${readerSettings.fontFamily}";
|
||||
background-color: "${readerSettings.theme}";
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.primary};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
epubUseCustomCSS
|
||||
? readerSettings.customCSS
|
||||
.replace(
|
||||
RegExp(`\#sourceId-${novel.sourceId}\\s*\\{`, 'g'),
|
||||
'body {',
|
||||
)
|
||||
.replace(
|
||||
RegExp(`\#sourceId-${novel.sourceId}[^\.\#A-Z]*`, 'gi'),
|
||||
'',
|
||||
)
|
||||
: ''
|
||||
}`,
|
||||
[novel, epubUseAppTheme, readerSettings, epubUseCustomCSS],
|
||||
);
|
||||
|
||||
const epubJS = `let novelName = "${novel.novelName}";
|
||||
let chapterName = "";
|
||||
let sourceId =${novel.sourceId};
|
||||
let chapterId ="";
|
||||
let novelId =${novel.novelId};
|
||||
let html = document.querySelector("chapter").innerHTML;
|
||||
${readerSettings.customJS}
|
||||
`;
|
||||
const epubJS = useMemo(
|
||||
() =>
|
||||
`
|
||||
let novelName = "${novel.novelName}";
|
||||
let chapterName = "";
|
||||
let sourceId =${novel.sourceId};
|
||||
let chapterId ="";
|
||||
let novelId =${novel.novelId};
|
||||
let html = document.querySelector("chapter").innerHTML;
|
||||
|
||||
${readerSettings.customJS}
|
||||
`,
|
||||
[novel, readerSettings],
|
||||
);
|
||||
|
||||
const createEpub = async (uri: string) => {
|
||||
var epub = new EpubBuilder(
|
||||
@@ -130,12 +151,12 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
|
||||
iconColor={theme.onBackground}
|
||||
size={21}
|
||||
style={style}
|
||||
onPress={chooseEpubLocationModal.setTrue}
|
||||
onPress={showModal}
|
||||
/>
|
||||
<Portal>
|
||||
<ChooseEpubLocationModal
|
||||
modalVisible={chooseEpubLocationModal.value}
|
||||
hideModal={chooseEpubLocationModal.setFalse}
|
||||
isVisible={isVisible}
|
||||
hideModal={hideModal}
|
||||
onSubmit={createEpub}
|
||||
/>
|
||||
</Portal>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import color from 'color';
|
||||
import { IconButton, Portal } from 'react-native-paper';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
@@ -16,6 +17,7 @@ import FastImage from 'react-native-fast-image';
|
||||
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
|
||||
import { Chip } from '../../../../components';
|
||||
import { coverPlaceholderColor } from '../../../../theme/colors';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
|
||||
const NovelInfoContainer = ({ children }) => (
|
||||
<View style={styles.novelInfoContainer}>{children}</View>
|
||||
@@ -69,17 +71,41 @@ const NovelThumbnail = ({ source, theme, setCustomNovelCover }) => {
|
||||
} else {
|
||||
return (
|
||||
<Portal>
|
||||
<IconButton
|
||||
icon="pencil-outline"
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StatusBar.currentHeight + 10,
|
||||
right: 10,
|
||||
zIndex: 10,
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
iconColor={theme.onBackground}
|
||||
onPress={setCustomNovelCover}
|
||||
/>
|
||||
>
|
||||
<IconButton
|
||||
icon="pencil-outline"
|
||||
iconColor={theme.onBackground}
|
||||
onPress={setCustomNovelCover}
|
||||
/>
|
||||
<IconButton
|
||||
icon="content-save-outline"
|
||||
iconColor={theme.onBackground}
|
||||
onPress={() => {
|
||||
RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
addAndroidDownloads: {
|
||||
useDownloadManager: true,
|
||||
notification: true,
|
||||
path:
|
||||
RNFetchBlob.fs.dirs.DownloadDir +
|
||||
'/' +
|
||||
source.uri.split('/').pop(),
|
||||
description: 'Image',
|
||||
},
|
||||
})
|
||||
.fetch('GET', source.uri)
|
||||
.then(() => showToast('Cover saved'));
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Pressable
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -11,6 +11,8 @@ import { followNovelAction } from '../../../../redux/novel/novel.actions';
|
||||
import { useSettings } from '../../../../hooks/reduxHooks';
|
||||
import { showToast } from '../../../../hooks/showToast';
|
||||
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
import {
|
||||
CoverImage,
|
||||
NovelAuthor,
|
||||
@@ -57,13 +59,23 @@ const NovelInfoHeader = ({
|
||||
return (
|
||||
<>
|
||||
<CoverImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
source={{
|
||||
uri: novel.novelCover,
|
||||
headers: {
|
||||
'User-Agent': defaultUserAgentString,
|
||||
},
|
||||
}}
|
||||
theme={theme}
|
||||
hideBackdrop={hideBackdrop}
|
||||
>
|
||||
<NovelInfoContainer>
|
||||
<NovelThumbnail
|
||||
source={{ uri: novel.novelCover }}
|
||||
source={{
|
||||
uri: novel.novelCover,
|
||||
headers: {
|
||||
'User-Agent': defaultUserAgentString,
|
||||
},
|
||||
}}
|
||||
theme={theme}
|
||||
setCustomNovelCover={setCustomNovelCover}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { openChapter } from '@utils/handleNavigateParams';
|
||||
import { getChapterScreenRouteParams } from '@utils/NavigationUtils';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@components';
|
||||
@@ -11,7 +11,7 @@ const ReadButton = ({ novel, chapters, lastRead }) => {
|
||||
const { useFabForContinueReading = false } = useSettings();
|
||||
|
||||
const navigateToLastReadChapter = () =>
|
||||
navigate('Chapter', openChapter(novel, lastRead));
|
||||
navigate('Chapter', getChapterScreenRouteParams(novel, lastRead));
|
||||
|
||||
if (!useFabForContinueReading) {
|
||||
return (
|
||||
|
||||
@@ -34,12 +34,24 @@ const ChaptersSettingsSheet = ({
|
||||
theme={theme}
|
||||
label="Downloaded"
|
||||
color={theme.primary}
|
||||
status={filter.match('AND downloaded=1')}
|
||||
onPress={() =>
|
||||
status={
|
||||
filter.match('AND downloaded=1')
|
||||
? filterChapters(filter.replace(' AND downloaded=1', ''))
|
||||
: filterChapters(filter + ' AND downloaded=1')
|
||||
? true
|
||||
: filter.match('AND downloaded=0')
|
||||
? 'indeterminate'
|
||||
: false
|
||||
}
|
||||
onPress={() => {
|
||||
if (filter.match('AND downloaded=1')) {
|
||||
filterChapters(
|
||||
filter.replace(' AND downloaded=1', ' AND downloaded=0'),
|
||||
);
|
||||
} else if (filter.match('AND downloaded=0')) {
|
||||
filterChapters(filter.replace(' AND downloaded=0', ''));
|
||||
} else {
|
||||
filterChapters(filter + ' AND downloaded=1');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
theme={theme}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { getString } from '@strings/translations';
|
||||
import React, { useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import {
|
||||
LayoutAnimation,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { ThemeColors } from '../../../../theme/types';
|
||||
@@ -22,6 +28,10 @@ const NovelSummary: React.FC<NovelSummaryProps> = ({
|
||||
const [expanded, setExpanded] = useState(isExpanded);
|
||||
const toggleExpanded = () => {
|
||||
if (summary) {
|
||||
LayoutAnimation.configureNext({
|
||||
...LayoutAnimation.Presets.linear,
|
||||
duration: 100,
|
||||
});
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
};
|
||||
@@ -38,8 +48,13 @@ const NovelSummary: React.FC<NovelSummaryProps> = ({
|
||||
onPress={toggleExpanded}
|
||||
>
|
||||
<Text
|
||||
style={[styles.summaryText, { color: textColor }]}
|
||||
numberOfLines={expanded ? Number.MAX_SAFE_INTEGER : 3}
|
||||
style={[
|
||||
styles.summaryText,
|
||||
{
|
||||
color: textColor,
|
||||
height: expanded ? undefined : styles.summaryText.lineHeight * 3,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{summary || getString('novelScreen.noSummary')}
|
||||
</Text>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Dimensions, NativeModules, NativeEventEmitter } from 'react-native';
|
||||
|
||||
import VolumeButtonListener from './../../utils/volumeButtonListener';
|
||||
@@ -40,6 +46,8 @@ import { createDrawerNavigator } from '@react-navigation/drawer';
|
||||
import { htmlToText } from '../../sources/helpers/htmlToText';
|
||||
import ChapterLoadingScreen from './ChapterLoadingScreen/ChapterLoadingScreen';
|
||||
import { ErrorScreenV2 } from '@components';
|
||||
import { getString } from '@strings/translations';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const Chapter = ({ route }) => {
|
||||
const DrawerNav = createDrawerNavigator();
|
||||
@@ -76,9 +84,11 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { navigate } = useNavigation();
|
||||
|
||||
const {
|
||||
swipeGestures = false,
|
||||
readerPages = false,
|
||||
useVolumeButtons = false,
|
||||
autoScroll = false,
|
||||
autoScrollInterval = 10,
|
||||
@@ -96,6 +106,7 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
|
||||
const position = usePosition(novelId, chapterId);
|
||||
const minScroll = useRef(0);
|
||||
const pages = useRef(0);
|
||||
|
||||
const { tracker, trackedNovels } = useTrackerReducer();
|
||||
const isTracked = trackedNovels.find(obj => obj.novelId === novelId);
|
||||
@@ -145,7 +156,7 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
emmiter.current.removeAllListeners('VolumeDown');
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [useVolumeButtons, scrollAmount]);
|
||||
}, [useVolumeButtons, scrollAmount, chapter]);
|
||||
|
||||
const onLayout = useCallback(() => {
|
||||
if (useVolumeButtons) {
|
||||
@@ -195,22 +206,51 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chapter]);
|
||||
|
||||
const [ttsStatus, startTts] = useTextToSpeech(
|
||||
htmlToText(chapter?.chapterText).split('\n'),
|
||||
() => {
|
||||
if (!incognitoMode) {
|
||||
dispatch(markChapterReadAction(chapterId, novelId));
|
||||
updateTracker();
|
||||
}
|
||||
},
|
||||
const htmlText = useMemo(
|
||||
() => htmlToText(chapter?.chapterText).split('\n'),
|
||||
[chapter.chapterText],
|
||||
);
|
||||
|
||||
const [ttsStatus, startTts] = useTextToSpeech(htmlText, () => {
|
||||
if (!incognitoMode) {
|
||||
dispatch(markChapterReadAction(chapterId, novelId));
|
||||
updateTracker();
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!readerPages) {
|
||||
webViewRef?.current?.injectJavaScript(
|
||||
"document.querySelector('chapter').style.transform = 'translate(0%)';",
|
||||
);
|
||||
}
|
||||
}, [readerPages]);
|
||||
const scrollTo = useCallback(
|
||||
offsetY => {
|
||||
offset => {
|
||||
requestAnimationFrame(() => {
|
||||
webViewRef?.current?.injectJavaScript(`(()=>{
|
||||
window.scrollTo({top:${offsetY},behavior:'smooth'})
|
||||
})()`);
|
||||
webViewRef?.current?.injectJavaScript(
|
||||
!readerPages
|
||||
? `(()=>{
|
||||
window.scrollTo({top:${offset},behavior:'smooth'})
|
||||
})()`
|
||||
: `(()=>{
|
||||
document.querySelector('chapter').setAttribute('data-page', ${
|
||||
offset / 100
|
||||
});
|
||||
document.querySelector("chapter").style.transform = 'translate(-${offset}%)';
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify(
|
||||
{
|
||||
type:"scrollend",
|
||||
data:{
|
||||
offSetY: ${offset},
|
||||
percentage: (${offset / 100 / pages.current} > 0 ? ${
|
||||
offset / 100 / pages.current
|
||||
} * 100 : 1),
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
})()`,
|
||||
);
|
||||
});
|
||||
},
|
||||
[webViewRef],
|
||||
@@ -268,13 +308,15 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
);
|
||||
|
||||
const hideHeader = useCallback(() => {
|
||||
if (!hidden) {
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
showStatusAndNavBar();
|
||||
}
|
||||
setHidden(!hidden);
|
||||
}, [hidden]);
|
||||
setHidden(h => {
|
||||
if (!h) {
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
showStatusAndNavBar();
|
||||
}
|
||||
return !h;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const navigateToChapterBySwipe = useCallback(
|
||||
name => {
|
||||
@@ -313,29 +355,69 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scrollToSavedProgress = () => scrollTo(position?.position);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (position?.percentage === 100) {
|
||||
doSaveProgress(0, 0);
|
||||
}
|
||||
};
|
||||
}, [position?.percentage]);
|
||||
|
||||
const chapterText = sanitizeChapterText(chapter.chapterText, {
|
||||
removeExtraParagraphSpacing,
|
||||
bionicReading,
|
||||
sourceId: sourceId,
|
||||
});
|
||||
const scrollToSavedProgress = useCallback(
|
||||
() => scrollTo(position?.position),
|
||||
[],
|
||||
);
|
||||
|
||||
const chapterText = useMemo(
|
||||
() =>
|
||||
sanitizeChapterText(chapter.chapterText, {
|
||||
removeExtraParagraphSpacing,
|
||||
bionicReading,
|
||||
sourceId,
|
||||
}),
|
||||
[chapter.chapterText, removeExtraParagraphSpacing, bionicReading, sourceId],
|
||||
);
|
||||
const openDrawer = () => {
|
||||
navigation.openDrawer();
|
||||
hideHeader();
|
||||
};
|
||||
|
||||
const bookmarkChapter = () =>
|
||||
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
|
||||
|
||||
if (loading) {
|
||||
return <ChapterLoadingScreen />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorScreenV2 error={error} />;
|
||||
return (
|
||||
<ErrorScreenV2
|
||||
error={error}
|
||||
actions={[
|
||||
{
|
||||
iconName: 'refresh',
|
||||
title: getString('common.retry'),
|
||||
onPress: () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
getChapter(chapterId);
|
||||
},
|
||||
},
|
||||
{
|
||||
iconName: 'earth',
|
||||
title: 'WebView',
|
||||
onPress: () =>
|
||||
navigate('WebviewScreen', {
|
||||
sourceId,
|
||||
name: `${chapterName} | ${novelName}`,
|
||||
url: chapterUrl,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const bookmarkChapter = () =>
|
||||
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebViewReader
|
||||
@@ -343,7 +425,9 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
html={chapterText}
|
||||
chapterName={chapter.chapterName || chapterName}
|
||||
swipeGestures={swipeGestures}
|
||||
readerPages={readerPages}
|
||||
minScroll={minScroll}
|
||||
pages={pages}
|
||||
nextChapter={nextChapter}
|
||||
webViewRef={webViewRef}
|
||||
onLayout={onLayout}
|
||||
@@ -373,7 +457,9 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
/>
|
||||
<ReaderSeekBar
|
||||
theme={theme}
|
||||
readerPages={readerPages}
|
||||
minScroll={minScroll.current}
|
||||
pages={pages.current}
|
||||
verticalSeekbar={verticalSeekbar}
|
||||
percentage={position?.percentage}
|
||||
scrollTo={scrollTo}
|
||||
|
||||
@@ -99,11 +99,24 @@ const ChapterDrawer = ({ state: st, navigation }) => {
|
||||
onPress={() => changeChapter(item)}
|
||||
style={styles.chapterCtn}
|
||||
>
|
||||
<Text numberOfLines={1} style={styles.chapterNameCtn}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
styles.chapterNameCtn,
|
||||
{ color: item.read ? theme.outline : theme.onSurface },
|
||||
]}
|
||||
>
|
||||
{item.chapterName}
|
||||
</Text>
|
||||
{item?.releaseDate ? (
|
||||
<Text style={styles.releaseDateCtn}>{item.releaseDate}</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.releaseDateCtn,
|
||||
{ color: item.read ? theme.outline : theme.onSurfaceVariant },
|
||||
]}
|
||||
>
|
||||
{item.releaseDate}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -226,11 +239,9 @@ const createStylesheet = theme => {
|
||||
chapterNameCtn: {
|
||||
fontSize: 12,
|
||||
marginBottom: 2,
|
||||
color: theme.onSurface,
|
||||
},
|
||||
releaseDateCtn: {
|
||||
fontSize: 10,
|
||||
color: theme.onSurfaceVariant,
|
||||
},
|
||||
drawerElementContainer: {
|
||||
margin: 4,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import React, { Ref, useMemo, useState } from 'react';
|
||||
import color from 'color';
|
||||
|
||||
import { BottomSheetView, BottomSheetModal } from '@gorhom/bottom-sheet';
|
||||
import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import BottomSheet from '@components/BottomSheet/BottomSheet';
|
||||
@@ -25,6 +25,8 @@ import ReaderTextAlignSelector from './ReaderTextAlignSelector';
|
||||
import ReaderLineHeight from './ReaderLineHeight';
|
||||
import ReaderFontPicker from './ReaderFontPicker';
|
||||
import { overlay } from 'react-native-paper';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const ReaderTab: React.FC = () => {
|
||||
return (
|
||||
@@ -50,13 +52,14 @@ const GeneralTab: React.FC = () => {
|
||||
useVolumeButtons = false,
|
||||
scrollAmount = 200,
|
||||
swipeGestures = false,
|
||||
readerPages = false,
|
||||
removeExtraParagraphSpacing = false,
|
||||
addChapterNameInReader = false,
|
||||
bionicReading = false,
|
||||
} = useSettingsV1();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.fullscreen')}
|
||||
onPress={() =>
|
||||
@@ -105,6 +108,12 @@ const GeneralTab: React.FC = () => {
|
||||
value={swipeGestures}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.readerPages')}
|
||||
onPress={() => dispatch(setAppSettings('readerPages', !readerPages))}
|
||||
value={readerPages}
|
||||
theme={theme}
|
||||
/>
|
||||
<ReaderSheetPreferenceItem
|
||||
label={getString('readerScreen.bottomSheet.removeExtraSpacing')}
|
||||
onPress={() =>
|
||||
@@ -161,7 +170,7 @@ const GeneralTab: React.FC = () => {
|
||||
value={bionicReading}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
interface ReaderBottomSheetV2Props {
|
||||
@@ -198,31 +207,37 @@ const ReaderBottomSheetV2: React.FC<ReaderBottomSheetV2Props> = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const renderTabBar = (props: any) => (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{ backgroundColor: theme.primary }}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: tabHeaderColor,
|
||||
borderBottomColor: theme.outline,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
styles.tabBar,
|
||||
]}
|
||||
renderLabel={({ route, color }) => (
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.onSurfaceVariant}
|
||||
activeColor={theme.primary}
|
||||
pressColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTabBar = (props: any) => {
|
||||
return (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{ backgroundColor: theme.primary }}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: tabHeaderColor,
|
||||
borderBottomColor: theme.outline,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
styles.tabBar,
|
||||
]}
|
||||
renderLabel={({ route, color }) => (
|
||||
<Text style={{ color }}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.onSurfaceVariant}
|
||||
activeColor={theme.primary}
|
||||
pressColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
return (
|
||||
<BottomSheet bottomSheetRef={bottomSheetRef} snapPoints={[360, 600]}>
|
||||
<BottomSheetView
|
||||
style={[styles.bottomSheetContainer, { backgroundColor }]}
|
||||
<BottomSheetScrollView
|
||||
contentContainerStyle={[
|
||||
styles.bottomSheetContainer,
|
||||
{ backgroundColor },
|
||||
{ marginBottom: bottom },
|
||||
]}
|
||||
>
|
||||
<TabView
|
||||
navigationState={{ index, routes }}
|
||||
@@ -232,7 +247,7 @@ const ReaderBottomSheetV2: React.FC<ReaderBottomSheetV2Props> = ({
|
||||
initialLayout={{ width: layout.width }}
|
||||
style={styles.tabView}
|
||||
/>
|
||||
</BottomSheetView>
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
+27
-7
@@ -5,19 +5,39 @@ import color from 'color';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useDeviceOrientation } from '@hooks/useDeviceOrientation';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
|
||||
const VerticalScrollbar = ({
|
||||
interface IScrollbar {
|
||||
theme: ThemeColors;
|
||||
minScroll: number;
|
||||
pages: number;
|
||||
verticalSeekbar: boolean;
|
||||
readerPages: boolean;
|
||||
percentage?: number;
|
||||
scrollTo: (offset: number) => void;
|
||||
}
|
||||
|
||||
const VerticalScrollbar: React.FC<IScrollbar> = ({
|
||||
theme,
|
||||
minScroll = 0,
|
||||
pages,
|
||||
verticalSeekbar,
|
||||
readerPages,
|
||||
percentage,
|
||||
scrollTo,
|
||||
}) => {
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
const onSlidingComplete = value => {
|
||||
let offsetY =
|
||||
((value - minScroll) * Dimensions.get('window').height) / minScroll;
|
||||
scrollTo(offsetY);
|
||||
const onSlidingComplete = (value: number) => {
|
||||
let offset;
|
||||
if (readerPages) {
|
||||
let newPage = Math.round(pages * (value / 100)) * 100;
|
||||
// pages = { ...pages, current: newPage };
|
||||
offset = newPage;
|
||||
} else {
|
||||
offset =
|
||||
((value - minScroll) * Dimensions.get('window').height) / minScroll;
|
||||
}
|
||||
scrollTo(offset);
|
||||
};
|
||||
const screenOrientation = useDeviceOrientation();
|
||||
|
||||
@@ -43,7 +63,7 @@ const VerticalScrollbar = ({
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
minimumValue={Math.round(minScroll)}
|
||||
minimumValue={readerPages ? 1 : Math.round(minScroll)}
|
||||
maximumValue={100}
|
||||
step={1}
|
||||
value={percentage || Math.round(minScroll)}
|
||||
@@ -87,7 +107,7 @@ const VerticalScrollbar = ({
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
minimumValue={Math.round(minScroll)}
|
||||
minimumValue={readerPages ? 1 : Math.round(minScroll)}
|
||||
maximumValue={100}
|
||||
step={1}
|
||||
value={percentage || Math.round(minScroll)}
|
||||
@@ -10,6 +10,9 @@ import { useReaderSettings, useSettingsV1 } from '@redux/hooks';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { sourceManager } from '../../../sources/sourceManager';
|
||||
import { createHorizontalReaderPages } from './stringCreators/horizontalReaderPages';
|
||||
import { createStyles } from './stringCreators/createStyle';
|
||||
import { createSwipeGestures } from './stringCreators/createSwipeGestures';
|
||||
|
||||
type WebViewPostEvent = {
|
||||
type: string;
|
||||
@@ -30,7 +33,9 @@ type WebViewReaderProps = {
|
||||
html: string;
|
||||
chapterName: string;
|
||||
swipeGestures: boolean;
|
||||
readerPages: boolean;
|
||||
minScroll: React.MutableRefObject<number>;
|
||||
pages: React.MutableRefObject<number>;
|
||||
nextChapter: ChapterItem;
|
||||
webViewRef: React.MutableRefObject<WebView>;
|
||||
onPress(): void;
|
||||
@@ -52,9 +57,11 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
html,
|
||||
chapterName,
|
||||
swipeGestures,
|
||||
readerPages,
|
||||
minScroll,
|
||||
nextChapter,
|
||||
webViewRef,
|
||||
pages,
|
||||
onPress,
|
||||
onLayout,
|
||||
doSaveProgress,
|
||||
@@ -112,6 +119,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
if (event.data) {
|
||||
const offSetY = Number(event.data?.offSetY);
|
||||
const percentage = Math.round(Number(event.data?.percentage));
|
||||
|
||||
doSaveProgress(offSetY, percentage);
|
||||
}
|
||||
break;
|
||||
@@ -121,6 +129,11 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
minScroll.current = (layoutHeight / contentHeight) * 100;
|
||||
}
|
||||
break;
|
||||
case 'pages':
|
||||
if (event.data) {
|
||||
pages.current = Number(event.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
source={{
|
||||
@@ -128,85 +141,13 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<style>
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
padding-bottom: 40px;
|
||||
|
||||
font-size: ${readerSettings.textSize}px;
|
||||
color: ${readerSettings.textColor};
|
||||
text-align: ${readerSettings.textAlign};
|
||||
line-height: ${readerSettings.lineHeight};
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
}
|
||||
chapter{
|
||||
display: block;
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.primary};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
img.load-icon {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
animation: rotation 1s infinite linear;
|
||||
}
|
||||
@keyframes rotation {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
.nextButton,
|
||||
.infoText {
|
||||
width: 100%;
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: ${theme.onPrimary};
|
||||
background-color: ${theme.primary};
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
}
|
||||
.nextButton {
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.infoText {
|
||||
background-color: transparent;
|
||||
text-align:center;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
color: inherit;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.chapterCtn {
|
||||
min-height: ${layoutHeight - 140};
|
||||
margin-bottom: auto;
|
||||
}
|
||||
</style>
|
||||
${createStyles(
|
||||
StatusBar.currentHeight ?? 0,
|
||||
readerSettings,
|
||||
theme,
|
||||
layoutHeight,
|
||||
readerPages,
|
||||
)}
|
||||
|
||||
<style>
|
||||
${readerSettings.customCSS}
|
||||
@@ -220,10 +161,22 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
</style>
|
||||
</head>
|
||||
<body id='sourceId-${chapterInfo.sourceId}'>
|
||||
<div class="chapterCtn" ${onClickWebViewPostMessage({
|
||||
type: 'hide',
|
||||
})}>
|
||||
<div class="chapterCtn" ${
|
||||
!readerPages &&
|
||||
onClickWebViewPostMessage({
|
||||
type: 'hide',
|
||||
})
|
||||
}>
|
||||
<div id="left"></div>
|
||||
<div id="right"></div>
|
||||
<div id="middle" ${
|
||||
readerPages &&
|
||||
onClickWebViewPostMessage({
|
||||
type: 'hide',
|
||||
})
|
||||
}></div>
|
||||
<chapter
|
||||
data-page=0
|
||||
data-novel-id='${chapterInfo.novelId}'
|
||||
data-chapter-id='${chapterInfo.chapterId}'
|
||||
>
|
||||
@@ -233,7 +186,29 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
: ''
|
||||
}
|
||||
${html}
|
||||
<p id="spacer"></p>
|
||||
|
||||
</chapter>
|
||||
<div id="infoContainer" class="hide">
|
||||
<div class="infoText">
|
||||
${getString(
|
||||
'readerScreen.finished',
|
||||
)}: ${chapterName?.trim()}
|
||||
</div>
|
||||
${
|
||||
nextChapter
|
||||
? `<button class="nextButton"
|
||||
${onClickWebViewPostMessage({
|
||||
type: 'next',
|
||||
})}>
|
||||
Next: ${nextChapter.chapterName}
|
||||
</button>`
|
||||
: `<div class="infoText">${getString(
|
||||
'readerScreen.noNextChapter',
|
||||
)}</div>`
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
if(!document.querySelector("input[offline]") && ${!!headers}){
|
||||
@@ -241,9 +216,8 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({type:"imgfile",data:img.getAttribute("delayed-src")}));
|
||||
});
|
||||
}
|
||||
|
||||
var scrollTimeout;
|
||||
window.addEventListener("scroll", (event) => {
|
||||
${!readerPages} && window.addEventListener("scroll", (event) => {
|
||||
window.clearTimeout( scrollTimeout );
|
||||
scrollTimeout = setTimeout(() => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
@@ -259,65 +233,31 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
let sendHeightTimeout;
|
||||
const sendHeight = (timeOut) => {
|
||||
clearTimeout(sendHeightTimeout);
|
||||
sendHeightTimeout = setTimeout(
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({type:"height",data:document.body.scrollHeight})
|
||||
JSON.stringify({type:"height",data: document.body.scrollHeight})
|
||||
), timeOut
|
||||
);
|
||||
}
|
||||
sendHeight(1000);
|
||||
</script>
|
||||
<div class="infoText">
|
||||
${getString(
|
||||
'readerScreen.finished',
|
||||
)}: ${chapterName?.trim()}
|
||||
</div>
|
||||
${
|
||||
nextChapter
|
||||
? `<button class="nextButton" ${onClickWebViewPostMessage(
|
||||
{ type: 'next' },
|
||||
)}>
|
||||
Next: ${nextChapter.chapterName}
|
||||
</button>`
|
||||
: `<div class="infoText">${getString(
|
||||
'readerScreen.noNextChapter',
|
||||
)}</div>`
|
||||
}
|
||||
${
|
||||
swipeGestures
|
||||
? `
|
||||
<script>
|
||||
let initialX = null;
|
||||
let initialY = null;
|
||||
document.addEventListener("touchstart", e => {
|
||||
initialX = e.changedTouches[0].screenX;
|
||||
initialY = e.changedTouches[0].screenY;
|
||||
});
|
||||
document.addEventListener("touchend", e => {
|
||||
let diffX = e.changedTouches[0].screenX - initialX;
|
||||
let diffY = e.changedTouches[0].screenY - initialY;
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
|
||||
e.preventDefault();
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({type: diffX<0 ? "next" : "prev"}))
|
||||
}
|
||||
});
|
||||
</script>`
|
||||
: ''
|
||||
}
|
||||
|
||||
|
||||
${swipeGestures ? createSwipeGestures() : ''}
|
||||
<script>
|
||||
async function fn(){
|
||||
// Position important to prevent layout bugs
|
||||
${readerPages && createHorizontalReaderPages()}
|
||||
let novelName = "${chapterInfo.novelName}";
|
||||
let chapterName = "${chapterInfo.chapterName}";
|
||||
let sourceId =${chapterInfo.sourceId};
|
||||
let chapterId =${chapterInfo.chapterId};
|
||||
let novelId =${chapterInfo.novelId};
|
||||
let html = document.querySelector("chapter").innerHTML;
|
||||
${readerSettings.customJS}
|
||||
|
||||
${readerSettings.customJS}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", fn);
|
||||
</script>
|
||||
@@ -328,5 +268,4 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(WebViewReader, isEqual);
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { ThemeColors } from '@theme/types';
|
||||
|
||||
export const createStyles = (
|
||||
currentHeight: number,
|
||||
readerSettings: {
|
||||
theme: string;
|
||||
textColor: string;
|
||||
textSize: number;
|
||||
textAlign: string;
|
||||
padding: number;
|
||||
fontFamily: string;
|
||||
lineHeight: number;
|
||||
customCSS: string;
|
||||
customJS: string;
|
||||
},
|
||||
theme: ThemeColors,
|
||||
layoutHeight: number,
|
||||
readerPages: boolean,
|
||||
) => `
|
||||
<style>
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: ${currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-bottom: 40px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-size: ${readerSettings.textSize}px;
|
||||
color: ${readerSettings.textColor};
|
||||
text-align: ${readerSettings.textAlign};
|
||||
line-height: ${readerSettings.lineHeight};
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
}
|
||||
chapter{
|
||||
display: block;
|
||||
}
|
||||
|
||||
chapter > *{
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.primary};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
img.load-icon {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
animation: rotation 1s infinite linear;
|
||||
}
|
||||
@keyframes rotation {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
#infoContainer {
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
}
|
||||
.nextButton,
|
||||
.infoText {
|
||||
width: 100%;
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: ${theme.onPrimary};
|
||||
background-color: ${theme.primary};
|
||||
font-family: ${readerSettings.fontFamily};
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
}
|
||||
.nextButton {
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.infoText {
|
||||
background-color: transparent;
|
||||
text-align:center;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
color: inherit;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.chapterCtn {
|
||||
min-height: ${layoutHeight - 140};
|
||||
margin-bottom: auto;
|
||||
}
|
||||
${
|
||||
readerPages &&
|
||||
`
|
||||
body {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
#left,
|
||||
#right, #middle {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 35%;
|
||||
top: 0;
|
||||
z-index: 20
|
||||
}
|
||||
#middle {
|
||||
left: 35%;
|
||||
width: 30%;
|
||||
}
|
||||
#left {
|
||||
left: 0;
|
||||
}
|
||||
#right {
|
||||
right: 0;
|
||||
}
|
||||
#infoContainer {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 40;
|
||||
width: calc(100% - 2*${readerSettings.padding}%)
|
||||
}
|
||||
.nextButton{
|
||||
position: relative;
|
||||
z-index: 100000 !important
|
||||
}
|
||||
chapter {
|
||||
height: 100%;
|
||||
display: flexbox;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 0;
|
||||
column-width: 100vw;
|
||||
transition: 200ms;
|
||||
}
|
||||
chapter > * {
|
||||
width: calc(100% - 2*${readerSettings.padding}%) !important;
|
||||
}
|
||||
#spacer {
|
||||
display: block;
|
||||
min-height: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
.hide {
|
||||
transform: translate(110%);
|
||||
transition: 200ms
|
||||
}
|
||||
.show {
|
||||
transform: translate(0%);
|
||||
}
|
||||
`
|
||||
}
|
||||
</style>`;
|
||||
@@ -0,0 +1,18 @@
|
||||
export function createSwipeGestures() {
|
||||
return `<script>
|
||||
let initialX = null;
|
||||
let initialY = null;
|
||||
document.addEventListener("touchstart", e => {
|
||||
initialX = e.changedTouches[0].screenX;
|
||||
initialY = e.changedTouches[0].screenY;
|
||||
});
|
||||
document.addEventListener("touchend", e => {
|
||||
let diffX = e.changedTouches[0].screenX - initialX;
|
||||
let diffY = e.changedTouches[0].screenY - initialY;
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
|
||||
e.preventDefault();
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({type: diffX<0 ? "next" : "prev"}))
|
||||
}
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
export const createHorizontalReaderPages = () => {
|
||||
return `
|
||||
const id = (key) => {
|
||||
return document.getElementById(key)
|
||||
}
|
||||
const select = (key) => {
|
||||
return document.querySelector(key)
|
||||
}
|
||||
|
||||
const chapter = select("chapter");
|
||||
const clientWidth = document.documentElement.clientWidth;
|
||||
const textWidth = chapter.scrollWidth;
|
||||
const navLeft = id("left");
|
||||
const navRight = id("right");
|
||||
const infoBox = id("infoContainer");
|
||||
infoBox.classList.add("hidden");
|
||||
id("spacer").style.height = infoBox.scrollHeight + 'px';
|
||||
|
||||
const pages = (Math.ceil(textWidth / clientWidth) - 1);
|
||||
let page = 0;
|
||||
|
||||
|
||||
navRight.addEventListener("click", () => {
|
||||
page = select("chapter").getAttribute('data-page');
|
||||
if (isNaN(page)) {
|
||||
page = 0;
|
||||
}
|
||||
if (page < pages ) {
|
||||
page++;
|
||||
movePage();
|
||||
if (page === pages){
|
||||
infoBox.classList.add("show")
|
||||
}
|
||||
}
|
||||
});
|
||||
navLeft.addEventListener("click", () => {
|
||||
page = select("chapter").getAttribute('data-page');
|
||||
if (page > 0) {
|
||||
infoBox.classList.remove("show")
|
||||
page--;
|
||||
movePage();
|
||||
}
|
||||
})
|
||||
function movePage(){
|
||||
chapter.style.transform = 'translate(-'+page*100+'%)';
|
||||
select('chapter').setAttribute('data-page',
|
||||
page
|
||||
);
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify(
|
||||
{
|
||||
type:"scrollend",
|
||||
data:{
|
||||
offSetY: page * 100, percentage: page === 0 ? 1 : page / pages * 100,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
let sendWidthTimeout;
|
||||
const sendPages = (timeOut) => {
|
||||
clearTimeout(sendHeightTimeout);
|
||||
sendHeightTimeout = setTimeout(
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({type:"pages",data: pages})
|
||||
), timeOut
|
||||
);
|
||||
}
|
||||
sendPages(200);
|
||||
;`;
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export const sanitizeChapterText = (
|
||||
html: string,
|
||||
options?: Options,
|
||||
): string => {
|
||||
const isEPubSource = options?.sourceId === 0;
|
||||
let text = sanitizeHtml(html, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
|
||||
'img',
|
||||
@@ -24,13 +25,17 @@ export const sanitizeChapterText = (
|
||||
'em',
|
||||
'b',
|
||||
'a',
|
||||
'center',
|
||||
...(isEPubSource ? ['head', 'title', 'style'] : []),
|
||||
]),
|
||||
allowedAttributes: {
|
||||
'img': ['src', 'class'],
|
||||
'a': ['href'],
|
||||
'input': ['type', 'offline'],
|
||||
...(isEPubSource ? { '*': ['class'] } : {}),
|
||||
},
|
||||
allowedSchemes: ['data', 'http', 'https', 'file'],
|
||||
allowVulnerableTags: isEPubSource,
|
||||
});
|
||||
if (text) {
|
||||
if (options?.removeExtraParagraphSpacing) {
|
||||
@@ -42,23 +47,22 @@ export const sanitizeChapterText = (
|
||||
text = textVide(text);
|
||||
}
|
||||
|
||||
const loadedCheerio = loadCheerio(text);
|
||||
if (
|
||||
options?.sourceId &&
|
||||
sourceManager(options.sourceId).headers &&
|
||||
loadedCheerio('input[offline]').length === 0
|
||||
) {
|
||||
loadedCheerio('img').each((i, element) => {
|
||||
const src = loadedCheerio(element).attr('src');
|
||||
if (src) {
|
||||
loadedCheerio(element).attr({
|
||||
'src': LoadingImageSrc,
|
||||
'class': 'load-icon',
|
||||
'delayed-src': src,
|
||||
});
|
||||
}
|
||||
});
|
||||
text = loadedCheerio('body').html() || text;
|
||||
if (options?.sourceId && sourceManager(options.sourceId).headers) {
|
||||
// Some documents might take a few seconds to be parsed, only do when necessary
|
||||
const loadedCheerio = loadCheerio(text);
|
||||
if (loadedCheerio('input[offline]').length === 0) {
|
||||
loadedCheerio('img').each((i, element) => {
|
||||
const src = loadedCheerio(element).attr('src');
|
||||
if (src) {
|
||||
loadedCheerio(element).attr({
|
||||
'src': LoadingImageSrc,
|
||||
'class': 'load-icon',
|
||||
'delayed-src': src,
|
||||
});
|
||||
}
|
||||
});
|
||||
text = loadedCheerio('body').html() || text;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
@@ -12,13 +12,18 @@ import { getString } from '@strings/translations';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import ConfirmationDialog from '@components/ConfirmationDialog/ConfirmationDialog';
|
||||
import { deleteReadChaptersFromDb } from '../../database/queries/DownloadQueries';
|
||||
import { openDirectory } from '../../native/epubParser';
|
||||
|
||||
import { Appbar, Button, List } from '@components';
|
||||
import useSourceStorage from '@hooks/useSourceStorage';
|
||||
import { ScreenContainer } from '@components/Common';
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
|
||||
const AdvancedSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const { clearCookies } = useSourceStorage({});
|
||||
const clearCookies = () => {
|
||||
CookieManager.clearAll();
|
||||
showToast('Cookies cleared');
|
||||
};
|
||||
|
||||
/**
|
||||
* Confirm Clear Database Dialog
|
||||
@@ -38,113 +43,124 @@ const AdvancedSettings = ({ navigation }) => {
|
||||
} = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar
|
||||
title="Advanced"
|
||||
handleGoBack={() => navigation.goBack()}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Data Management</List.SubHeader>
|
||||
<List.Item
|
||||
title="Clear database"
|
||||
description="Delete history for novels not in your library"
|
||||
onPress={showClearDatabaseDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Clear updates tab"
|
||||
description="Clears chapter entries in updates tab"
|
||||
onPress={showClearUpdatesDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Clean up cached covers"
|
||||
description="Delete all covers cached"
|
||||
onPress={clearCoverCache}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('advancedSettings.deleteReadChapters')}
|
||||
onPress={showDeleteReadChaptersDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item title="Clear cookies" onPress={clearCookies} theme={theme} />
|
||||
</List.Section>
|
||||
<Portal>
|
||||
<ConfirmationDialog
|
||||
title={getString('advancedSettings.deleteReadChaptersDialogTitle')}
|
||||
visible={deleteReadChaptersDialog}
|
||||
onSubmit={deleteReadChaptersFromDb}
|
||||
onDismiss={hideDeleteReadChaptersDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<Dialog
|
||||
visible={clearDatabaseDialog}
|
||||
onDismiss={hideClearDatabaseDialog}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
backgroundColor: theme.overlay3,
|
||||
}}
|
||||
>
|
||||
<Dialog.Title
|
||||
<ScrollView>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Data Management</List.SubHeader>
|
||||
<List.Item
|
||||
title="Clear database"
|
||||
description="Delete history for novels not in your library"
|
||||
onPress={showClearDatabaseDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Clear updates tab"
|
||||
description="Clears chapter entries in updates tab"
|
||||
onPress={showClearUpdatesDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Clean up cached covers"
|
||||
description="Delete all covers cached"
|
||||
onPress={clearCoverCache}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('advancedSettings.deleteReadChapters')}
|
||||
onPress={showDeleteReadChaptersDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Clear cookies"
|
||||
onPress={clearCookies}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Import Epub"
|
||||
onPress={() => openDirectory()}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
<Portal>
|
||||
<ConfirmationDialog
|
||||
title={getString('advancedSettings.deleteReadChaptersDialogTitle')}
|
||||
visible={deleteReadChaptersDialog}
|
||||
onSubmit={deleteReadChaptersFromDb}
|
||||
onDismiss={hideDeleteReadChaptersDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<Dialog
|
||||
visible={clearDatabaseDialog}
|
||||
onDismiss={hideClearDatabaseDialog}
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
lineHeight: 16 * 1.5,
|
||||
color: theme.onSurface,
|
||||
borderRadius: 28,
|
||||
backgroundColor: theme.overlay3,
|
||||
}}
|
||||
>
|
||||
Are you sure? Read chapters and progress of non-library novels will
|
||||
be lost.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={hideClearDatabaseDialog}>
|
||||
{getString('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
deleteNovelCache();
|
||||
hideClearDatabaseDialog();
|
||||
<Dialog.Title
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
lineHeight: 16 * 1.5,
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
>
|
||||
{getString('common.ok')}
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
visible={clearUpdatesDialog}
|
||||
onDismiss={hideClearUpdatesDialog}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
backgroundColor: theme.overlay3,
|
||||
}}
|
||||
>
|
||||
<Dialog.Title
|
||||
Are you sure? Read chapters and progress of non-library novels
|
||||
will be lost.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={hideClearDatabaseDialog}>
|
||||
{getString('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
deleteNovelCache();
|
||||
hideClearDatabaseDialog();
|
||||
}}
|
||||
>
|
||||
{getString('common.ok')}
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
visible={clearUpdatesDialog}
|
||||
onDismiss={hideClearUpdatesDialog}
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
lineHeight: 16 * 1.5,
|
||||
color: theme.onSurface,
|
||||
borderRadius: 28,
|
||||
backgroundColor: theme.overlay3,
|
||||
}}
|
||||
>
|
||||
Are you sure? Updates tab will be cleared.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={hideClearUpdatesDialog}>Cancel</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
clearUpdates();
|
||||
showToast('Updates cleared.');
|
||||
hideClearUpdatesDialog();
|
||||
<Dialog.Title
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
lineHeight: 16 * 1.5,
|
||||
color: theme.onSurface,
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
</>
|
||||
Are you sure? Updates tab will be cleared.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={hideClearUpdatesDialog}>Cancel</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
clearUpdates();
|
||||
showToast('Updates cleared.');
|
||||
hideClearUpdatesDialog();
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
</ScrollView>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import NovelBadgesModal from './modals/NovelBadgesModal';
|
||||
import { RootState } from '@redux/store';
|
||||
import { NavigationState } from '@react-navigation/native';
|
||||
import { getString } from '@strings/translations';
|
||||
import ChooseEpubLocationModal from '@screens/novel/components/Modal/ChooseEpubLocationModal';
|
||||
import ChooseEpubLocationModal from '@screens/novel/components/ChooseEpubLocationModal';
|
||||
|
||||
interface GenralSettingsProps {
|
||||
navigation: NavigationState;
|
||||
@@ -62,6 +62,7 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
|
||||
refreshNovelMetadata = false,
|
||||
disableHapticFeedback = false,
|
||||
useLibraryFAB = false,
|
||||
jumpToLastReadChapter = false,
|
||||
} = useSettings();
|
||||
|
||||
const { showLastUpdateTime = true } = useSelector(
|
||||
@@ -105,10 +106,12 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
|
||||
* Chapter Sort Modal
|
||||
*/
|
||||
const defaultChapterSortModal = useBoolean();
|
||||
|
||||
/**
|
||||
* Convert to EPUB Modal
|
||||
*/
|
||||
const convertToEpubModal = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar
|
||||
@@ -213,6 +216,18 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
|
||||
onPress={defaultChapterSortModal.setTrue}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label={getString(
|
||||
'moreScreen.settingsScreen.generalSettingsScreen.jumpToLastReadChapter',
|
||||
)}
|
||||
value={jumpToLastReadChapter}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('jumpToLastReadChapter', !jumpToLastReadChapter),
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString(
|
||||
@@ -263,7 +278,6 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString('moreScreen.settingsScreen.generalSettingsScreen.epub')}
|
||||
</List.SubHeader>
|
||||
@@ -343,9 +357,8 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
|
||||
theme={theme}
|
||||
/>
|
||||
<ChooseEpubLocationModal
|
||||
isVisible={convertToEpubModal.value}
|
||||
hideModal={convertToEpubModal.setFalse}
|
||||
modalVisible={convertToEpubModal.value}
|
||||
dispatchConfig
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,118 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import { KeyboardAvoidingView, StyleSheet, Text, View } from 'react-native';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Modal, overlay, TextInput } from 'react-native-paper';
|
||||
import { Button } from '@components/index';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
import { getString } from '@strings/translations';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import { StorageAccessFramework } from 'expo-file-system';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { setReaderSettings } from '@redux/settings/settings.actions';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
|
||||
interface CustomCSSModalProps {
|
||||
import { Button } from '@components/index';
|
||||
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
interface CustomFileModal {
|
||||
visible: boolean;
|
||||
onDismiss: () => void;
|
||||
customFile: string;
|
||||
setCustomFile: (val: string) => void;
|
||||
theme: MD3ThemeType;
|
||||
defaultValue: string;
|
||||
onSave: (val: string) => void;
|
||||
title: string;
|
||||
type?: string;
|
||||
openFileButtonLabel?: string;
|
||||
mimeType: 'text/css' | 'application/javascript';
|
||||
openFileLabel: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const CustomFileModal: React.FC<CustomCSSModalProps> = ({
|
||||
const CustomFileModal: React.FC<CustomFileModal> = ({
|
||||
onDismiss,
|
||||
visible,
|
||||
customFile,
|
||||
setCustomFile,
|
||||
theme,
|
||||
defaultValue,
|
||||
onSave,
|
||||
title,
|
||||
type = 'CSS',
|
||||
openFileButtonLabel = getString(
|
||||
'moreScreen.settingsScreen.readerSettings.openCSSFile',
|
||||
),
|
||||
placeholder = '',
|
||||
mimeType,
|
||||
openFileLabel,
|
||||
placeholder,
|
||||
description,
|
||||
}) => {
|
||||
const [text, setText] = useState(customFile);
|
||||
const dispatch = useAppDispatch();
|
||||
let mimeType: string;
|
||||
if (type === 'CSS') {
|
||||
mimeType = 'text/css';
|
||||
} else {
|
||||
mimeType = 'application/javascript';
|
||||
}
|
||||
const theme = useTheme();
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const openCSS = async () => {
|
||||
const openDocumentPicker = async () => {
|
||||
try {
|
||||
const rawCSS = await DocumentPicker.getDocumentAsync({
|
||||
const file = await DocumentPicker.getDocumentAsync({
|
||||
copyToCacheDirectory: false,
|
||||
type: mimeType,
|
||||
});
|
||||
if (rawCSS.type === 'success') {
|
||||
let css = await StorageAccessFramework.readAsStringAsync(rawCSS.uri);
|
||||
setText(css);
|
||||
|
||||
if (file.type === 'success') {
|
||||
const content = await StorageAccessFramework.readAsStringAsync(
|
||||
file.uri,
|
||||
);
|
||||
|
||||
onSave(content.trim());
|
||||
onDismiss();
|
||||
}
|
||||
} catch (error: any) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
const dismiss = () => {
|
||||
setCustomFile(text.trim());
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onDismiss={dismiss}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={180}>
|
||||
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[{ color: theme.onSurfaceVariant }]}>
|
||||
{type === 'CSS'
|
||||
? getString('moreScreen.settingsScreen.readerSettings.cssHint')
|
||||
: getString('moreScreen.settingsScreen.readerSettings.jsHint')}
|
||||
</Text>
|
||||
<TextInput
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.outline}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
mode="outlined"
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.onSurfaceVariant}
|
||||
multiline
|
||||
style={[
|
||||
{ color: theme.onSurface },
|
||||
styles.fontSizeL,
|
||||
styles.customCSSContainer,
|
||||
]}
|
||||
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[{ color: theme.onSurfaceVariant }]}>{description}</Text>
|
||||
<TextInput
|
||||
multiline
|
||||
mode="outlined"
|
||||
defaultValue={defaultValue}
|
||||
onChangeText={setText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.onSurfaceVariant}
|
||||
underlineColor={theme.outline}
|
||||
style={[{ color: theme.onSurface }, styles.textInput]}
|
||||
theme={{ colors: { ...theme } }}
|
||||
/>
|
||||
<View style={styles.customCSSButtons}>
|
||||
<Button
|
||||
onPress={() => {
|
||||
onSave(text.trim());
|
||||
onDismiss();
|
||||
}}
|
||||
style={styles.button}
|
||||
title={getString('common.save')}
|
||||
mode="contained"
|
||||
/>
|
||||
<View style={styles.customCSSButtons}>
|
||||
<Button
|
||||
onPress={() => {
|
||||
dispatch(setReaderSettings(`custom${type}`, text.trim()));
|
||||
dismiss();
|
||||
}}
|
||||
style={styles.button}
|
||||
title={getString('common.save')}
|
||||
mode="contained"
|
||||
/>
|
||||
<Button
|
||||
style={styles.button}
|
||||
onPress={openCSS}
|
||||
title={openFileButtonLabel}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
<Button
|
||||
style={styles.button}
|
||||
onPress={openDocumentPicker}
|
||||
title={openFileLabel}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -125,14 +108,12 @@ const styles = StyleSheet.create({
|
||||
padding: 24,
|
||||
borderRadius: 28,
|
||||
},
|
||||
fontSizeL: {
|
||||
fontSize: 16,
|
||||
},
|
||||
customCSSContainer: {
|
||||
textInput: {
|
||||
height: 220,
|
||||
borderRadius: 14,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
fontSize: 16,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 24,
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { StyleSheet, View, TextInput } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import { Portal } from 'react-native-paper';
|
||||
|
||||
import { Button, List } from '@components/index';
|
||||
|
||||
import { setReaderSettings } from '@redux/settings/settings.actions';
|
||||
import { Button, List, ConfirmationDialog } from '@components/index';
|
||||
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
|
||||
import { setReaderSettings } from '@redux/settings/settings.actions';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import CustomFileModal from '../Modals/CustomFileModal';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { Portal } from 'react-native-paper';
|
||||
|
||||
const CustomCSSSettings = () => {
|
||||
const theme = useTheme();
|
||||
const readerSettings = useReaderSettings();
|
||||
const [customCSS, setCustomCSS] = useState(readerSettings.customCSS);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const cssModal = useBoolean();
|
||||
const clearCSSModal = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -25,22 +26,11 @@ const CustomCSSSettings = () => {
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString('moreScreen.settingsScreen.readerSettings.customCSS')}
|
||||
</List.SubHeader>
|
||||
<List.SubHeader theme={theme}>
|
||||
{readerSettings.customCSS !== customCSS
|
||||
? getString('moreScreen.settingsScreen.readerSettings.notSaved')
|
||||
: null}
|
||||
</List.SubHeader>
|
||||
</View>
|
||||
<View style={styles.customCSSContainer}>
|
||||
<TextInput
|
||||
style={[{ color: theme.onSurface }, styles.fontSizeL]}
|
||||
value={customCSS}
|
||||
onChangeText={text => setCustomCSS(text)}
|
||||
placeholderTextColor={theme.onSurfaceVariant}
|
||||
placeholder="Example: body {margin: 10px;}"
|
||||
multiline={true}
|
||||
editable={false}
|
||||
/>
|
||||
<Text numberOfLines={3} style={[{ color: theme.onSurface }]}>
|
||||
{readerSettings.customCSS || 'Example: body {margin: 10px;}'}
|
||||
</Text>
|
||||
<View style={styles.customCSSButtons}>
|
||||
<Button
|
||||
onPress={cssModal.setTrue}
|
||||
@@ -48,28 +38,41 @@ const CustomCSSSettings = () => {
|
||||
title={getString('common.edit')}
|
||||
/>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setCustomCSS('');
|
||||
dispatch(setReaderSettings('customCSS', ''));
|
||||
}}
|
||||
onPress={clearCSSModal.setTrue}
|
||||
title={getString('common.clear')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{/*
|
||||
Modals
|
||||
*/}
|
||||
|
||||
{/* Modals */}
|
||||
<Portal>
|
||||
<CustomFileModal
|
||||
visible={cssModal.value}
|
||||
onDismiss={cssModal.setFalse}
|
||||
defaultValue={readerSettings.customCSS}
|
||||
mimeType="text/css"
|
||||
title={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.customCSS',
|
||||
)}
|
||||
visible={cssModal.value}
|
||||
onDismiss={cssModal.setFalse}
|
||||
theme={theme}
|
||||
customFile={customCSS}
|
||||
setCustomFile={setCustomCSS}
|
||||
description={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.cssHint',
|
||||
)}
|
||||
placeholder="Example: body {margin: 10px;}"
|
||||
openFileLabel={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.openCSSFile',
|
||||
)}
|
||||
onSave={text => dispatch(setReaderSettings('customCSS', text))}
|
||||
/>
|
||||
<ConfirmationDialog
|
||||
title={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.clearCustomCSS',
|
||||
)}
|
||||
visible={clearCSSModal.value}
|
||||
onSubmit={() => {
|
||||
dispatch(setReaderSettings('customCSS', ''));
|
||||
}}
|
||||
onDismiss={clearCSSModal.setFalse}
|
||||
theme={theme}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { StyleSheet, TextInput, View } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, List } from '@components/index';
|
||||
|
||||
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { getString } from '@strings/translations';
|
||||
import { setReaderSettings } from '@redux/settings/settings.actions';
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Portal } from 'react-native-paper';
|
||||
import CustomFileModal from '../Modals/CustomFileModal';
|
||||
|
||||
import { Button, List, ConfirmationDialog } from '@components/index';
|
||||
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
|
||||
import { setReaderSettings } from '@redux/settings/settings.actions';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import CustomFileModal from '../Modals/CustomFileModal';
|
||||
|
||||
interface CustomJSSettingsProps {
|
||||
readerSettings: ReturnType<typeof useReaderSettings>;
|
||||
@@ -21,8 +22,8 @@ const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [customJS, setCustomJS] = useState(readerSettings.customJS);
|
||||
const jsModal = useBoolean();
|
||||
const clearJSModal = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -30,21 +31,11 @@ const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString('moreScreen.settingsScreen.readerSettings.customJS')}
|
||||
</List.SubHeader>
|
||||
<List.SubHeader theme={theme}>
|
||||
{readerSettings.customJS !== customJS
|
||||
? getString('moreScreen.settingsScreen.readerSettings.notSaved')
|
||||
: null}
|
||||
</List.SubHeader>
|
||||
</View>
|
||||
<View style={styles.customJSContainer}>
|
||||
<TextInput
|
||||
style={[{ color: theme.onSurface }, styles.fontSizeL]}
|
||||
value={customJS}
|
||||
placeholderTextColor={theme.onSurfaceVariant}
|
||||
placeholder="Example: document.getElementById('example');"
|
||||
multiline={true}
|
||||
editable={false}
|
||||
/>
|
||||
<Text numberOfLines={3} style={[{ color: theme.onSurface }]}>
|
||||
{readerSettings.customJS || 'Example: body {margin: 10px;}'}
|
||||
</Text>
|
||||
<View style={styles.customJSButtons}>
|
||||
<Button
|
||||
onPress={jsModal.setTrue}
|
||||
@@ -52,30 +43,39 @@ const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
|
||||
title={getString('common.edit')}
|
||||
/>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setCustomJS('');
|
||||
dispatch(setReaderSettings('customJS', ''));
|
||||
}}
|
||||
onPress={clearJSModal.setTrue}
|
||||
title={getString('common.clear')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{/*
|
||||
Modals
|
||||
*/}
|
||||
|
||||
{/* Modals */}
|
||||
<Portal>
|
||||
<CustomFileModal
|
||||
title={getString('moreScreen.settingsScreen.readerSettings.customJS')}
|
||||
visible={jsModal.value}
|
||||
onDismiss={jsModal.setFalse}
|
||||
theme={theme}
|
||||
customFile={customJS}
|
||||
setCustomFile={setCustomJS}
|
||||
type="JS"
|
||||
openFileButtonLabel={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.openJSFile',
|
||||
defaultValue={readerSettings.customJS}
|
||||
mimeType="application/javascript"
|
||||
title={getString('moreScreen.settingsScreen.readerSettings.customJS')}
|
||||
description={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.jsHint',
|
||||
)}
|
||||
placeholder="Example: document.getElementById('example');"
|
||||
openFileLabel={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.openJSFile',
|
||||
)}
|
||||
onSave={text => dispatch(setReaderSettings('customJS', text))}
|
||||
/>
|
||||
<ConfirmationDialog
|
||||
title={getString(
|
||||
'moreScreen.settingsScreen.readerSettings.clearCustomJS',
|
||||
)}
|
||||
visible={clearJSModal.value}
|
||||
onSubmit={() => {
|
||||
dispatch(setReaderSettings('customJS', ''));
|
||||
}}
|
||||
onDismiss={clearJSModal.setFalse}
|
||||
theme={theme}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
|
||||
@@ -24,6 +24,7 @@ const GeneralSettings: React.FC = () => {
|
||||
useVolumeButtons = false,
|
||||
verticalSeekbar = true,
|
||||
swipeGestures = false,
|
||||
readerPages = false,
|
||||
autoScroll = false,
|
||||
autoScrollInterval = 10,
|
||||
autoScrollOffset = null,
|
||||
@@ -76,6 +77,12 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchItem
|
||||
label={getString('readerScreen.bottomSheet.readerPages')}
|
||||
value={readerPages}
|
||||
onPress={() => dispatch(setAppSettings('readerPages', !readerPages))}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchItem
|
||||
label={getString('readerScreen.bottomSheet.autoscroll')}
|
||||
value={autoScroll}
|
||||
|
||||
@@ -6,11 +6,9 @@ import FastImage from 'react-native-fast-image';
|
||||
import { List } from 'react-native-paper';
|
||||
import { coverPlaceholderColor } from '../../../theme/colors';
|
||||
import {
|
||||
openChapter,
|
||||
openChapterFunctionTypes,
|
||||
openNovel,
|
||||
openNovelProps,
|
||||
} from '@utils/handleNavigateParams';
|
||||
getChapterScreenRouteParams,
|
||||
getNovelScreenRouteParams,
|
||||
} from '@utils/NavigationUtils';
|
||||
import {
|
||||
deleteChapterAction,
|
||||
downloadChapterAction,
|
||||
@@ -23,16 +21,27 @@ import { useAppDispatch } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
const NovelCover = ({
|
||||
uri,
|
||||
navigateToNovel,
|
||||
}: {
|
||||
uri: string;
|
||||
navigateToNovel: () => void;
|
||||
sourceId: number;
|
||||
}) => {
|
||||
return (
|
||||
<Pressable onPress={navigateToNovel}>
|
||||
<FastImage source={{ uri }} style={styles.cover} />
|
||||
<Pressable
|
||||
onPress={navigateToNovel}
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri, headers: { 'User-Agent': defaultUserAgentString } }}
|
||||
style={styles.cover}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -90,7 +99,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
|
||||
(chapter: ChapterItemExtended) =>
|
||||
navigate(
|
||||
'Chapter' as never,
|
||||
openChapter(chapter, chapter) as openChapterFunctionTypes as never,
|
||||
getChapterScreenRouteParams(chapter, chapter) as never,
|
||||
),
|
||||
[],
|
||||
);
|
||||
@@ -98,26 +107,30 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
|
||||
const navigateToNovel = () =>
|
||||
navigate(
|
||||
'Novel' as never,
|
||||
openNovel(chapterList[0]) as openNovelProps as never,
|
||||
getNovelScreenRouteParams(chapterList[0]) as never,
|
||||
);
|
||||
|
||||
const { downloadQueue } = useSelector(
|
||||
(state: RootState) => state.downloadsReducer,
|
||||
);
|
||||
|
||||
const renderNovelCover = useCallback(
|
||||
() => (
|
||||
<NovelCover
|
||||
navigateToNovel={navigateToNovel}
|
||||
uri={chapterList[0].novelCover}
|
||||
sourceId={chapterList[0].sourceId}
|
||||
/>
|
||||
),
|
||||
[JSON.stringify(chapterList[0])],
|
||||
);
|
||||
|
||||
if (chapterList.length > 1) {
|
||||
return (
|
||||
<List.Accordion
|
||||
title={chapterList[0].novelName}
|
||||
titleStyle={{ fontSize: 14, color: theme.onSurface }}
|
||||
left={() => (
|
||||
<View style={styles.listNovelCover}>
|
||||
<NovelCover
|
||||
navigateToNovel={navigateToNovel}
|
||||
uri={chapterList[0].novelCover}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
left={renderNovelCover}
|
||||
descriptionStyle={{ fontSize: 12 }}
|
||||
theme={{ colors: theme }}
|
||||
style={[styles.container, styles.padding]}
|
||||
@@ -145,6 +158,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
|
||||
<NovelCover
|
||||
navigateToNovel={navigateToNovel}
|
||||
uri={chapterList[0].novelCover}
|
||||
sourceId={chapterList[0].sourceId}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
@@ -172,6 +186,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
|
||||
<NovelCover
|
||||
navigateToNovel={navigateToNovel}
|
||||
uri={chapterList[0].novelCover}
|
||||
sourceId={chapterList[0].sourceId}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
@@ -186,7 +201,8 @@ export default UpdateNovelCard;
|
||||
const styles = StyleSheet.create({
|
||||
padding: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 3,
|
||||
paddingVertical: 0,
|
||||
alignItems: 'center',
|
||||
height: 64,
|
||||
},
|
||||
container: {
|
||||
@@ -198,9 +214,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
listNovelCover: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
novelCover: {
|
||||
marginRight: 16,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ import { fetchNovel } from '../Source/source';
|
||||
import { downloadChapter } from '../../database/queries/ChapterQueries';
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { getNovel } from '@database/queries/NovelQueries';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const updateNovelMetadata = async (novelId, novel) => {
|
||||
@@ -54,8 +56,16 @@ const updateNovel = async (sourceId, novelUrl, novelId, options) => {
|
||||
const { downloadNewChapters, refreshNovelMetadata } = options;
|
||||
|
||||
let novel = await fetchNovel(sourceId, novelUrl);
|
||||
let forceMetdataUpdate = false;
|
||||
|
||||
if (refreshNovelMetadata) {
|
||||
if (novelId === undefined) {
|
||||
// novelId will be undefined if novel is not saved in local library
|
||||
let dbNovel = await getNovel(sourceId, novelUrl);
|
||||
novelId = dbNovel.novelId;
|
||||
forceMetdataUpdate = true;
|
||||
}
|
||||
|
||||
if (refreshNovelMetadata || forceMetdataUpdate) {
|
||||
updateNovelMetadata(novelId, novel);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,56 +42,49 @@ const updateLibrary = async options => {
|
||||
i++
|
||||
) {
|
||||
try {
|
||||
if (BackgroundService.isRunning()) {
|
||||
/**
|
||||
* Update chapters
|
||||
*/
|
||||
await updateNovel(
|
||||
libraryNovels[i].sourceId,
|
||||
libraryNovels[i].novelUrl,
|
||||
libraryNovels[i].novelId,
|
||||
options,
|
||||
);
|
||||
/**
|
||||
* Update chapters
|
||||
*/
|
||||
await updateNovel(
|
||||
libraryNovels[i].sourceId,
|
||||
libraryNovels[i].novelUrl,
|
||||
libraryNovels[i].novelId,
|
||||
options,
|
||||
);
|
||||
|
||||
/**
|
||||
* Update notification
|
||||
*/
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: libraryNovels[i].novelName,
|
||||
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
|
||||
progressBar: { max: libraryNovels.length, value: i + 1 },
|
||||
});
|
||||
/**
|
||||
* Update notification
|
||||
*/
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: libraryNovels[i].novelName,
|
||||
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
|
||||
progressBar: { max: libraryNovels.length, value: i + 1 },
|
||||
});
|
||||
|
||||
/**
|
||||
* When updating library is finished
|
||||
*/
|
||||
if (libraryNovels.length === i + 1) {
|
||||
resolve();
|
||||
const nextNovelIndex = i + 1;
|
||||
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Library Updated',
|
||||
body: libraryNovels.length + ' novels updated',
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
|
||||
const nextNovelIndex = i + 1;
|
||||
|
||||
if (
|
||||
nextNovelIndex in libraryNovels &&
|
||||
libraryNovels[nextNovelIndex].sourceId ===
|
||||
libraryNovels[i].sourceId
|
||||
) {
|
||||
await sleep(taskData.delay);
|
||||
}
|
||||
if (
|
||||
nextNovelIndex in libraryNovels &&
|
||||
libraryNovels[nextNovelIndex].sourceId === libraryNovels[i].sourceId
|
||||
) {
|
||||
await sleep(taskData.delay);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(libraryNovels[i].novelName + ': ' + error.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When updating library is finished
|
||||
*/
|
||||
resolve();
|
||||
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Library Updated',
|
||||
body: libraryNovels.length + ' novels updated',
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
});
|
||||
|
||||
if (libraryNovels.length > 0) {
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
const sourceId = 165;
|
||||
const sourceName = 'Linovelib';
|
||||
|
||||
const baseUrl = 'https://w.linovelib.com';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const url = `${baseUrl}/top/monthvisit/${page}.html`;
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novels = [];
|
||||
|
||||
loadedCheerio('.module-rank-booklist .book-layout').each(function () {
|
||||
let novelUrl = loadedCheerio(this).attr('href');
|
||||
|
||||
if (novelUrl) {
|
||||
const novelName = loadedCheerio(this).find('.book-title').text();
|
||||
const novelCover = loadedCheerio(this)
|
||||
.find('img.book-cover')
|
||||
.attr('data-src');
|
||||
novelUrl = baseUrl + novelUrl;
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url: novelUrl,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('#bookDetailWrapper .book-title').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('#bookDetailWrapper img.book-cover').attr(
|
||||
'src',
|
||||
);
|
||||
|
||||
novel.summary = loadedCheerio('#bookSummary content').text();
|
||||
|
||||
novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a a').text();
|
||||
|
||||
novel.artist = null;
|
||||
|
||||
// TODO: Need some regex and dirty selector to get it
|
||||
// Need to look into how to translate that message
|
||||
novel.status = null;
|
||||
|
||||
let genres = [];
|
||||
loadedCheerio(
|
||||
'#bookDetailWrapper .book-cell .book-meta:last-child .tag-small.red a',
|
||||
).each(function () {
|
||||
genres.push(loadedCheerio(this).text());
|
||||
});
|
||||
|
||||
if (genres.length) {
|
||||
novel.genre = genres.join(', ');
|
||||
}
|
||||
|
||||
// Table of Content is on a different page than the summary page
|
||||
let chapters = [];
|
||||
|
||||
const idPattern = /\/(\d+)\.html/;
|
||||
const novelId = url.match(idPattern)[1];
|
||||
|
||||
const chaptersUrl = baseUrl + loadedCheerio('#btnReadBook').attr('href');
|
||||
const chaptersBody = await fetchHtml({ url: chaptersUrl, sourceId });
|
||||
|
||||
const chaptersLoadedCheerio = cheerio.load(chaptersBody);
|
||||
|
||||
let volumeName;
|
||||
|
||||
chaptersLoadedCheerio('#volumes .chapter-li').each(function () {
|
||||
if (chaptersLoadedCheerio(this).hasClass('chapter-bar')) {
|
||||
volumeName = chaptersLoadedCheerio(this).text();
|
||||
} else {
|
||||
const urlPart = chaptersLoadedCheerio(this)
|
||||
.find('.chapter-li-a')
|
||||
.attr('href');
|
||||
const chapterIdMatch = urlPart.match(idPattern);
|
||||
|
||||
// Sometimes the href attribute does not contain the url, but javascript:cid(0).
|
||||
// Increment the previous chapter ID should result in the right URL
|
||||
if (chapterIdMatch) {
|
||||
chapterId = chapterIdMatch[1];
|
||||
} else {
|
||||
chapterId++;
|
||||
}
|
||||
|
||||
const chapterUrl = `${baseUrl}/novel/${novelId}/${chapterId}.html`;
|
||||
|
||||
if (chapterUrl) {
|
||||
const chapterName =
|
||||
volumeName +
|
||||
' — ' +
|
||||
chaptersLoadedCheerio(this).find('.chapter-index').text().trim();
|
||||
const releaseDate = null;
|
||||
|
||||
chapters.push({
|
||||
chapterName,
|
||||
releaseDate,
|
||||
chapterUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
let chapterName, chapterText, hasNextPage, pageHasNextPage, pageText, urlNext;
|
||||
let pageNumber = 1;
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
/*
|
||||
* TODO: Maybe there are other ways to get the translation table
|
||||
* It is embed and encrypted inside readtool.js
|
||||
* UPDATE: Decrypted, by skillgg
|
||||
*/
|
||||
|
||||
const mapping_dict = {
|
||||
'\u201c': '\u300c',
|
||||
'\u201d': '\u300d',
|
||||
'\u2018': '\u300e',
|
||||
'\u2019': '\u300f',
|
||||
'\ue82c': '\u7684',
|
||||
'\ue836': '\u4e00',
|
||||
'\ue852': '\u662f',
|
||||
'\ue850': '\u4e86',
|
||||
'\ue832': '\u6211',
|
||||
'\ue812': '\u4e0d',
|
||||
'\ue833': '\u4eba',
|
||||
'\ue849': '\u5728',
|
||||
'\ue821': '\u4ed6',
|
||||
'\ue810': '\u6709',
|
||||
'\ue84c': '\u8fd9',
|
||||
'\ue815': '\u4e2a',
|
||||
'\ue842': '\u4e0a',
|
||||
'\ue82e': '\u4eec',
|
||||
'\ue817': '\u6765',
|
||||
'\ue835': '\u5230',
|
||||
'\ue837': '\u65f6',
|
||||
'\ue82d': '\u5927',
|
||||
'\ue859': '\u5730',
|
||||
'\ue85c': '\u4e3a',
|
||||
'\ue82f': '\u5b50',
|
||||
'\ue84d': '\u4e2d',
|
||||
'\ue854': '\u4f60',
|
||||
'\ue81e': '\u8bf4',
|
||||
'\ue853': '\u751f',
|
||||
'\ue80f': '\u56fd',
|
||||
'\ue80e': '\u5e74',
|
||||
'\ue813': '\u7740',
|
||||
'\ue802': '\u5c31',
|
||||
'\ue81a': '\u90a3',
|
||||
'\ue83b': '\u548c',
|
||||
'\ue851': '\u8981',
|
||||
'\ue82a': '\u5979',
|
||||
'\ue838': '\u51fa',
|
||||
'\ue808': '\u4e5f',
|
||||
'\ue83a': '\u5f97',
|
||||
'\ue814': '\u91cc',
|
||||
'\ue857': '\u540e',
|
||||
'\ue855': '\u81ea',
|
||||
'\ue800': '\u4ee5',
|
||||
'\ue81b': '\u4f1a',
|
||||
'\ue85f': '\u5bb6',
|
||||
'\ue816': '\u53ef',
|
||||
'\ue83e': '\u4e0b',
|
||||
'\ue84f': '\u800c',
|
||||
'\ue80b': '\u8fc7',
|
||||
'\ue828': '\u5929',
|
||||
'\ue843': '\u53bb',
|
||||
'\ue806': '\u80fd',
|
||||
'\ue81f': '\u5bf9',
|
||||
'\ue834': '\u5c0f',
|
||||
'\ue81c': '\u591a',
|
||||
'\ue848': '\u7136',
|
||||
'\ue830': '\u4e8e',
|
||||
'\ue84b': '\u5fc3',
|
||||
'\ue84a': '\u5b66',
|
||||
'\ue85d': '\u4e48',
|
||||
'\ue861': '\u4e4b',
|
||||
'\ue809': '\u90fd',
|
||||
'\ue80c': '\u597d',
|
||||
'\ue84e': '\u770b',
|
||||
'\ue858': '\u8d77',
|
||||
'\ue840': '\u53d1',
|
||||
'\ue85b': '\u5f53',
|
||||
'\ue863': '\u6ca1',
|
||||
'\ue839': '\u6210',
|
||||
'\ue827': '\u53ea',
|
||||
'\ue841': '\u5982',
|
||||
'\ue805': '\u4e8b',
|
||||
'\ue845': '\u628a',
|
||||
'\ue820': '\u8fd8',
|
||||
'\ue83c': '\u7528',
|
||||
'\ue847': '\u7b2c',
|
||||
'\ue819': '\u6837',
|
||||
'\ue82b': '\u9053',
|
||||
'\ue80a': '\u60f3',
|
||||
'\ue822': '\u4f5c',
|
||||
'\ue85e': '\u79cd',
|
||||
'\ue801': '\u5f00',
|
||||
'\ue856': '\u7f8e',
|
||||
'\ue811': '\u4e73',
|
||||
'\ue860': '\u9634',
|
||||
'\ue80d': '\u6db2',
|
||||
'\ue83f': '\u830e',
|
||||
'\ue803': '\u6b32',
|
||||
'\ue804': '\u547b',
|
||||
'\ue825': '\u8089',
|
||||
'\ue846': '\u4ea4',
|
||||
'\ue85a': '\u6027',
|
||||
'\ue831': '\u80f8',
|
||||
'\ue81d': '\u79c1',
|
||||
'\ue826': '\u7a74',
|
||||
'\ue818': '\u6deb',
|
||||
'\ue823': '\u81c0',
|
||||
'\ue829': '\u8214',
|
||||
'\ue807': '\u5c04',
|
||||
'\ue862': '\u8131',
|
||||
'\ue83d': '\u88f8',
|
||||
'\ue824': '\u9a9a',
|
||||
'\ue844': '\u5507',
|
||||
};
|
||||
|
||||
const addPage = async pageCheerio => {
|
||||
const formatPage = async () => {
|
||||
// Remove JS
|
||||
let style = pageCheerio('style:first').prop('innerHTML');
|
||||
style = style.replace(/(.*?)\{.*?\}/g, '$1,').split(',');
|
||||
style.push(style.pop().replace(/\}/, '.cgo'));
|
||||
style.map(tag => pageCheerio(tag).remove());
|
||||
|
||||
// Load lazyloaded images
|
||||
pageCheerio('.atitle')
|
||||
.next()
|
||||
.find('img.imagecontent')
|
||||
.each(function () {
|
||||
// Sometimes images are either in data-src or src
|
||||
const imgSrc =
|
||||
pageCheerio(this).attr('data-src') || pageCheerio(this).attr('src');
|
||||
if (imgSrc) {
|
||||
// The original CDN URL is locked behind a CF-like challenge, switch the URL to bypass that
|
||||
// There are no react-native-url-polyfill lib, can't use URL API
|
||||
const regex = /\/\/.+\.com\//;
|
||||
const imgUrl = imgSrc.replace(regex, '//img.linovelib.com/');
|
||||
// Clean up img element
|
||||
pageCheerio(this)
|
||||
.attr('src', imgUrl)
|
||||
.removeAttr('data-src')
|
||||
.removeClass('lazyload')
|
||||
.addClass('delayed-src');
|
||||
}
|
||||
});
|
||||
|
||||
// Recover the original character
|
||||
pageText = pageCheerio('.atitle').next().html();
|
||||
pageText = pageText?.replace(/./g, char => mapping_dict[char] || char);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
await formatPage();
|
||||
chapterName =
|
||||
pageCheerio('.atitle h3').text() + ' — ' + pageCheerio('#atitle').text();
|
||||
if (chapterText === undefined) {
|
||||
chapterText = '<h2>' + chapterName + '</h2>';
|
||||
}
|
||||
chapterText +=
|
||||
pageText ||
|
||||
'Chapter not found, Report at lnreader github/discord if you see this message';
|
||||
};
|
||||
|
||||
const loadPage = async url => {
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
const pageCheerio = cheerio.load(body);
|
||||
await addPage(pageCheerio);
|
||||
urlNext = pageCheerio('#aread script:first')
|
||||
.prop('innerHTML')
|
||||
.replace(/.*next:'(.*?)'.*/g, '$1');
|
||||
pageHasNextPage = urlNext.match(/_/) ? true : false;
|
||||
return { pageCheerio, pageHasNextPage };
|
||||
};
|
||||
|
||||
let url = chapterUrl;
|
||||
do {
|
||||
const page = await loadPage(url);
|
||||
await delay(1000);
|
||||
hasNextPage = page.pageHasNextPage;
|
||||
if (hasNextPage === true) {
|
||||
pageNumber++;
|
||||
url = chapterUrl.replace(/\.html/gi, `_${pageNumber}` + '.html');
|
||||
}
|
||||
} while (hasNextPage === true);
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const searchUrl = `${baseUrl}/search/`;
|
||||
const Term = encodeURI(searchTerm);
|
||||
let nextPage, noNextPage, deadEnd;
|
||||
let pageNumber = 1;
|
||||
const novels = [];
|
||||
|
||||
const addPage = async (pageCheerio, redirect) => {
|
||||
const loadSearchResults = function () {
|
||||
pageCheerio('.book-ol .book-layout').each(function () {
|
||||
let novelUrl = pageCheerio(this).attr('href');
|
||||
|
||||
if (novelUrl) {
|
||||
const novelName = pageCheerio(this).find('.book-title').text();
|
||||
const novelCover = pageCheerio(this)
|
||||
.find('img.book-cover')
|
||||
.attr('data-src');
|
||||
novelUrl = baseUrl + novelUrl;
|
||||
|
||||
const novel = { sourceId, novelUrl, novelName, novelCover };
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const novelResults = pageCheerio('.book-ol a.book-layout');
|
||||
if (novelResults.length === 0) {
|
||||
showToast('Bypass check by searching in Webview');
|
||||
} else {
|
||||
loadSearchResults();
|
||||
}
|
||||
|
||||
if (redirect.length) {
|
||||
novels.length = 0;
|
||||
const novelName = pageCheerio('#bookDetailWrapper .book-title').text();
|
||||
|
||||
const novelCover = pageCheerio('#bookDetailWrapper img.book-cover').attr(
|
||||
'src',
|
||||
);
|
||||
const novelUrl =
|
||||
baseUrl +
|
||||
pageCheerio('#btnReadBook').attr('href').slice(0, -8) +
|
||||
'.html';
|
||||
const novel = { sourceId, novelUrl, novelName, novelCover };
|
||||
novels.push(novel);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPage = async url => {
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
const pageCheerio = cheerio.load(body);
|
||||
const redirect = pageCheerio('div.book-layout');
|
||||
await addPage(pageCheerio, redirect);
|
||||
nextPage = pageCheerio('.next').attr('href');
|
||||
if (!nextPage) {
|
||||
noNextPage === true;
|
||||
} else {
|
||||
noNextPage = nextPage === '#' ? true : false;
|
||||
}
|
||||
return { pageCheerio, noNextPage };
|
||||
};
|
||||
|
||||
let url = `${searchUrl}${Term}_${pageNumber}.html`;
|
||||
do {
|
||||
const page = await loadPage(url);
|
||||
deadEnd = page.noNextPage;
|
||||
if (deadEnd === false) {
|
||||
pageNumber++;
|
||||
url = `${searchUrl}${Term}_${pageNumber}.html`;
|
||||
}
|
||||
} while (deadEnd === false);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const headers = {
|
||||
Referer: 'https://w.linovelib.com',
|
||||
};
|
||||
|
||||
const LinovelibScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
headers,
|
||||
};
|
||||
|
||||
export default LinovelibScraper;
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
const sourceId = 97;
|
||||
const sourceName = 'NovelFull.me';
|
||||
const sourceName = 'Novel Buddy';
|
||||
|
||||
const baseUrl = 'https://novelfull.me/';
|
||||
const baseUrl = 'https://novelbuddy.io/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const url = `${baseUrl}popular?page=${page}`;
|
||||
@@ -56,11 +56,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
novel.novelCover =
|
||||
'https:' + loadedCheerio('.img-cover img').attr('data-src');
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content',
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
novel.summary = loadedCheerio('div.summary > p').text().trim();
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span',
|
||||
@@ -84,9 +80,9 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
let chapters = [];
|
||||
|
||||
const chaptersUrl =
|
||||
novelUrl.replace(baseUrl, 'https://novelfull.me/api/novels/') +
|
||||
'/chapters?source=detail';
|
||||
const novelId = /bookId = (\d+);/.exec(loadedCheerio('script').text())[1];
|
||||
|
||||
const chaptersUrl = `https://novelbuddy.io/api/manga/${novelId}/chapters?source=detail`;
|
||||
|
||||
const chaptersRequest = await fetch(chaptersUrl);
|
||||
const chaptersHtml = await chaptersRequest.text();
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
import * as cheerio from 'cheerio';
|
||||
import {
|
||||
SourceChapter,
|
||||
SourceChapterItem,
|
||||
SourceNovel,
|
||||
SourceNovelItem,
|
||||
} from '../types';
|
||||
import { SourceChapterItem, SourceNovel, SourceNovelItem } from '../types';
|
||||
|
||||
const sourceId = 163;
|
||||
const sourceName = 'novelsOnline';
|
||||
|
||||
// const baseUrl = 'https://novelsonline.net';
|
||||
const baseUrl = 'https://novelsonline.net';
|
||||
|
||||
const searchNovels = async (searchTerm: string) => {
|
||||
const result = await fetchHtml({
|
||||
@@ -101,32 +96,37 @@ const parseNovelAndChapters = async (novelUrl: string) => {
|
||||
return novel;
|
||||
};
|
||||
|
||||
const popularNovels = async () => {
|
||||
return { novels: [] as SourceNovelItem[] }; /** TO DO */
|
||||
};
|
||||
const popularNovels = async (page: number) => {
|
||||
const url = `${baseUrl}/top-novel/${page}`;
|
||||
const result = await fetchHtml({ sourceId, url });
|
||||
let $ = cheerio.load(result);
|
||||
|
||||
const parseChapter = async (novelUrl: string, chapterUrl: string) => {
|
||||
const result = await fetchHtml({ url: chapterUrl });
|
||||
let loadedCheerio = cheerio.load(result);
|
||||
const headers = $('div.top-novel-block');
|
||||
return headers
|
||||
.map((i, h) => {
|
||||
const novelName = $(h).find('h2').text();
|
||||
const novelUrl = $(h).find('a').attr('href');
|
||||
const novelCover = $(h).find('img').attr('src');
|
||||
|
||||
const chapterName = loadedCheerio('h1').text();
|
||||
const chapterText = loadedCheerio('#contentall').html() || '';
|
||||
if (!novelUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chapter: SourceChapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
return {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
} as SourceNovelItem;
|
||||
})
|
||||
.get()
|
||||
.filter(sr => sr !== null);
|
||||
};
|
||||
|
||||
const NovelsOnlineScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
// parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ const parseChapter = async (novelUrl: string, chapterUrl: string) => {
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
loadedCheerio('#chapter-content div').remove();
|
||||
|
||||
const chapterName = loadedCheerio('.chapter-title').attr('title');
|
||||
const chapterText = loadedCheerio('#chapter-content').html() || '';
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
const baseUrl = 'https://freewebnovel.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
let url = baseUrl + 'completed-novel/' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.li-row').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.tit').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('h3 > a')
|
||||
.attr('href')
|
||||
.replace('.html', '')
|
||||
.slice(1);
|
||||
|
||||
const novel = {
|
||||
sourceId: 13,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = `${baseUrl}${novelUrl.replace('/', '')}.html`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {};
|
||||
|
||||
novel.sourceId = 13;
|
||||
|
||||
novel.sourceName = 'FreeWebNovel';
|
||||
|
||||
novel.url = url;
|
||||
|
||||
novel.novelUrl = novelUrl;
|
||||
|
||||
novel.novelName = loadedCheerio('h1.tit').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('.pic > img').attr('src');
|
||||
|
||||
novel.genre = loadedCheerio('[title=Genre]')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '');
|
||||
|
||||
novel.author = loadedCheerio('[title=Author]')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '');
|
||||
|
||||
novel.artist = null;
|
||||
|
||||
novel.status = loadedCheerio('[title=Status]')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '');
|
||||
|
||||
let novelSummary = loadedCheerio('.inner').text().trim();
|
||||
novel.summary = novelSummary;
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
let latestChapter;
|
||||
|
||||
loadedCheerio('h3.tit').each(function (res) {
|
||||
if (loadedCheerio(this).find('a').text() === novel.novelName) {
|
||||
latestChapter = loadedCheerio(this)
|
||||
.next()
|
||||
.find('span.s3')
|
||||
.text()
|
||||
.match(/\d+/);
|
||||
}
|
||||
});
|
||||
|
||||
latestChapter = latestChapter[0];
|
||||
|
||||
for (let i = 1; i <= parseInt(latestChapter, 10); i++) {
|
||||
const chapterName = 'Chapter ' + i;
|
||||
|
||||
const releaseDate = null;
|
||||
|
||||
const chapterUrl = 'chapter-' + i;
|
||||
|
||||
const chapter = { chapterName, releaseDate, chapterUrl };
|
||||
|
||||
novelChapters.push(chapter);
|
||||
}
|
||||
|
||||
novel.chapters = novelChapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
let novelId = novelUrl.replace('/', '');
|
||||
|
||||
const url = `${baseUrl}${novelId}/${chapterUrl}.html`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let chapterName = loadedCheerio('h1.tit').text();
|
||||
|
||||
let chapterText = loadedCheerio('div.txt').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId: 13,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = baseUrl + 'search/';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('searchkey', searchTerm);
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.li-row > .li > .con').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.tit').text();
|
||||
const novelCover = loadedCheerio(this)
|
||||
.find('.pic > a > img')
|
||||
.attr('data-cfsrc');
|
||||
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('h3 > a')
|
||||
.attr('href')
|
||||
.replace('.html', '')
|
||||
.slice(1);
|
||||
|
||||
novelUrl += '/';
|
||||
|
||||
const novel = {
|
||||
sourceId: 13,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const FreeWebNovelScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default FreeWebNovelScraper;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Status } from '../helpers/constants';
|
||||
import * as cheerio from 'cheerio';
|
||||
import {
|
||||
SourceChapter,
|
||||
SourceChapterItem,
|
||||
SourceNovel,
|
||||
SourceNovelItem,
|
||||
} from '../types';
|
||||
|
||||
const sourceId = 13;
|
||||
const sourceName = 'FreeWebNovel';
|
||||
const baseUrl = 'https://freewebnovel.com';
|
||||
|
||||
const popularNovels = async (page: number, { showLatestNovels }) => {
|
||||
const sort = showLatestNovels
|
||||
? '/latest-release-novels/'
|
||||
: '/completed-novels/';
|
||||
|
||||
const result = await fetch(baseUrl + sort + page).then(res => res.text());
|
||||
const loadedCheerio = cheerio.load(result);
|
||||
|
||||
const novels: SourceNovelItem[] = loadedCheerio('.li-row')
|
||||
.map((index, element) => ({
|
||||
sourceId,
|
||||
novelName: loadedCheerio(element).find('.tit').text(),
|
||||
novelCover: loadedCheerio(element).find('img').attr('src'),
|
||||
novelUrl: baseUrl + loadedCheerio(element).find('h3 > a').attr('href'),
|
||||
}))
|
||||
.get();
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (novelUrl: string) => {
|
||||
const result = await fetch(novelUrl).then(res => res.text());
|
||||
const loadedCheerio = cheerio.load(result);
|
||||
|
||||
const novel: SourceNovel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
novelUrl,
|
||||
url: novelUrl,
|
||||
novelName: loadedCheerio('h1.tit').text(),
|
||||
novelCover: loadedCheerio('.pic > img').attr('src'),
|
||||
summary: loadedCheerio('.inner').text().trim(),
|
||||
};
|
||||
|
||||
novel.genre = loadedCheerio('[title=Genre]')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '');
|
||||
|
||||
novel.author = loadedCheerio('[title=Author]')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '');
|
||||
|
||||
novel.status =
|
||||
loadedCheerio('[title=Status]')
|
||||
.next()
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '') === 'OnGoing'
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED;
|
||||
|
||||
const chapters: SourceChapterItem[] = loadedCheerio('#idData > li > a')
|
||||
.map((index, element) => ({
|
||||
chapterName: loadedCheerio(element).attr('title') || 'Chapter ' + index,
|
||||
releaseDate: null,
|
||||
chapterUrl: baseUrl + loadedCheerio(element).attr('href'),
|
||||
}))
|
||||
.get();
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl: string, chapterUrl: string) => {
|
||||
const result = await fetch(chapterUrl).then(res => res.text());
|
||||
const loadedCheerio = cheerio.load(result);
|
||||
|
||||
const chapter: SourceChapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName: loadedCheerio('h1.tit').text(),
|
||||
chapterText: loadedCheerio('div.txt').html(),
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm: string) => {
|
||||
const result = await fetch(baseUrl + '/search/', {
|
||||
method: 'POST',
|
||||
body: 'searchkey=' + encodeURIComponent(searchTerm),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
}).then(res => res.text());
|
||||
|
||||
const loadedCheerio = cheerio.load(result);
|
||||
const novels: SourceNovelItem[] = loadedCheerio('.li-row > .li > .con')
|
||||
.map((index, element) => ({
|
||||
sourceId,
|
||||
novelName: loadedCheerio(element).find('.tit').text(),
|
||||
novelCover: loadedCheerio(element).find('.pic > a > img').attr('src'),
|
||||
novelUrl: baseUrl + loadedCheerio(element).find('h3 > a').attr('href'),
|
||||
}))
|
||||
.get();
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const FreeWebNovelScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default FreeWebNovelScraper;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { FilterInputs } from '../types/filterTypes';
|
||||
|
||||
const sourceId = 170;
|
||||
const sourceName = 'I cant read japanese tl';
|
||||
|
||||
const baseUrl = 'https://icantreadjapanese.wordpress.com/';
|
||||
|
||||
const popularNovels = async (page, { filters }) => {
|
||||
let link = `${baseUrl}`;
|
||||
|
||||
if (page === 1) {
|
||||
link += (filters?.storyStatus ? filters.storyStatus : 'projects') + '/';
|
||||
}
|
||||
|
||||
const body = await fetchHtml({ url: link, sourceId });
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('figure.wp-block-image').each(function () {
|
||||
const novelName = loadedCheerio(this).find('a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
const novel = { sourceId, novelName, novelCover, novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const body = await fetchHtml({ url: novelUrl, sourceId: sourceId });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url: novelUrl,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('h1.entry-title').text().trim();
|
||||
|
||||
novel.novelCover = loadedCheerio('figure.wp-block-image > img').attr('src');
|
||||
|
||||
novel.author = loadedCheerio('.has-text-align-left')
|
||||
.text()
|
||||
.replace(/.*Author: (.*) \|.*/g, '$1');
|
||||
|
||||
novel.summary = loadedCheerio('.entry-content > p:first')
|
||||
.nextUntil('hr:first')
|
||||
.text();
|
||||
|
||||
let chapters = [];
|
||||
|
||||
loadedCheerio('.wp-block-column li a').each(function () {
|
||||
let chapterName = loadedCheerio(this).text();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).attr('href');
|
||||
|
||||
const volumeName = loadedCheerio(this)
|
||||
.parent()
|
||||
.prop('innerHTML')
|
||||
.replace(/<a.*>/g, '')
|
||||
.replace(/<br>/g, ' ')
|
||||
.trim();
|
||||
if (volumeName.length) {
|
||||
chapterName = volumeName + ' - ' + chapterName;
|
||||
}
|
||||
|
||||
chapters.push({ chapterName, releaseDate, chapterUrl });
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const body = await fetchHtml({
|
||||
url: chapterUrl,
|
||||
sourceId: sourceId,
|
||||
});
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h1.entry-title').text();
|
||||
|
||||
const chapterText = loadedCheerio('.entry-content').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async () => {
|
||||
showToast('Search is not implemented for this source');
|
||||
return;
|
||||
};
|
||||
|
||||
const filters = [
|
||||
{
|
||||
key: 'storyStatus',
|
||||
label: 'Translation Status',
|
||||
values: [
|
||||
{ label: 'Ongoing Projects', value: 'projects' },
|
||||
{
|
||||
label: 'Maybe Will Read Again, Caught Up',
|
||||
value: 'maybe-will-read-again-caught-up',
|
||||
},
|
||||
{ label: 'Dropped', value: 'dropped' },
|
||||
{ label: 'Finished', value: 'finished' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
];
|
||||
|
||||
const ICantReadJPTLScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
filters,
|
||||
};
|
||||
|
||||
export default ICantReadJPTLScraper;
|
||||
@@ -111,7 +111,6 @@ const parseNovelAndChapters = async (novelUrl: string) => {
|
||||
|
||||
return novelChapters;
|
||||
};
|
||||
console.log(novelId);
|
||||
|
||||
if (novelId) {
|
||||
novel.chapters = await getChapters(novelId);
|
||||
|
||||
@@ -87,15 +87,17 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
const chaptersHtml = await fetchHtml({
|
||||
url: chaptersUrl,
|
||||
init: { headers },
|
||||
sourceId,
|
||||
});
|
||||
|
||||
loadedCheerio = cheerio.load(chaptersHtml);
|
||||
|
||||
loadedCheerio('.chapter-list li').each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find('.chapter-title')
|
||||
.text()
|
||||
.trim();
|
||||
const chapterName =
|
||||
'Chapter ' +
|
||||
loadedCheerio(this).find('.chapter-no').text().trim() +
|
||||
' - ';
|
||||
loadedCheerio(this).find('.chapter-title').text().trim();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find('.chapter-update')
|
||||
|
||||
+37
-35
@@ -1,6 +1,5 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { htmlToText } from '../helpers/htmlToText';
|
||||
|
||||
const baseUrl = 'https://lnmtl.com/';
|
||||
|
||||
@@ -56,24 +55,26 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
novel.novelName = loadedCheerio('.novel-name').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('div.novel').find('img').attr('src');
|
||||
novel.novelCover = loadedCheerio('.novel').find('img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('div.description').text().trim();
|
||||
novel.summary = loadedCheerio('.description').text().trim();
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
'main > div:nth-child(3) > div > div.col-lg-3.col-md-4 > div:nth-child(2) > div.panel-body > dl:nth-child(1) > dd > a > span',
|
||||
).text();
|
||||
loadedCheerio('.panel-body > dl').each(function () {
|
||||
let detailName = loadedCheerio(this).find('dt').text().trim();
|
||||
let detail = loadedCheerio(this).find('dd').text().trim();
|
||||
|
||||
novel.status = loadedCheerio(
|
||||
'main > div:nth-child(3) > div > div.col-lg-3.col-md-4 > div:nth-child(2) > div.panel-body > dl:last-child > dd',
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
console.log(novel.status);
|
||||
switch (detailName) {
|
||||
case 'Authors':
|
||||
novel.author = detail;
|
||||
break;
|
||||
case 'Current status':
|
||||
novel.status = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
novel.genre = loadedCheerio(
|
||||
'main > div.container > div > div.col-lg-3.col-md-4 > div:nth-child(4) > div.panel-body > ul',
|
||||
)
|
||||
novel.genre = loadedCheerio('.panel-heading:contains(" Genres ")')
|
||||
.next()
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/\s\s/g, ',');
|
||||
@@ -129,17 +130,15 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
let chapterName = loadedCheerio('h3 > span.chapter-title').text().trim();
|
||||
|
||||
loadedCheerio('.original').remove();
|
||||
loadedCheerio('.original, script').remove();
|
||||
loadedCheerio('sentence.translated').wrap('<p></p>');
|
||||
|
||||
let chapterText = loadedCheerio('.chapter-body').html();
|
||||
let chapterText = loadedCheerio('.chapter-body').html().replace(/„/g, '“');
|
||||
|
||||
if (!chapterText) {
|
||||
chapterText = loadedCheerio('.alert.alert-warning').text();
|
||||
}
|
||||
|
||||
chapterText =
|
||||
chapterName + '\n\n' + htmlToText(chapterText, { removeLineBreaks: false });
|
||||
|
||||
const chapter = {
|
||||
sourceId: 37,
|
||||
novelUrl,
|
||||
@@ -152,32 +151,35 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = 'https://lnmtl.com/term';
|
||||
|
||||
const result = await fetch(url);
|
||||
const result = await fetch(baseUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = loadedCheerio('footer')
|
||||
const list = loadedCheerio('footer')
|
||||
.next()
|
||||
.next()
|
||||
.html()
|
||||
.match(/local: \[(.*?)\]/)[0]
|
||||
.replace('local: ', '');
|
||||
.match(/prefetch: '\/(.*json)/)[1];
|
||||
|
||||
novels = JSON.parse(novels);
|
||||
const search = await fetch(`${baseUrl}${list}`);
|
||||
const data = await search.json();
|
||||
|
||||
novels = novels.filter(novel =>
|
||||
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
let nov = data.filter(res =>
|
||||
res.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
novels = novels.map(novel => ({
|
||||
sourceId: 37,
|
||||
novelName: novel.name,
|
||||
novelUrl: novel.slug,
|
||||
novelCover: novel.image,
|
||||
}));
|
||||
const novels = [];
|
||||
|
||||
nov.map(res => {
|
||||
const novelName = res.name;
|
||||
const novelUrl = res.slug;
|
||||
const novelCover = res.image;
|
||||
|
||||
const novel = { sourceId: 37, novelName, novelUrl, novelCover };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import { fetchHtml } from '@utils/fetch/fetch';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
const sourceId = 3;
|
||||
const baseUrl = 'https://fastnovel.org';
|
||||
const searchUrl = 'https://fastnovel.org/search/';
|
||||
const baseUrl = 'https://novelbin.org';
|
||||
const searchUrl = 'https://novelbin.org/search/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const url = `${baseUrl}/sort/p/?page=${page}`;
|
||||
@@ -14,7 +14,7 @@ const popularNovels = async page => {
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.list-novel .row').each(function () {
|
||||
loadedCheerio('.col-novel-main .list-novel .row').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h3.novel-title > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img.cover').attr('src');
|
||||
const novelUrl = loadedCheerio(this)
|
||||
@@ -42,7 +42,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName: 'FastNovel',
|
||||
sourceName: 'NovelBin',
|
||||
url,
|
||||
novelUrl,
|
||||
};
|
||||
@@ -53,44 +53,41 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
novel.summary = loadedCheerio('div.desc-text').text().trim();
|
||||
|
||||
novel.author = loadedCheerio('ul.info > li > h3')
|
||||
.filter(function () {
|
||||
return loadedCheerio(this).text().trim() === 'Author:';
|
||||
})
|
||||
.siblings()
|
||||
.text();
|
||||
novel.genre = loadedCheerio('ul.info > li')
|
||||
.filter(function () {
|
||||
return loadedCheerio(this).find('h3').text().trim() === 'Genre:';
|
||||
})
|
||||
.text()
|
||||
.trim()
|
||||
.replace('Genre:', '')
|
||||
.trim();
|
||||
|
||||
novel.status = loadedCheerio('ul.info > li > h3')
|
||||
.filter(function () {
|
||||
return loadedCheerio(this).text().trim() === 'Status:';
|
||||
})
|
||||
.next()
|
||||
.text();
|
||||
loadedCheerio('ul.info > li > h3').each(function () {
|
||||
let detailName = loadedCheerio(this).text();
|
||||
let detail = loadedCheerio(this)
|
||||
.siblings()
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
switch (detailName) {
|
||||
case 'Author:':
|
||||
novel.author = detail;
|
||||
break;
|
||||
case 'Status:':
|
||||
novel.status = detail;
|
||||
break;
|
||||
case 'Genre:':
|
||||
novel.genre = detail;
|
||||
break;
|
||||
}
|
||||
});
|
||||
const novelId = loadedCheerio('#rating').attr('data-novel-id');
|
||||
|
||||
const getChapters = async id => {
|
||||
const chapterListUrl = baseUrl + '/ajax/chapter-archive?novelId=' + id;
|
||||
|
||||
const data = await fetch(chapterListUrl);
|
||||
const chapters = await data.text();
|
||||
const chapters = await fetchHtml({ url: chapterListUrl, sourceId });
|
||||
|
||||
loadedCheerio = cheerio.load(chapters);
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
loadedCheerio('select > option').each(function () {
|
||||
let chapterName = loadedCheerio(this).text();
|
||||
loadedCheerio('.list-chapter > li').each(function () {
|
||||
let chapterName = loadedCheerio(this).find('span').text();
|
||||
let releaseDate = null;
|
||||
let chapterUrl = loadedCheerio(this).attr('value');
|
||||
let chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
if (chapterUrl) {
|
||||
novelChapters.push({
|
||||
@@ -120,6 +117,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
const chapterName = loadedCheerio('.chr-text').text();
|
||||
|
||||
loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove();
|
||||
let chapterText = loadedCheerio('#chr-content').html();
|
||||
const chapter = {
|
||||
sourceId,
|
||||
@@ -134,21 +132,19 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = searchUrl + '?keyword=' + searchTerm;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('div.col-novel-main > div.list-novel > .row').each(function () {
|
||||
loadedCheerio('.col-novel-main .list-novel .row').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h3.novel-title > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find('h3.novel-title > a')
|
||||
.attr('href');
|
||||
|
||||
const novelName = loadedCheerio(this).find('h3.novel-title > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
|
||||
if (novelUrl) {
|
||||
novels.push({
|
||||
sourceId,
|
||||
@@ -162,11 +158,11 @@ const searchNovels = async searchTerm => {
|
||||
return novels;
|
||||
};
|
||||
|
||||
const fastNovelScraper = {
|
||||
const NovelBinScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default fastNovelScraper;
|
||||
export default NovelBinScraper;
|
||||
+41
-32
@@ -1,4 +1,6 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { fetchApi, fetchHtml } from '@utils/fetch/fetch';
|
||||
|
||||
const baseUrl = 'https://www.novelpub.com/';
|
||||
|
||||
const sourceName = 'NovelPub';
|
||||
@@ -7,15 +9,12 @@ const sourceId = 94;
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
|
||||
});
|
||||
|
||||
const popularNovels = async page => {
|
||||
let url = baseUrl + 'browse/all/popular/all/' + page;
|
||||
|
||||
const result = await fetch(url, { method: 'GET', headers });
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -41,45 +40,39 @@ const popularNovels = async page => {
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url, { method: 'GET', headers });
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = { url, novelUrl, sourceId, sourceName, genre: '' };
|
||||
let novel = {
|
||||
sourceId,
|
||||
url: novelUrl,
|
||||
novelUrl,
|
||||
sourceName,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('h1.novel-title').text().trim();
|
||||
|
||||
novel.novelCover = loadedCheerio('figure.cover > img').attr('data-src');
|
||||
|
||||
loadedCheerio('div.categories > ul > li').each(function () {
|
||||
novel.genre +=
|
||||
loadedCheerio(this)
|
||||
.text()
|
||||
.replace(/[\t\n]/g, '') + ',';
|
||||
});
|
||||
novel.genre = loadedCheerio('.categories li')
|
||||
.find('a')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
loadedCheerio('div.header-stats > span').each(function () {
|
||||
if (loadedCheerio(this).find('small').text() === 'Status') {
|
||||
novel.status = loadedCheerio(this).find('strong').text();
|
||||
}
|
||||
});
|
||||
|
||||
novel.genre = novel.genre.slice(0, -1);
|
||||
novel.status = loadedCheerio('small:contains("Status")').prev().text().trim();
|
||||
|
||||
novel.author = loadedCheerio('.author > a > span').text();
|
||||
|
||||
loadedCheerio('.expand').remove();
|
||||
novel.summary = loadedCheerio('.summary > .content').text().trim();
|
||||
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
let lastPage = 1;
|
||||
|
||||
lastPage = loadedCheerio(
|
||||
'#novel > header > div.header-body.container > div.novel-info > div.header-stats > span:nth-child(1) > strong',
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
lastPage = loadedCheerio('small:contains("Chapters")').prev().text().trim();
|
||||
|
||||
lastPage = Math.ceil(lastPage / 100);
|
||||
|
||||
@@ -89,8 +82,11 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
for (let i = 1; i <= lastPage; i++) {
|
||||
const chaptersUrl = `${novelUrl}/chapters/page-${i}`;
|
||||
|
||||
const chaptersRequest = await fetch(chaptersUrl, { headers });
|
||||
const chaptersHtml = await chaptersRequest.text();
|
||||
const chaptersHtml = await fetchHtml({
|
||||
url: chaptersUrl,
|
||||
init: { headers },
|
||||
sourceId,
|
||||
});
|
||||
|
||||
loadedCheerio = cheerio.load(chaptersHtml);
|
||||
|
||||
@@ -125,9 +121,8 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url, { method: 'GET', headers });
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
const body = await result.text();
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h2').text();
|
||||
@@ -139,10 +134,24 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}lnwsearchlive?inputContent=${searchTerm}`;
|
||||
const url = `${baseUrl}lnsearchlive`;
|
||||
const link = `${baseUrl}search`;
|
||||
const response = await fetchApi({ url: link, sourceId }).then(r => r.text());
|
||||
const token = cheerio.load(response);
|
||||
let verifytoken = token('#novelSearchForm > input').attr('value');
|
||||
|
||||
const result = await fetch(url, { method: 'GET', headers });
|
||||
const body = await result.text();
|
||||
let formData = new FormData();
|
||||
formData.append('inputContent', searchTerm);
|
||||
|
||||
const body = await fetchHtml({
|
||||
url,
|
||||
init: {
|
||||
method: 'POST',
|
||||
headers: { 'LNRequestVerifyToken': verifytoken },
|
||||
body: formData,
|
||||
},
|
||||
sourceId,
|
||||
});
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { fetchApi, fetchHtml, cloudflareCheck } from '@utils/fetch/fetch';
|
||||
import {
|
||||
fetchApi,
|
||||
fetchHtml,
|
||||
cloudflareCheck,
|
||||
defaultUserAgentString,
|
||||
} from '@utils/fetch/fetch';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { FilterInputs } from '../types/filterTypes';
|
||||
|
||||
const sourceId = 50;
|
||||
|
||||
const sourceName = 'Novel Updates';
|
||||
|
||||
const baseUrl = 'https://www.novelupdates.com/';
|
||||
|
||||
const userAgent =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36';
|
||||
const userAgent = defaultUserAgentString;
|
||||
|
||||
const getPopularNovelsUrl = (page, { showLatestNovels, filters }) => {
|
||||
let url = `${baseUrl}${
|
||||
@@ -105,7 +110,9 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
novel.novelName = loadedCheerio('.seriestitlenu').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('.seriesimg > img').attr('src');
|
||||
novel.novelCover =
|
||||
loadedCheerio('.seriesimg > img').attr('src') ||
|
||||
loadedCheerio('.serieseditimg > img').attr('src');
|
||||
|
||||
novel.author = loadedCheerio('#showauthors').text().trim();
|
||||
novel.genre = loadedCheerio('#seriesgenre')
|
||||
@@ -204,6 +211,12 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
let isWattpad = result.url.toLowerCase().includes('wattpad');
|
||||
|
||||
let isAnomalously = result.url.toLowerCase().includes('anotivereads');
|
||||
|
||||
let isBlossomTranslation = result.url
|
||||
.toLowerCase()
|
||||
.includes('blossomtranslation');
|
||||
|
||||
let isLightNovelsTls = result.url
|
||||
.toLowerCase()
|
||||
.includes('lightnovelstranslations');
|
||||
@@ -241,32 +254,39 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
if (isWuxiaWorld) {
|
||||
chapterText = loadedCheerio('#chapter-content').html();
|
||||
} else if (isRainOfSnow) {
|
||||
chapterText = loadedCheerio('div.content').html();
|
||||
chapterText = loadedCheerio('.content').html();
|
||||
} else if (isTumblr) {
|
||||
chapterText = loadedCheerio('.post').html();
|
||||
} else if (isBlogspot) {
|
||||
loadedCheerio('.post-share-buttons').remove();
|
||||
chapterText = loadedCheerio('.entry-content').html();
|
||||
chapterText =
|
||||
loadedCheerio('.entry-content').html() ||
|
||||
loadedCheerio('#post-body').html();
|
||||
} else if (isHostedNovel) {
|
||||
chapterText = loadedCheerio('.chapter').html();
|
||||
} else if (isScribbleHub) {
|
||||
chapterText = loadedCheerio('div.chp_raw').html();
|
||||
chapterText = loadedCheerio('.chp_raw').html();
|
||||
} else if (isWattpad) {
|
||||
chapterText = loadedCheerio('.container pre').html();
|
||||
} else if (isTravisTranslation) {
|
||||
chapterText = loadedCheerio('.reader-content').html();
|
||||
} else if (isLightNovelsTls) {
|
||||
chapterText = loadedCheerio('.text_story').html();
|
||||
} else if (isBlossomTranslation) {
|
||||
chapterText = loadedCheerio('.manga-child-content').html();
|
||||
} else if (isAnomalously) {
|
||||
chapterText = loadedCheerio('#comic').html();
|
||||
} else if (isiNovelTranslation) {
|
||||
const link = 'https://api.' + result.url.slice(8);
|
||||
const json = await fetchApi({
|
||||
url: link,
|
||||
sourceId,
|
||||
}).then(r => r.json());
|
||||
chapterText =
|
||||
json.content.replace(/\n/g, '<br>') +
|
||||
'<br><hr><br>TL Notes:<br>' +
|
||||
json.notes.replace(/\n/g, '<br>');
|
||||
chapterText = json.content.replace(/\n/g, '<br>');
|
||||
if (json.notes) {
|
||||
chapterText +=
|
||||
'<br><hr><br>TL Notes:<br>' + json.notes.replace(/\n/g, '<br>');
|
||||
}
|
||||
} else if (isWordPress) {
|
||||
/**
|
||||
* Remove wordpress bloat tags
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fetchHtml } from '@utils/fetch/fetch';
|
||||
const sourceId = 51;
|
||||
const sourceName = 'Ranobes';
|
||||
|
||||
const baseUrl = 'https://ranobes.net';
|
||||
const baseUrl = 'https://ranobes.top';
|
||||
|
||||
const popularNovels = async page => {
|
||||
let url = `${baseUrl}/novels/page/${page}/`;
|
||||
|
||||
@@ -127,7 +127,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let chapterName = loadedCheerio('div.chapter-content').find('strong').text();
|
||||
let chapterName = loadedCheerio('h1.font-white').text();
|
||||
|
||||
let chapterText = loadedCheerio('div.chapter-content').html();
|
||||
|
||||
|
||||
@@ -112,15 +112,15 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}series/${novelUrl}/${chapterUrl}/`;
|
||||
const url = `${baseUrl}series/${novelUrl}${chapterUrl}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h1#chapter-heading').text();
|
||||
|
||||
const chapterName = loadedCheerio('.reading-content b:first').text();
|
||||
loadedCheerio('input, .reading-content b:first').remove();
|
||||
let chapterText = loadedCheerio('.reading-content').html();
|
||||
|
||||
const chapter = {
|
||||
@@ -144,11 +144,11 @@ const searchNovels = async searchTerm => {
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.c-tabs-item__content').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.h4 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
loadedCheerio('.tab-thumb').each(function () {
|
||||
const novelName = loadedCheerio(this).find('a').attr('title');
|
||||
const novelCover = loadedCheerio(this).find('img').attr('data-src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('.h4 > a').attr('href');
|
||||
let novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
novelUrl = novelUrl.replace(`${baseUrl}series/`, '');
|
||||
|
||||
const novel = {
|
||||
|
||||
+17
-25
@@ -23,12 +23,12 @@ const popularNovels = async page => {
|
||||
if (novelName) {
|
||||
const novelId = loadedCheerio(this).find('td:nth-child(1)').text();
|
||||
|
||||
const novelCover = baseUrl + '/data/image/' + novelId + '.jpg';
|
||||
const novelCover = baseUrl + 'data/image/' + novelId + '.jpg';
|
||||
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('td.novel > a')
|
||||
.attr('href')
|
||||
.replace('/novel/', '');
|
||||
.replace('/book/', '');
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
@@ -45,7 +45,7 @@ const popularNovels = async page => {
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = `${baseUrl}novel/${novelUrl}/`;
|
||||
const url = `${baseUrl}book/${novelUrl}/`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
@@ -59,25 +59,19 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio(
|
||||
'body > div.container-fluid.text-center > div.row.content > div.col-sm-8.text-left > div:nth-child(6) > div.panel-heading.clearfix > h4',
|
||||
).text();
|
||||
novel.novelName = loadedCheerio('h4[itemprop="name>"]:first').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('img[itemprop="image"]').attr('src');
|
||||
novel.novelCover = loadedCheerio('.img-thumbnail').attr('src');
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
'div.col-md-6 > div > div:nth-child(2) > div > div:nth-child(2) > a',
|
||||
).text();
|
||||
novel.author = loadedCheerio('h4:contains("Author:")').next().text();
|
||||
|
||||
novel.genre = loadedCheerio(
|
||||
'div.row > div.col-md-6 > div > a:nth-child(4)',
|
||||
).text();
|
||||
|
||||
novel.summary = loadedCheerio('div[itemprop="description"]')
|
||||
.find('p')
|
||||
.text()
|
||||
.trim();
|
||||
novel.genre = loadedCheerio('h4:contains("Genre:")')
|
||||
.nextUntil('h4')
|
||||
.map((i, el) => loadedCheerio(el).text())
|
||||
.toArray()
|
||||
.join(',');
|
||||
|
||||
novel.summary = loadedCheerio('h4:contains("Description:")').next().text();
|
||||
let novelChapters = [];
|
||||
|
||||
loadedCheerio('table#chplist > #chapters > tr').each(function () {
|
||||
@@ -137,20 +131,18 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}novel/${novelUrl}/${chapterUrl}`;
|
||||
const url = `${baseUrl}book/${novelUrl}/${chapterUrl}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let chapterName = loadedCheerio('div.panel-body.article').find('h4').text();
|
||||
let chapterName = loadedCheerio('.panel-body.article').find('h4').text();
|
||||
|
||||
loadedCheerio('ul.pager').remove();
|
||||
loadedCheerio('ul.pager, .panel-body.article div,button,span').remove();
|
||||
|
||||
let chapterText = loadedCheerio('div.panel-body.article')
|
||||
.html()
|
||||
.replace(/<span(.*?)>(.*?)<\/span>/g, '');
|
||||
let chapterText = loadedCheerio('.panel-body.article').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
@@ -184,7 +176,7 @@ const searchNovels = async searchTerm => {
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('a')
|
||||
.attr('href')
|
||||
.replace(baseUrl + 'novel/', '');
|
||||
.replace(baseUrl + 'book/', '');
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
const baseUrl = 'https://es.mtlnovel.com';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const url = `${baseUrl}/alltime-rank/page/${page}`;
|
||||
|
||||
// console.log(url);
|
||||
|
||||
let headers = new Headers({
|
||||
referer: 'https://es.mtlnovel.com/',
|
||||
'User-Agent':
|
||||
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
|
||||
});
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('div.box.wide').each(function () {
|
||||
const novelName = loadedCheerio(this).find('a.list-title').text().slice(4);
|
||||
const novelCover = loadedCheerio(this).find('amp-img').attr('src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('a.list-title').attr('href');
|
||||
novelUrl = novelUrl.replace('https://es.mtlnovel.com/', '');
|
||||
|
||||
const novel = {
|
||||
sourceId: 173,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = `${baseUrl}/${novelUrl}`;
|
||||
|
||||
let headers = new Headers({
|
||||
referer: 'https://es.mtlnovel.com/alltime-rank/',
|
||||
'User-Agent':
|
||||
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
|
||||
});
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {};
|
||||
|
||||
novel.sourceId = 173;
|
||||
|
||||
novel.sourceName = 'Esmtlnovel';
|
||||
|
||||
novel.url = url;
|
||||
|
||||
novel.novelUrl = novelUrl;
|
||||
|
||||
novel.novelName = loadedCheerio('h1.entry-title').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('.nov-head > amp-img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('div.desc > h2').next().text().trim();
|
||||
|
||||
novel.author = loadedCheerio('tr > td')
|
||||
.filter(function () {
|
||||
return loadedCheerio(this).prev().text().trim() === 'Author';
|
||||
})
|
||||
.next()
|
||||
.text()
|
||||
.replace('Auhtor:', '');
|
||||
|
||||
novel.status = loadedCheerio('tr > td')
|
||||
.filter(function () {
|
||||
return loadedCheerio(this).prev().text().trim() === 'Status';
|
||||
})
|
||||
.next()
|
||||
.text()
|
||||
.replace('Status:', '');
|
||||
|
||||
novel.genre = loadedCheerio('td')
|
||||
.filter(function () {
|
||||
return loadedCheerio(this).prev().text().trim() === 'Genre';
|
||||
})
|
||||
.next()
|
||||
.text()
|
||||
.replace('Genre:', '');
|
||||
|
||||
novel.artist = null;
|
||||
|
||||
const chapterListUrl = url + '/chapter-list/';
|
||||
|
||||
const getChapters = async () => {
|
||||
const listResult = await fetch(chapterListUrl);
|
||||
const listBody = await listResult.text();
|
||||
|
||||
loadedCheerio = cheerio.load(listBody);
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
loadedCheerio('div.ch-list')
|
||||
.find('a.ch-link')
|
||||
.each(function () {
|
||||
const chapterName = loadedCheerio(this).text().replace('~ ', '');
|
||||
const releaseDate = null;
|
||||
|
||||
let chapterUrl = loadedCheerio(this).attr('href');
|
||||
chapterUrl = chapterUrl.replace(
|
||||
`https://es.mtlnovel.com/${novelUrl}`,
|
||||
'',
|
||||
);
|
||||
|
||||
novelChapters.push({
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
releaseDate,
|
||||
});
|
||||
});
|
||||
return novelChapters.reverse();
|
||||
};
|
||||
|
||||
novel.chapters = await getChapters();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}/${novelUrl}${chapterUrl}`;
|
||||
// console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h1.main-title').text();
|
||||
let chapterText = loadedCheerio('div.par').html();
|
||||
const chapter = {
|
||||
sourceId: 173,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const searchUrl =
|
||||
'https://es.mtlnovel.com/wp-admin/admin-ajax.php?action=autosuggest&q=' +
|
||||
searchTerm +
|
||||
'&__amp_source_origin=https%3A%2F%2Fes.mtlnovel.com';
|
||||
|
||||
const res = await fetch(searchUrl);
|
||||
const result = await res.json();
|
||||
|
||||
let novels = [];
|
||||
|
||||
result.items[0].results.map(item => {
|
||||
const novelName = item.title.replace(/<\/?strong>/g, '');
|
||||
const novelCover = item.thumbnail;
|
||||
const novelUrl = item.permalink.replace('https://es.mtlnovel.com/', '');
|
||||
|
||||
const novel = { sourceId: 173, novelName, novelCover, novelUrl };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const mtlNovelEsScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default mtlNovelEsScraper;
|
||||
@@ -2,6 +2,10 @@ import * as cheerio from 'cheerio';
|
||||
const baseUrl = 'https://hasutl.wordpress.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
if (page !== 1) {
|
||||
return { novels: [] };
|
||||
}
|
||||
|
||||
let url = baseUrl + 'light-novels-activas/';
|
||||
|
||||
const result = await fetch(url);
|
||||
@@ -125,8 +129,8 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga`;
|
||||
const searchNovels = async (searchTerm, page) => {
|
||||
const url = `${baseUrl}page/${page}/?s=${searchTerm}&post_type=wp-manga`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
import { bypassImages } from '../helpers/cloudflareImagesBypass';
|
||||
import { htmlToText } from '../helpers/htmlToText';
|
||||
|
||||
const baseUrl = 'https://novelasligeras.net';
|
||||
const searchQuery =
|
||||
'/wp-admin/admin-ajax.php?tags=1&sku=&limit=30&category_results=&order=DESC&category_limit=5&order_by=title&product_thumbnails=1&title=1&excerpt=1&content=&categories=1&attributes=1';
|
||||
|
||||
const sourceName = 'NOVA';
|
||||
const sourceId = 166;
|
||||
|
||||
const popularNovels = async page => {
|
||||
return { novels: await searchNovels('', page) };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = baseUrl + novelUrl;
|
||||
|
||||
const body = await fetchHtml({ url });
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
const novelCoverImg = loadedCheerio('.woocommerce-product-gallery').find(
|
||||
'img',
|
||||
);
|
||||
|
||||
novel.novelName = loadedCheerio('h1').text();
|
||||
novel.novelCover =
|
||||
novelCoverImg.attr('src') || novelCoverImg.attr('data-cfsrc');
|
||||
|
||||
novel.artist = loadedCheerio(
|
||||
'.woocommerce-product-attributes-item--attribute_pa_ilustrador td',
|
||||
).text();
|
||||
novel.author = loadedCheerio(
|
||||
'.woocommerce-product-attributes-item--attribute_pa_escritor td',
|
||||
).text();
|
||||
novel.status = loadedCheerio(
|
||||
'.woocommerce-product-attributes-item--attribute_pa_estado td',
|
||||
).text();
|
||||
|
||||
novel.summary = htmlToText(
|
||||
loadedCheerio('.woocommerce-product-details__short-description').html(),
|
||||
);
|
||||
|
||||
novel.chapters = [];
|
||||
|
||||
loadedCheerio('.vc_row div.vc_column-inner > div.wpb_wrapper').each(
|
||||
function () {
|
||||
const e = loadedCheerio(this);
|
||||
const volume = e.find('.dt-fancy-title').first().text();
|
||||
|
||||
if (!volume.startsWith('Volumen')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.find('.wpb_tab a').map(function () {
|
||||
const chapterPartName = loadedCheerio(this).text();
|
||||
const chapterUrl = loadedCheerio(this)
|
||||
.attr('href')
|
||||
.replace(baseUrl, '');
|
||||
|
||||
const match = chapterPartName.match(/(Parte \d+) . (.+?): (.+)/);
|
||||
const part = match?.[1];
|
||||
const chapter = match?.[2];
|
||||
const name = match?.[3];
|
||||
|
||||
let chapterName;
|
||||
if (part && chapter) {
|
||||
chapterName = `${volume} - ${chapter} - ${part}: ${name}`;
|
||||
} else {
|
||||
chapterName = `${volume} - ${chapterPartName}`;
|
||||
}
|
||||
|
||||
novel.chapters.push({ chapterName, chapterUrl });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = baseUrl + chapterUrl;
|
||||
|
||||
const body = await fetchHtml({ url });
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let chapterName = loadedCheerio('h2')
|
||||
.toArray()
|
||||
.map(e => loadedCheerio(e).text())
|
||||
.join(' ');
|
||||
|
||||
let chapterText;
|
||||
if (body.includes('Nadie entra sin permiso en la Gran Tumba de Nazarick')) {
|
||||
chapterText = loadedCheerio('#content');
|
||||
} else {
|
||||
chapterText = loadedCheerio(
|
||||
'.wpb_text_column.wpb_content_element > .wpb_wrapper',
|
||||
);
|
||||
}
|
||||
|
||||
// Remove ads
|
||||
chapterText.find('center').each(function () {
|
||||
loadedCheerio(this).remove();
|
||||
});
|
||||
|
||||
chapterText.find('*').each(function () {
|
||||
if (/text-align:.center/.test(loadedCheerio(this).attr('style'))) {
|
||||
loadedCheerio(this).replaceWith(
|
||||
`<center>${loadedCheerio(this).html()}</center>`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
chapterText = await bypassImages(loadedCheerio, chapterText, sourceId);
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm, searchPage = 0) => {
|
||||
let novels = [];
|
||||
|
||||
if (searchPage > 0) {
|
||||
const url = `${baseUrl}/index.php/page/${searchPage}/?s=${searchTerm}&post_type=product&title=1&excerpt=1&content=0&categories=1&attributes=1&tags=1&sku=0&orderby=popularity&ixwps=1`;
|
||||
|
||||
const body = await fetchHtml({ url });
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
loadedCheerio('.dt-css-grid')
|
||||
.find('div.wf-cell')
|
||||
.each(function () {
|
||||
const img = loadedCheerio(this).find('img');
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: loadedCheerio(this).find('h4.entry-title a').text(),
|
||||
novelCover: img.attr('data-src') || img.attr('data-cfsrc'),
|
||||
novelUrl: loadedCheerio(this)
|
||||
.find('h4.entry-title a')
|
||||
.attr('href')
|
||||
.replace(baseUrl, ''),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const url = baseUrl + searchQuery;
|
||||
const form = new FormData();
|
||||
|
||||
form.append('action', 'product_search');
|
||||
form.append('product-search', searchPage ?? 1);
|
||||
form.append('product-query', searchTerm);
|
||||
|
||||
const data = await fetchHtml({
|
||||
url,
|
||||
init: {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
},
|
||||
sourceId: sourceId.toString(),
|
||||
});
|
||||
|
||||
novels = JSON.parse(data).map(novel => ({
|
||||
sourceId,
|
||||
novelName: novel.title,
|
||||
novelCover: novel.thumbnail,
|
||||
novelUrl: novel.url.replace(baseUrl, ''),
|
||||
}));
|
||||
}
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const NOVAScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default NOVAScraper;
|
||||
@@ -1,9 +1,14 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { htmlToText } from '../helpers/htmlToText';
|
||||
|
||||
const baseUrl = 'https://novelasligera.com/';
|
||||
const sourceName = 'Novelas Ligera';
|
||||
const sourceId = 26;
|
||||
|
||||
const popularNovels = async page => {
|
||||
if (page > 1) {
|
||||
return { novels: [] };
|
||||
}
|
||||
|
||||
let url = baseUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
@@ -18,14 +23,14 @@ const popularNovels = async page => {
|
||||
.find('.widget-image-caption.wp-caption-text')
|
||||
.text();
|
||||
if (novelName) {
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelCover = loadedCheerio(this).find('img').attr('data-lazy-src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
novelUrl = novelUrl.replace(baseUrl, '');
|
||||
novelUrl = novelUrl.replace('novela/', '');
|
||||
|
||||
const novel = {
|
||||
sourceId: 26,
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
@@ -41,30 +46,24 @@ const popularNovels = async page => {
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = baseUrl + 'novela/' + novelUrl;
|
||||
|
||||
// console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {};
|
||||
|
||||
novel.sourceId = 26;
|
||||
|
||||
novel.sourceName = 'Novelas Ligera';
|
||||
|
||||
novel.url = url;
|
||||
|
||||
novel.novelUrl = novelUrl;
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('h1').text();
|
||||
|
||||
novel.novelCover = loadedCheerio('.elementor-widget-container')
|
||||
.find('img')
|
||||
.attr('src');
|
||||
.attr('data-lazy-src');
|
||||
|
||||
loadedCheerio('.elementor-row')
|
||||
loadedCheerio('.elementor-widget-container')
|
||||
.find('p')
|
||||
.each(function () {
|
||||
if (loadedCheerio(this).text().includes('Autor:')) {
|
||||
@@ -83,11 +82,16 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
}
|
||||
});
|
||||
|
||||
novel.artist = null;
|
||||
|
||||
novel.novelSummary = loadedCheerio(
|
||||
'.elementor-text-editor.elementor-clearfix',
|
||||
).text();
|
||||
const summary = loadedCheerio(
|
||||
'div[data-widget_type="text-editor.default"] .elementor-widget-container',
|
||||
).first();
|
||||
summary.find('div.yasr-visitor-votes').remove();
|
||||
novel.summary = summary
|
||||
.children()
|
||||
.toArray()
|
||||
.filter(e => e.childNodes[0]?.type === 'text')
|
||||
.map(e => e.childNodes[0].data)
|
||||
.join('\n\n');
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
@@ -115,7 +119,6 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = baseUrl + 'novela/' + chapterUrl;
|
||||
// console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
@@ -131,7 +134,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
loadedCheerio('.wp-post-navigation').remove();
|
||||
|
||||
let chapterText = loadedCheerio('.entry-content').html();
|
||||
chapterText = htmlToText(chapterText, { preserveNewlines: true });
|
||||
|
||||
const chapter = {
|
||||
sourceId: 26,
|
||||
@@ -146,7 +148,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = baseUrl + '?s=' + searchTerm + '&post_type=wp-manga';
|
||||
// console.log(url);
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
@@ -156,7 +157,7 @@ const searchNovels = async searchTerm => {
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.inside-article').each(function () {
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelCover = loadedCheerio(this).find('img').attr('data-lazy-src');
|
||||
let novelUrl = loadedCheerio(this).find('a').attr('href').split('/')[4];
|
||||
|
||||
let novelName;
|
||||
@@ -179,8 +180,6 @@ const searchNovels = async searchTerm => {
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
novels = [{ ...novels[1] }];
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user