Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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>
|
||||
|
||||
@@ -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
+147
-14
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.15",
|
||||
"version": "1.1.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lnreader",
|
||||
"version": "1.1.15",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@cd-z/react-native-epub-creator": "^2.0.4",
|
||||
"@gorhom/bottom-sheet": "^4.4.5",
|
||||
@@ -2110,9 +2110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cd-z/react-native-epub-creator/node_modules/expo-file-system": {
|
||||
"version": "15.4.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-15.4.2.tgz",
|
||||
"integrity": "sha512-WFaEWuFEuUpETiq85YlhKYJgedccWTjtCMnYGAgyNfCfvnIgfMCVH7dWudGuxhfAcTZqh36OcqtSckbtbhOtyg==",
|
||||
"version": "15.4.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-15.4.3.tgz",
|
||||
"integrity": "sha512-HaaCBTUATs2+i7T4jxIvoU9rViAHMvOD2eBaJ1H7xPHlwZlMORjQs7bsNKonR/TQoduxZBJLVZGawvaAJNCH8g==",
|
||||
"dependencies": {
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
@@ -14823,14 +14823,15 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-native": {
|
||||
"version": "0.69.12",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.12.tgz",
|
||||
"integrity": "sha512-kHOj4V0wD/hvjyiiXPP96fMBSRNnx0MXGTTrL43KppsWvKnOtvnQ/qSTWR0d3s6Hi4n7mx3Un+NqCuBKsf+5yg==",
|
||||
"version": "0.69.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.9.tgz",
|
||||
"integrity": "sha512-I1xqIn47RWxBToO4E6yqyIPSaK9mZnMiscMfrFpWjQr3Gdkicr9y+twmtrRszxaLdQLjHzh/M3y4qOqc3hZnpg==",
|
||||
"deprecated": "Issues and pull requests filed against this version are not supported. See the React Native release support policy to learn more: https://github.com/reactwg/react-native-releases#releases-support-policy",
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^27.0.1",
|
||||
"@react-native-community/cli": "^8.0.7",
|
||||
"@react-native-community/cli-platform-android": "^8.0.5",
|
||||
"@react-native-community/cli-platform-ios": "^8.0.6",
|
||||
"@react-native-community/cli": "^8.0.4",
|
||||
"@react-native-community/cli-platform-android": "^8.0.4",
|
||||
"@react-native-community/cli-platform-ios": "^8.0.4",
|
||||
"@react-native/assets": "1.0.0",
|
||||
"@react-native/normalize-color": "2.0.0",
|
||||
"@react-native/polyfills": "2.0.0",
|
||||
@@ -14842,9 +14843,9 @@
|
||||
"invariant": "^2.2.4",
|
||||
"jsc-android": "^250230.2.1",
|
||||
"memoize-one": "^5.0.0",
|
||||
"metro-react-native-babel-transformer": "0.70.4",
|
||||
"metro-runtime": "0.70.4",
|
||||
"metro-source-map": "0.70.4",
|
||||
"metro-react-native-babel-transformer": "0.70.3",
|
||||
"metro-runtime": "0.70.3",
|
||||
"metro-source-map": "0.70.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nullthrows": "^1.1.1",
|
||||
"pretty-format": "^26.5.2",
|
||||
@@ -15357,6 +15358,130 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/metro-babel-transformer": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.70.3.tgz",
|
||||
"integrity": "sha512-bWhZRMn+mIOR/s3BDpFevWScz9sV8FGktVfMlF1eJBLoX24itHDbXvTktKBYi38PWIKcHedh6THSFpJogfuwNA==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.14.0",
|
||||
"hermes-parser": "0.6.0",
|
||||
"metro-source-map": "0.70.3",
|
||||
"nullthrows": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/metro-react-native-babel-preset": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.3.tgz",
|
||||
"integrity": "sha512-4Nxc1zEiHEu+GTdEMEsHnRgfaBkg8f/Td3+FcQ8NTSvs+xL3LBrQy6N07idWSQZHIdGFf+tTHvRfSIWLD8u8Tg==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.14.0",
|
||||
"@babel/plugin-proposal-async-generator-functions": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-syntax-export-default-from": "^7.0.0",
|
||||
"@babel/plugin-syntax-flow": "^7.2.0",
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0",
|
||||
"@babel/plugin-syntax-optional-chaining": "^7.0.0",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.0.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.0.0",
|
||||
"@babel/plugin-transform-block-scoping": "^7.0.0",
|
||||
"@babel/plugin-transform-classes": "^7.0.0",
|
||||
"@babel/plugin-transform-computed-properties": "^7.0.0",
|
||||
"@babel/plugin-transform-destructuring": "^7.0.0",
|
||||
"@babel/plugin-transform-exponentiation-operator": "^7.0.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.0.0",
|
||||
"@babel/plugin-transform-function-name": "^7.0.0",
|
||||
"@babel/plugin-transform-literals": "^7.0.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
|
||||
"@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0",
|
||||
"@babel/plugin-transform-parameters": "^7.0.0",
|
||||
"@babel/plugin-transform-react-display-name": "^7.0.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.0.0",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.0.0",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.0.0",
|
||||
"@babel/plugin-transform-spread": "^7.0.0",
|
||||
"@babel/plugin-transform-sticky-regex": "^7.0.0",
|
||||
"@babel/plugin-transform-template-literals": "^7.0.0",
|
||||
"@babel/plugin-transform-typescript": "^7.5.0",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.0.0",
|
||||
"@babel/template": "^7.0.0",
|
||||
"react-refresh": "^0.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/metro-react-native-babel-transformer": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.70.3.tgz",
|
||||
"integrity": "sha512-WKBU6S/G50j9cfmFM4k4oRYprd8u3qjleD4so1E2zbTNILg+gYla7ZFGCAvi2G0ZcqS2XuGCR375c2hF6VVvwg==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.14.0",
|
||||
"babel-preset-fbjs": "^3.4.0",
|
||||
"hermes-parser": "0.6.0",
|
||||
"metro-babel-transformer": "0.70.3",
|
||||
"metro-react-native-babel-preset": "0.70.3",
|
||||
"metro-source-map": "0.70.3",
|
||||
"nullthrows": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/metro-runtime": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.70.3.tgz",
|
||||
"integrity": "sha512-22xU7UdXZacniTIDZgN2EYtmfau2pPyh97Dcs+cWrLcJYgfMKjWBtesnDcUAQy3PHekDYvBdJZkoQUeskYTM+w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/metro-source-map": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.70.3.tgz",
|
||||
"integrity": "sha512-zsYtZGrwRbbGEFHtmMqqeCH9K9aTGNVPsurMOWCUeQA3VGyVGXPGtLMC+CdAM9jLpUyg6jw2xh0esxi+tYH7Uw==",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"metro-symbolicate": "0.70.3",
|
||||
"nullthrows": "^1.1.1",
|
||||
"ob1": "0.70.3",
|
||||
"source-map": "^0.5.6",
|
||||
"vlq": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/metro-symbolicate": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.70.3.tgz",
|
||||
"integrity": "sha512-JTYkF1dpeDUssQ84juE1ycnhHki2ylJBBdJE1JHtfu5oC+z1ElDbBdPHq90Uvt8HbRov/ZAnxvv7Zy6asS+WCA==",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
"metro-source-map": "0.70.3",
|
||||
"nullthrows": "^1.1.1",
|
||||
"source-map": "^0.5.6",
|
||||
"through2": "^2.0.1",
|
||||
"vlq": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"metro-symbolicate": "src/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/ob1": {
|
||||
"version": "0.70.3",
|
||||
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.70.3.tgz",
|
||||
"integrity": "sha512-Vy9GGhuXgDRY01QA6kdhToPd8AkLdLpX9GjH5kpqluVqTu70mgOm7tpGoJDZGaNbr9nJlJgnipqHJQRPORixIQ=="
|
||||
},
|
||||
"node_modules/react-native/node_modules/pretty-format": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
|
||||
@@ -15384,6 +15509,14 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-native/node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.15",
|
||||
"version": "1.1.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -81,7 +81,7 @@ const InfoItem = ({
|
||||
theme,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
icon?: string;
|
||||
theme: ThemeColors;
|
||||
}) => (
|
||||
<View style={styles.infoCtn}>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { StyleSheet, View, Text, Pressable } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
|
||||
import { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
import color from 'color';
|
||||
|
||||
const ListView = ({
|
||||
@@ -16,6 +19,7 @@ const ListView = ({
|
||||
isSelected,
|
||||
onLongPress,
|
||||
}) => {
|
||||
const { cookies = '' } = getSourceStorage(item.sourceId);
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
@@ -31,6 +35,7 @@ const ListView = ({
|
||||
<FastImage
|
||||
source={{
|
||||
uri: item.novelCover,
|
||||
headers: { Cookie: cookies, '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,9 @@ import { SourceNovelItem } from 'src/sources/types';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
|
||||
|
||||
import { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
interface NovelCoverProps {
|
||||
item: LibraryNovelInfo;
|
||||
onPress: () => void;
|
||||
@@ -65,6 +68,8 @@ const NovelCover: React.FC<NovelCoverProps> = ({
|
||||
|
||||
const uri = item.novelCover;
|
||||
|
||||
const { cookies = '' } = getSourceStorage(item.sourceId);
|
||||
|
||||
return item.sourceId < 0 ? (
|
||||
<SourceScreenSkeletonLoading theme={theme} completeRow={item.sourceId} />
|
||||
) : displayMode !== DisplayModes.List ? (
|
||||
@@ -75,9 +80,9 @@ const NovelCover: React.FC<NovelCoverProps> = ({
|
||||
},
|
||||
styles.standardNovelCover,
|
||||
isSelected && {
|
||||
backgroundColor: theme.primary,
|
||||
} &&
|
||||
styles.selectedNovelCover,
|
||||
backgroundColor: theme.primary,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
@@ -108,7 +113,10 @@ const NovelCover: React.FC<NovelCoverProps> = ({
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{ uri }}
|
||||
source={{
|
||||
uri,
|
||||
headers: { Cookie: cookies, 'User-Agent': defaultUserAgentString },
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
height: coverHeight,
|
||||
@@ -348,9 +356,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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -105,6 +109,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';
|
||||
|
||||
@@ -130,6 +130,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
) : (
|
||||
<NovelList
|
||||
data={novelList}
|
||||
isFetchingNextPage={hasNextPage && !searchText}
|
||||
inSource
|
||||
renderItem={({ item }) => {
|
||||
const inLibrary = novelInLibrary(item.novelUrl);
|
||||
|
||||
@@ -8,6 +8,9 @@ import { SourceNovelItem } from '../../../sources/types';
|
||||
import { ThemeColors } from '../../../theme/types';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
interface Props {
|
||||
novel: SourceNovelItem;
|
||||
navigateToNovel: (novel: SourceNovelItem) => void;
|
||||
@@ -32,6 +35,8 @@ const GlobalSearchNovelItem: React.FC<Props> = ({
|
||||
[inLibrary],
|
||||
);
|
||||
|
||||
const { cookies = '' } = getSourceStorage(novel.sourceId);
|
||||
|
||||
return (
|
||||
<View style={styles.novelItem}>
|
||||
<Pressable
|
||||
@@ -41,7 +46,10 @@ const GlobalSearchNovelItem: React.FC<Props> = ({
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
source={{
|
||||
uri: novel.novelCover,
|
||||
headers: { Cookie: cookies, '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',
|
||||
},
|
||||
|
||||
@@ -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,22 @@ 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 { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -56,6 +59,8 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
[chapterName, historyTimeRead],
|
||||
);
|
||||
|
||||
const { cookies = '' } = getSourceStorage(sourceId);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.container}
|
||||
@@ -85,7 +90,16 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
})
|
||||
}
|
||||
>
|
||||
<FastImage source={{ uri: novelCover }} style={styles.cover} />
|
||||
<FastImage
|
||||
source={{
|
||||
uri: novelCover,
|
||||
headers: {
|
||||
Cookie: cookies,
|
||||
'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));
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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,
|
||||
@@ -49,15 +56,15 @@ 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';
|
||||
@@ -104,6 +111,22 @@ 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 });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getNovelAction(
|
||||
@@ -129,8 +152,8 @@ const Novel = ({ route, navigation }) => {
|
||||
progressViewOffset={progressViewOffset}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={updating}
|
||||
colors={[theme.primary]}
|
||||
progressBackgroundColor={theme.onPrimary}
|
||||
colors={[theme.onPrimary]}
|
||||
progressBackgroundColor={theme.primary}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -294,7 +317,7 @@ const Novel = ({ route, navigation }) => {
|
||||
const navigateToChapter = chapter => {
|
||||
navigation.navigate(
|
||||
'Chapter',
|
||||
openChapter({ sourceId, novelUrl, novelName }, chapter),
|
||||
getChapterScreenRouteParams({ sourceId, novelUrl, novelName }, chapter),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -340,7 +363,6 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const renderItem = ({ item: it, index }) => (
|
||||
<ChapterItem
|
||||
theme={theme}
|
||||
chapter={it}
|
||||
index={index}
|
||||
showChapterTitles={showChapterTitles}
|
||||
@@ -367,7 +389,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 +420,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 +557,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 +601,7 @@ const Novel = ({ route, navigation }) => {
|
||||
/>
|
||||
</Menu>
|
||||
</Row>
|
||||
</View>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(150)}
|
||||
@@ -638,6 +661,7 @@ const Novel = ({ route, navigation }) => {
|
||||
deleteDownloadsSnackbar={deleteDownloadsSnackbar}
|
||||
/>
|
||||
}
|
||||
onScroll={onPageScroll}
|
||||
refreshControl={refreshControl()}
|
||||
/>
|
||||
</View>
|
||||
@@ -655,7 +679,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>
|
||||
|
||||
@@ -11,6 +11,9 @@ import { followNovelAction } from '../../../../redux/novel/novel.actions';
|
||||
import { useSettings } from '../../../../hooks/reduxHooks';
|
||||
import { showToast } from '../../../../hooks/showToast';
|
||||
|
||||
import { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString } from '@utils/fetch/fetch';
|
||||
|
||||
import {
|
||||
CoverImage,
|
||||
NovelAuthor,
|
||||
@@ -54,16 +57,30 @@ const NovelInfoHeader = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const { cookies = '' } = getSourceStorage(novel.sourceId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CoverImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
source={{
|
||||
uri: novel.novelCover,
|
||||
headers: {
|
||||
Cookie: cookies,
|
||||
'User-Agent': defaultUserAgentString,
|
||||
},
|
||||
}}
|
||||
theme={theme}
|
||||
hideBackdrop={hideBackdrop}
|
||||
>
|
||||
<NovelInfoContainer>
|
||||
<NovelThumbnail
|
||||
source={{ uri: novel.novelCover }}
|
||||
source={{
|
||||
uri: novel.novelCover,
|
||||
headers: {
|
||||
Cookie: cookies,
|
||||
'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 (
|
||||
|
||||
@@ -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';
|
||||
@@ -145,7 +151,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,15 +201,16 @@ 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();
|
||||
}
|
||||
});
|
||||
|
||||
const scrollTo = useCallback(
|
||||
offsetY => {
|
||||
@@ -268,13 +275,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,18 +322,28 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scrollToSavedProgress = () => scrollTo(position?.position);
|
||||
const scrollToSavedProgress = useCallback(
|
||||
() => scrollTo(position?.position),
|
||||
[],
|
||||
);
|
||||
|
||||
const chapterText = sanitizeChapterText(chapter.chapterText, {
|
||||
removeExtraParagraphSpacing,
|
||||
bionicReading,
|
||||
sourceId: sourceId,
|
||||
});
|
||||
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 />;
|
||||
}
|
||||
@@ -333,9 +352,6 @@ const ChapterContent = ({ route, navigation }) => {
|
||||
return <ErrorScreenV2 error={error} />;
|
||||
}
|
||||
|
||||
const bookmarkChapter = () =>
|
||||
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebViewReader
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
@@ -70,6 +71,11 @@ const AdvancedSettings = ({ navigation }) => {
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item title="Clear cookies" onPress={clearCookies} theme={theme} />
|
||||
<List.Item
|
||||
title="Import Epub"
|
||||
onPress={() => openDirectory()}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
<Portal>
|
||||
<ConfirmationDialog
|
||||
|
||||
@@ -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;
|
||||
@@ -105,10 +105,12 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
|
||||
* Chapter Sort Modal
|
||||
*/
|
||||
const defaultChapterSortModal = useBoolean();
|
||||
|
||||
/**
|
||||
* Convert to EPUB Modal
|
||||
*/
|
||||
const convertToEpubModal = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar
|
||||
@@ -263,7 +265,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 +344,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>
|
||||
</>
|
||||
|
||||
@@ -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,29 @@ import { useAppDispatch } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
import { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
|
||||
const NovelCover = ({
|
||||
uri,
|
||||
navigateToNovel,
|
||||
sourceId,
|
||||
}: {
|
||||
uri: string;
|
||||
navigateToNovel: () => void;
|
||||
sourceId: number;
|
||||
}) => {
|
||||
const { cookies = '' } = getSourceStorage(sourceId);
|
||||
return (
|
||||
<Pressable onPress={navigateToNovel}>
|
||||
<FastImage source={{ uri }} style={styles.cover} />
|
||||
<Pressable
|
||||
onPress={navigateToNovel}
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri, headers: { 'cookie': cookies } }}
|
||||
style={styles.cover}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -90,7 +101,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 +109,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 +160,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
|
||||
<NovelCover
|
||||
navigateToNovel={navigateToNovel}
|
||||
uri={chapterList[0].novelCover}
|
||||
sourceId={chapterList[0].sourceId}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
@@ -172,6 +188,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
|
||||
<NovelCover
|
||||
navigateToNovel={navigateToNovel}
|
||||
uri={chapterList[0].novelCover}
|
||||
sourceId={chapterList[0].sourceId}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
@@ -186,7 +203,8 @@ export default UpdateNovelCard;
|
||||
const styles = StyleSheet.create({
|
||||
padding: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 3,
|
||||
paddingVertical: 0,
|
||||
alignItems: 'center',
|
||||
height: 64,
|
||||
},
|
||||
container: {
|
||||
@@ -198,9 +216,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
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 && genres.length > 0) {
|
||||
novel.genres = 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;
|
||||
let pageNumber = 1;
|
||||
|
||||
/*
|
||||
* TODO: Maybe there are other ways to get the translation table
|
||||
* It is embed and encrypted inside readtool.js
|
||||
*/
|
||||
const mapping_dict = {
|
||||
'“': '「',
|
||||
'’': '』',
|
||||
'': '是',
|
||||
'': '不',
|
||||
'': '他',
|
||||
'': '个',
|
||||
'': '来',
|
||||
'': '大',
|
||||
'': '子',
|
||||
'': '说',
|
||||
'': '年',
|
||||
'': '那',
|
||||
'': '她',
|
||||
'': '得',
|
||||
'': '自',
|
||||
'': '家',
|
||||
'': '而',
|
||||
'': '去',
|
||||
'': '小',
|
||||
'': '于',
|
||||
'': '么',
|
||||
'': '好',
|
||||
'': '发',
|
||||
'': '成',
|
||||
'': '事',
|
||||
'': '用',
|
||||
'': '道',
|
||||
'': '种',
|
||||
'': '乳',
|
||||
'': '茎',
|
||||
'': '肉',
|
||||
'': '胸',
|
||||
'': '淫',
|
||||
'': '射',
|
||||
'': '骚',
|
||||
'”': '」',
|
||||
'': '的',
|
||||
'': '了',
|
||||
'': '人',
|
||||
'': '有',
|
||||
'': '上',
|
||||
'': '到',
|
||||
'': '地',
|
||||
'': '中',
|
||||
'': '生',
|
||||
'': '着',
|
||||
'': '和',
|
||||
'': '出',
|
||||
'': '里',
|
||||
'': '以',
|
||||
'': '可',
|
||||
'': '过',
|
||||
'': '能',
|
||||
'': '多',
|
||||
'': '心',
|
||||
'': '之',
|
||||
'': '看',
|
||||
'': '当',
|
||||
'': '只',
|
||||
'': '把',
|
||||
'': '第',
|
||||
'': '想',
|
||||
'': '开',
|
||||
'': '阴',
|
||||
'': '欲',
|
||||
'': '交',
|
||||
'': '私',
|
||||
'': '臀',
|
||||
'': '脱',
|
||||
'': '唇',
|
||||
'‘': '『',
|
||||
'': '一',
|
||||
'': '我',
|
||||
'': '在',
|
||||
'': '这',
|
||||
'': '们',
|
||||
'': '时',
|
||||
'': '为',
|
||||
'': '你',
|
||||
'': '国',
|
||||
'': '就',
|
||||
'': '要',
|
||||
'': '也',
|
||||
'': '后',
|
||||
'': '会',
|
||||
'': '下',
|
||||
'': '天',
|
||||
'': '对',
|
||||
'': '然',
|
||||
'': '学',
|
||||
'': '都',
|
||||
'': '起',
|
||||
'': '没',
|
||||
'': '如',
|
||||
'': '还',
|
||||
'': '样',
|
||||
'': '作',
|
||||
'': '美',
|
||||
'': '液',
|
||||
'': '呻',
|
||||
'': '性',
|
||||
'': '穴',
|
||||
'': '舔',
|
||||
'': '裸',
|
||||
};
|
||||
|
||||
const addPage = async pageCheerio => {
|
||||
let pageText;
|
||||
|
||||
const formatPage = async () => {
|
||||
// Remove JS
|
||||
pageCheerio('#acontent .cgo').remove();
|
||||
|
||||
// Load lazyloaded images
|
||||
pageCheerio('#acontent 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');
|
||||
}
|
||||
});
|
||||
|
||||
// Recover the original character
|
||||
pageText = pageCheerio('#acontent').html();
|
||||
pageText = pageText.replace(/./g, char => mapping_dict[char] || char);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
await formatPage();
|
||||
chapterName ??=
|
||||
pageCheerio('#atitle + h3').text() +
|
||||
' — ' +
|
||||
pageCheerio('#atitle').text();
|
||||
chapterText += pageText;
|
||||
};
|
||||
|
||||
const loadPage = async url => {
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
const pageCheerio = cheerio.load(body);
|
||||
addPage(pageCheerio);
|
||||
pageHasNextPage =
|
||||
pageCheerio('#footlink > a[onclick$=ReadParams.url_next;]').text() ===
|
||||
'下一页'
|
||||
? true
|
||||
: false;
|
||||
return { pageCheerio, pageHasNextPage };
|
||||
};
|
||||
|
||||
let url = chapterUrl;
|
||||
do {
|
||||
const page = await loadPage(url);
|
||||
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 url = `${baseUrl}/search.html?searchkey=` + encodeURI(searchTerm);
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novels = [];
|
||||
|
||||
const loadSearchResults = function () {
|
||||
loadedCheerio('.book-ol .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 = {
|
||||
url: novelUrl,
|
||||
name: novelName,
|
||||
cover: novelCover,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const novelResults = loadedCheerio('.book-ol .book-layout');
|
||||
|
||||
if (novelResults.length === 0) {
|
||||
// console.log('Challenge');
|
||||
} else {
|
||||
loadSearchResults();
|
||||
}
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const LinovelibScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
+29
-33
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -111,7 +111,6 @@ const parseNovelAndChapters = async (novelUrl: string) => {
|
||||
|
||||
return novelChapters;
|
||||
};
|
||||
console.log(novelId);
|
||||
|
||||
if (novelId) {
|
||||
novel.chapters = await getChapters(novelId);
|
||||
|
||||
@@ -69,7 +69,6 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
console.log(novel.status);
|
||||
|
||||
novel.genre = loadedCheerio(
|
||||
'main > div.container > div > div.col-lg-3.col-md-4 > div:nth-child(4) > div.panel-body > ul',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
|
||||
const baseUrl = 'https://www.novelpub.com/';
|
||||
|
||||
const sourceName = 'NovelPub';
|
||||
@@ -14,8 +16,11 @@ const headers = new Headers({
|
||||
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,
|
||||
init: { headers: { 'User-Agent': userAgent } },
|
||||
});
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -41,8 +46,11 @@ 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,
|
||||
init: { headers: { 'User-Agent': userAgent } },
|
||||
});
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -125,9 +133,12 @@ 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,
|
||||
init: { headers: { 'User-Agent': userAgent } },
|
||||
});
|
||||
|
||||
const body = await result.text();
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h2').text();
|
||||
@@ -141,8 +152,11 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}lnwsearchlive?inputContent=${searchTerm}`;
|
||||
|
||||
const result = await fetch(url, { method: 'GET', headers });
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({
|
||||
url,
|
||||
sourceId,
|
||||
init: { headers: { 'User-Agent': userAgent } },
|
||||
});
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
|
||||
@@ -263,10 +263,11 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
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}/`;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ function getNovelName(y) {
|
||||
}
|
||||
|
||||
const popularNovels = async page => {
|
||||
if (page !== 1) {
|
||||
return { novels: [] };
|
||||
}
|
||||
|
||||
let url = baseUrl + 'p/todas-las-novelas.html';
|
||||
|
||||
let headers = new Headers({
|
||||
|
||||
@@ -2,6 +2,10 @@ import * as cheerio from 'cheerio';
|
||||
const baseUrl = 'https://oasistranslations.wordpress.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
if (page !== 1) {
|
||||
return { novels: [] };
|
||||
}
|
||||
|
||||
let url = baseUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const baseUrl = 'https://www.skynovels.net/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
if (page !== 1) {
|
||||
return { novels: [] };
|
||||
}
|
||||
|
||||
const url = 'https://api.skynovels.net/api/novels?&q';
|
||||
|
||||
const result = await fetch(url);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { defaultCoverUri, Status } from '../helpers/constants';
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
|
||||
const sourceId = 23;
|
||||
const sourceName = 'TuNovelaLigera';
|
||||
@@ -7,10 +8,9 @@ const sourceName = 'TuNovelaLigera';
|
||||
const baseUrl = 'https://tunovelaligera.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
let url = baseUrl + 'novelas/page/' + page + '/?m_orderby=rating';
|
||||
let url = `${baseUrl}novelas/page/${page}/?m_orderby=rating`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -18,7 +18,9 @@ const popularNovels = async page => {
|
||||
|
||||
loadedCheerio('.page-item-detail').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.h5 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelCoverImg = loadedCheerio(this).find('img');
|
||||
const novelCover =
|
||||
novelCoverImg.attr('src') || novelCoverImg.attr('data-cfsrc');
|
||||
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('.h5 > a')
|
||||
@@ -42,8 +44,7 @@ const popularNovels = async page => {
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = `${baseUrl}novelas/${novelUrl}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -58,10 +59,13 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
novel.novelName = loadedCheerio('.post-title > h1').text().trim();
|
||||
|
||||
let novelCover = loadedCheerio('.summary_image > a > img');
|
||||
let novelCover = loadedCheerio('.summary_image img');
|
||||
|
||||
novel.novelCover =
|
||||
novelCover.attr('data-src') || novelCover.attr('src') || defaultCoverUri;
|
||||
novelCover.attr('data-src') ||
|
||||
novelCover.attr('src') ||
|
||||
novelCover.attr('data-cfsrc') ||
|
||||
defaultCoverUri;
|
||||
|
||||
loadedCheerio('.post-content_item').each(function () {
|
||||
const detailName = loadedCheerio(this)
|
||||
@@ -98,14 +102,13 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
formData.append('action', 'manga_get_chapters');
|
||||
formData.append('manga', novelId);
|
||||
|
||||
const data = await fetch(
|
||||
'https://tunovelaligera.com/wp-admin/admin-ajax.php',
|
||||
{
|
||||
const text = await fetchHtml({
|
||||
url: 'https://tunovelaligera.com/wp-admin/admin-ajax.php',
|
||||
init: {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
const text = await data.text();
|
||||
});
|
||||
|
||||
loadedCheerio = cheerio.load(text);
|
||||
|
||||
@@ -135,8 +138,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}novelas/${novelUrl}/${chapterUrl}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -159,8 +161,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -168,7 +169,9 @@ const searchNovels = async searchTerm => {
|
||||
|
||||
loadedCheerio('.c-tabs-item__content').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.h4 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelCoverImg = loadedCheerio(this).find('img');
|
||||
const novelCover =
|
||||
novelCoverImg.attr('src') ?? novelCoverImg.attr('data-cfsrc');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('.h4 > a').attr('href');
|
||||
novelUrl = novelUrl.replace(`${baseUrl}novelas/`, '');
|
||||
|
||||
+20
-16
@@ -2,6 +2,10 @@ import * as cheerio from 'cheerio';
|
||||
const baseUrl = 'https://yuukitls.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
if (page !== 1) {
|
||||
return { novels: [] };
|
||||
}
|
||||
|
||||
let url = baseUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
@@ -11,25 +15,25 @@ const popularNovels = async page => {
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.menu-item-2869')
|
||||
.find('.menu-item.menu-item-type-post_type.menu-item-object-post')
|
||||
.each(function () {
|
||||
const novelName = loadedCheerio(this).text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
loadedCheerio(
|
||||
'li.quadmenu-item.quadmenu-item-object-post.quadmenu-item-level-2',
|
||||
).each(function () {
|
||||
const novelName = loadedCheerio(this).find('span.quadmenu-text').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
novelUrl = novelUrl.split('/');
|
||||
novelUrl = novelUrl[novelUrl.length - 2] + '/';
|
||||
let novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
novelUrl = novelUrl.split('/');
|
||||
novelUrl = novelUrl[novelUrl.length - 2] + '/';
|
||||
|
||||
const novel = {
|
||||
sourceId: 28,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
const novel = {
|
||||
sourceId: 28,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { getSourceStorage } from '@hooks/useSourceStorage';
|
||||
import { defaultUserAgentString, fetchApi } from '@utils/fetch/fetch';
|
||||
import { Cheerio, CheerioAPI, Element } from 'cheerio';
|
||||
|
||||
export const bypassImages = async (
|
||||
loadedCheerio: CheerioAPI,
|
||||
element: Cheerio<Element>,
|
||||
sourceId: number,
|
||||
): Promise<string> => {
|
||||
const { cookies = '' } = getSourceStorage(sourceId);
|
||||
let promises: Promise<number>[] = [];
|
||||
|
||||
element.find('noscript').each(function () {
|
||||
loadedCheerio(this).remove();
|
||||
});
|
||||
|
||||
element.find('img').each(function () {
|
||||
const attr: any = this.attributes.find(
|
||||
a => a.name.includes('src') && !a.value.includes(', '),
|
||||
);
|
||||
|
||||
if (!attr) {
|
||||
loadedCheerio(this).remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const promise: Promise<number> = new Promise(async r => {
|
||||
const response = await fetchApi({
|
||||
url: attr.value,
|
||||
init: {
|
||||
headers: { Cookie: cookies, 'User-Agent': defaultUserAgentString },
|
||||
},
|
||||
});
|
||||
|
||||
const blob = await response.blob();
|
||||
const encodedImage: string = await new Promise((onSuccess, onError) => {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
onSuccess(this.result as string);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
});
|
||||
|
||||
loadedCheerio(this).replaceWith(`<img src="${encodedImage}"/>`);
|
||||
r(0);
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return element.html()!!;
|
||||
};
|
||||
@@ -12,7 +12,7 @@ const popularNovels = async page => {
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: `mode=get_data_novel_list_p&novel_menu=3&np_day=${day}&np_rank=1&np_distributor=0&np_genre=00&np_order=1&np_genre_ex_1=00&np_genre_ex_2=00&list_limit=${list_limit}&is_query_first=true`,
|
||||
body: `mode=get_data_novel_list_p&novel_menu=3&np_day=${day}&np_rank=0&np_distributor=0&np_genre=00&np_order=1&np_genre_ex_1=00&np_genre_ex_2=00&list_limit=${list_limit}&is_query_first=true`,
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import RNFS from 'react-native-fs';
|
||||
import { htmlToText } from '../helpers/htmlToText';
|
||||
|
||||
const sourceName = 'EPub';
|
||||
const sourceId = 0;
|
||||
|
||||
const resolveRelativePath = (absPath: string, relPath: string): string => {
|
||||
const absPathParts = absPath.split('/');
|
||||
const relPathParts = relPath.split('/');
|
||||
|
||||
let backNavigations = 0;
|
||||
relPathParts.forEach(p => p === '..' && backNavigations++);
|
||||
|
||||
const newAbsPath = absPathParts
|
||||
.slice(0, absPathParts.length - backNavigations)
|
||||
.join('/');
|
||||
const newRelPath = relPathParts.slice(backNavigations).join('/');
|
||||
|
||||
return `${newAbsPath}/${newRelPath}`;
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (epubPath: string) => {
|
||||
const metadata = JSON.parse(await RNFS.readFile(`${epubPath}/metadata.json`));
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url: epubPath,
|
||||
novelUrl: epubPath,
|
||||
|
||||
novelName: metadata.title,
|
||||
novelCover: `file://${epubPath}/${metadata.cover}`,
|
||||
artist: metadata.artist,
|
||||
author: metadata.authors,
|
||||
summary: htmlToText(metadata.summary),
|
||||
status: 'Completed',
|
||||
|
||||
chapters: metadata.chapters.map((chapter: any) => ({
|
||||
chapterName: chapter.name,
|
||||
chapterUrl: `file://${epubPath}/${chapter.path}`,
|
||||
})),
|
||||
};
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (epubPath: string, chapterUrl: string) => {
|
||||
const metadata = JSON.parse(await RNFS.readFile(`${epubPath}/metadata.json`));
|
||||
const chapterInfo = metadata.chapters.find((f: any) =>
|
||||
chapterUrl.includes(f.path),
|
||||
);
|
||||
|
||||
const dirPath = chapterUrl.substring(0, chapterUrl.lastIndexOf('/'));
|
||||
let chapterText = await RNFS.readFile(chapterUrl);
|
||||
|
||||
const loadedCheerio = cheerio.load(chapterText);
|
||||
const promises: Promise<number>[] = [];
|
||||
|
||||
const imageReplace = function (this: cheerio.Element) {
|
||||
const src =
|
||||
this.attribs.src ??
|
||||
this.attributes.find(
|
||||
attr => attr.name.includes('src') || attr.name.includes('href'),
|
||||
)?.value;
|
||||
|
||||
if (src === undefined) {
|
||||
loadedCheerio(this).remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const promise: Promise<number> = new Promise(async r => {
|
||||
const url = resolveRelativePath(dirPath, src);
|
||||
loadedCheerio(this).replaceWith(`<img src="${url}"/>`);
|
||||
r(0);
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
};
|
||||
|
||||
// Specify images absolute path
|
||||
loadedCheerio('img').each(imageReplace);
|
||||
loadedCheerio('image').each(imageReplace);
|
||||
|
||||
// Add styles
|
||||
loadedCheerio('link').each(function () {
|
||||
if (this.attribs.href === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise: Promise<number> = new Promise(async r => {
|
||||
const path = resolveRelativePath(dirPath, this.attribs.href);
|
||||
const style = await RNFS.readFile(path);
|
||||
loadedCheerio(this).replaceWith(`<style>${style}</style>`);
|
||||
r(0);
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
chapterText = loadedCheerio.html().replace(/href=".+?"/g, '');
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl: epubPath,
|
||||
chapterUrl,
|
||||
chapterName: chapterInfo.name,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const EPubSource = {
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
};
|
||||
|
||||
export default EPubSource;
|
||||
@@ -22,7 +22,7 @@ export const NovelTranslateScraper = new MadaraScraper(
|
||||
42,
|
||||
'https://noveltranslate.com/',
|
||||
'NovelTranslate',
|
||||
{ 'path': { 'novels': 'all-novels', 'novel': 'manga', 'chapter': 'manga' } },
|
||||
{ 'path': { 'novels': 'all-novels', 'novel': 'novel', 'chapter': 'novel' } },
|
||||
);
|
||||
|
||||
export const LunarLettersScraper = new MadaraScraper(
|
||||
@@ -95,7 +95,15 @@ export const ArNovelScraper = new MadaraScraper(
|
||||
59,
|
||||
'https://arnovel.me/',
|
||||
'ArNovel',
|
||||
{ 'useNewChapterEndpoint': true, 'lang': 'Arabic' },
|
||||
{
|
||||
'path': {
|
||||
'novels': 'home/novel',
|
||||
'novel': 'home/novel',
|
||||
'chapter': 'home/novel',
|
||||
},
|
||||
'useNewChapterEndpoint': true,
|
||||
'lang': 'Arabic',
|
||||
},
|
||||
);
|
||||
|
||||
export const MeioNovelScraper = new MadaraScraper(
|
||||
@@ -170,7 +178,7 @@ export const OnlyMTLScraper = new MadaraScraper(
|
||||
|
||||
export const HizoMangaScraper = new MadaraScraper(
|
||||
96,
|
||||
'https://hizomanga.com/',
|
||||
'https://hizomanga.net/',
|
||||
'HizoManga',
|
||||
{
|
||||
'path': { 'novels': 'serie', 'novel': 'serie', 'chapter': 'serie' },
|
||||
|
||||
@@ -172,10 +172,15 @@ class MadaraScraper {
|
||||
|
||||
let chapterUrl = loadedCheerio(this).find('a').attr('href').split('/');
|
||||
|
||||
chapterUrl[6]
|
||||
? (chapterUrl = chapterUrl[5] + '/' + chapterUrl[6])
|
||||
: (chapterUrl = chapterUrl[5]);
|
||||
|
||||
if (novel.sourceId === 59) {
|
||||
chapterUrl[7]
|
||||
? (chapterUrl = chapterUrl[6] + '/' + chapterUrl[7])
|
||||
: (chapterUrl = chapterUrl[6]);
|
||||
} else {
|
||||
chapterUrl[6]
|
||||
? (chapterUrl = chapterUrl[5] + '/' + chapterUrl[6])
|
||||
: (chapterUrl = chapterUrl[5]);
|
||||
}
|
||||
novelChapters.push({ chapterName, releaseDate, chapterUrl });
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"baseUrl": "https://noveltranslate.com/",
|
||||
"sourceName": "NovelTranslate",
|
||||
"options": {
|
||||
"path": { "novels": "all-novels", "novel": "manga", "chapter": "manga" }
|
||||
"path": { "novels": "all-novels", "novel": "novel", "chapter": "novel" }
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -94,6 +94,11 @@
|
||||
"baseUrl": "https://arnovel.me/",
|
||||
"sourceName": "ArNovel",
|
||||
"options": {
|
||||
"path": {
|
||||
"novels": "home/novel",
|
||||
"novel": "home/novel",
|
||||
"chapter": "home/novel"
|
||||
},
|
||||
"useNewChapterEndpoint": true,
|
||||
"lang": "Arabic"
|
||||
}
|
||||
@@ -172,7 +177,7 @@
|
||||
},
|
||||
{
|
||||
"sourceId": 96,
|
||||
"baseUrl": "https://hizomanga.com/",
|
||||
"baseUrl": "https://hizomanga.net/",
|
||||
"sourceName": "HizoManga",
|
||||
"options": {
|
||||
"path": { "novels": "serie", "novel": "serie", "chapter": "serie" },
|
||||
|
||||
@@ -2,20 +2,200 @@ import MultiSrcScraper from './ReadwnScraper';
|
||||
|
||||
const ReadwnScraper = new MultiSrcScraper(
|
||||
68,
|
||||
'https://www.readwn.com/',
|
||||
'https://www.wuxiap.com/',
|
||||
'Readwn.com',
|
||||
{
|
||||
label: 'Genre / Category',
|
||||
values: [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Action', value: 'action' },
|
||||
{ label: 'Adventure', value: 'adventure' },
|
||||
{ label: 'Chinese', value: 'chinese' },
|
||||
{ label: 'Comedy', value: 'comedy' },
|
||||
{ label: 'Contemporary Romance', value: 'contemporary-romance' },
|
||||
{ label: 'Drama', value: 'drama' },
|
||||
{ label: 'Eastern Fantasy', value: 'eastern-fantasy' },
|
||||
{ label: 'Ecchi', value: 'ecchi' },
|
||||
{ label: 'Erciyuan', value: 'erciyuan' },
|
||||
{ label: 'Faloo', value: 'faloo' },
|
||||
{ label: 'Fan-Fiction', value: 'fan-fiction' },
|
||||
{ label: 'Fantasy', value: 'fantasy' },
|
||||
{ label: 'Fantasy Romance', value: 'fantasy-romance' },
|
||||
{ label: 'Game', value: 'game' },
|
||||
{ label: 'Gender Bender', value: 'gender-bender' },
|
||||
{ label: 'Harem', value: 'harem' },
|
||||
{ label: 'Hentai', value: 'hentai' },
|
||||
{ label: 'Historical', value: 'historical' },
|
||||
{ label: 'Horror', value: 'horror' },
|
||||
{ label: 'Isekai', value: 'isekai' },
|
||||
{ label: 'Japanese', value: 'japanese' },
|
||||
{ label: 'Josei', value: 'josei' },
|
||||
{ label: 'Korean', value: 'korean' },
|
||||
{ label: 'Lolicon', value: 'lolicon' },
|
||||
{ label: 'Magic', value: 'magic' },
|
||||
{ label: 'Magical Realism', value: 'magical-realism' },
|
||||
{ label: 'Martial Arts', value: 'martial-arts' },
|
||||
{ label: 'Mecha', value: 'mecha' },
|
||||
{ label: 'Military', value: 'military' },
|
||||
{ label: 'Mystery', value: 'mystery' },
|
||||
{ label: 'Official Circles', value: 'official_circles' },
|
||||
{ label: 'Psychological', value: 'psychological' },
|
||||
{ label: 'Romance', value: 'romance' },
|
||||
{ label: 'School Life', value: 'school-life' },
|
||||
{ label: 'Sci-fi', value: 'sci-fi' },
|
||||
{ label: 'Science Fiction', value: 'science_fiction' },
|
||||
{ label: 'Seinen', value: 'seinen' },
|
||||
{ label: 'Shoujo', value: 'shoujo' },
|
||||
{ label: 'Shoujo Ai', value: 'shoujo-ai' },
|
||||
{ label: 'Shounen', value: 'shounen' },
|
||||
{ label: 'Shounen Ai', value: 'shounen-ai' },
|
||||
{ label: 'Slice of Life', value: 'slice-of-life' },
|
||||
{ label: 'Sports', value: 'sports' },
|
||||
{ label: 'Supernatural', value: 'supernatural' },
|
||||
{ label: 'Suspense Thriller', value: 'suspense_thriller' },
|
||||
{ label: 'Tragedy', value: 'tragedy' },
|
||||
{ label: 'Travel Through Time', value: 'travel_through_time' },
|
||||
{ label: 'Two-dimensional', value: 'two-dimensional' },
|
||||
{ label: 'Urban', value: 'urban' },
|
||||
{ label: 'Urban Life', value: 'urban-life' },
|
||||
{ label: 'Video Games', value: 'video-games' },
|
||||
{ label: 'Virtual Reality', value: 'virtual-reality' },
|
||||
{ label: 'Wuxia', value: 'wuxia' },
|
||||
{ label: 'Wuxia Xianxia', value: 'wuxia_xianxia' },
|
||||
{ label: 'Xianxia', value: 'xianxia' },
|
||||
{ label: 'Xuanhuan', value: 'xuanhuan' },
|
||||
{ label: 'Yaoi', value: 'yaoi' },
|
||||
{ label: 'Yuri', value: 'yuri' },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const NovelmtScraper = new MultiSrcScraper(
|
||||
99,
|
||||
'https://www.novelmt.com/',
|
||||
'https://www.wuxiapub.com/',
|
||||
'Novelmt.com',
|
||||
{
|
||||
label: 'Genre / Category',
|
||||
values: [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Action', value: 'action' },
|
||||
{ label: 'Adult', value: 'adult' },
|
||||
{ label: 'Adventure', value: 'adventure' },
|
||||
{ label: 'Billionaire', value: 'billionaire' },
|
||||
{ label: 'CEO', value: 'ceo' },
|
||||
{ label: 'Chinese', value: 'chinese' },
|
||||
{ label: 'Comedy', value: 'comedy' },
|
||||
{ label: 'Contemporary Romance', value: 'contemporary-romance' },
|
||||
{ label: 'Drama', value: 'drama' },
|
||||
{ label: 'Eastern Fantasy', value: 'eastern-fantasy' },
|
||||
{ label: 'Ecchi', value: 'ecchi' },
|
||||
{ label: 'Erciyuan', value: 'erciyuan' },
|
||||
{ label: 'Faloo', value: 'faloo' },
|
||||
{ label: 'Fan-Fiction', value: 'fan-fiction' },
|
||||
{ label: 'Fantasy', value: 'fantasy' },
|
||||
{ label: 'Fantasy Romance', value: 'fantasy-romance' },
|
||||
{ label: 'Farming', value: 'farming' },
|
||||
{ label: 'Game', value: 'game' },
|
||||
{ label: 'Games', value: 'games' },
|
||||
{ label: 'Gay Romance', value: 'gay-romance' },
|
||||
{ label: 'Gender Bender', value: 'gender-bender' },
|
||||
{ label: 'Harem', value: 'harem' },
|
||||
{ label: 'Historical', value: 'historical' },
|
||||
{ label: 'Historical Romance', value: 'historical-romance' },
|
||||
{ label: 'Horror', value: 'horror' },
|
||||
{ label: 'Isekai', value: 'isekai' },
|
||||
{ label: 'Japanese', value: 'japanese' },
|
||||
{ label: 'Josei', value: 'josei' },
|
||||
{ label: 'Korean', value: 'korean' },
|
||||
{ label: 'Lolicon', value: 'lolicon' },
|
||||
{ label: 'Magic', value: 'magic' },
|
||||
{ label: 'Magical Realism', value: 'magical-realism' },
|
||||
{ label: 'Martial Arts', value: 'martial-arts' },
|
||||
{ label: 'Mature', value: 'mature' },
|
||||
{ label: 'Mecha', value: 'mecha' },
|
||||
{ label: 'Military', value: 'military' },
|
||||
{ label: 'Modern Life', value: 'modern-life' },
|
||||
{ label: 'Modern Romance', value: 'modern-romance' },
|
||||
{ label: 'Mystery', value: 'mystery' },
|
||||
{ label: 'Psychological', value: 'psychological' },
|
||||
{ label: 'Romance', value: 'romance' },
|
||||
{ label: 'Romantic', value: 'romantic' },
|
||||
{ label: 'School Life', value: 'school-life' },
|
||||
{ label: 'Sci-fi', value: 'sci-fi' },
|
||||
{ label: 'Seinen', value: 'seinen' },
|
||||
{ label: 'Shoujo', value: 'shoujo' },
|
||||
{ label: 'Shoujo Ai', value: 'shoujo-ai' },
|
||||
{ label: 'Shounen', value: 'shounen' },
|
||||
{ label: 'Shounen Ai', value: 'shounen-ai' },
|
||||
{ label: 'Slice of Life', value: 'slice-of-life' },
|
||||
{ label: 'Smut', value: 'smut' },
|
||||
{ label: 'Sports', value: 'sports' },
|
||||
{ label: 'Supernatural', value: 'supernatural' },
|
||||
{ label: 'Tragedy', value: 'tragedy' },
|
||||
{ label: 'Two-dimensional', value: 'two-dimensional' },
|
||||
{ label: 'Urban', value: 'urban' },
|
||||
{ label: 'Urban Life', value: 'urban-life' },
|
||||
{ label: 'Video Games', value: 'video-games' },
|
||||
{ label: 'Virtual Reality', value: 'virtual-reality' },
|
||||
{ label: 'Wuxia', value: 'wuxia' },
|
||||
{ label: 'Xianxia', value: 'xianxia' },
|
||||
{ label: 'Xuanhuan', value: 'xuanhuan' },
|
||||
{ label: 'Yaoi', value: 'yaoi' },
|
||||
{ label: 'Yuri', value: 'yuri' },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const LtnovelScraper = new MultiSrcScraper(
|
||||
100,
|
||||
'https://www.ltnovel.com/',
|
||||
'Ltnovel.com',
|
||||
{
|
||||
label: 'Genre / Category',
|
||||
values: [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Action', value: 'action' },
|
||||
{ label: 'Adult', value: 'adult' },
|
||||
{ label: 'Adventure', value: 'adventure' },
|
||||
{ label: 'Comedy', value: 'comedy' },
|
||||
{ label: 'Contemporary Romance', value: 'contemporary-romance' },
|
||||
{ label: 'Drama', value: 'drama' },
|
||||
{ label: 'Eastern Fantasy', value: 'eastern-fantasy' },
|
||||
{ label: 'Ecchi', value: 'ecchi' },
|
||||
{ label: 'Fantasy', value: 'fantasy' },
|
||||
{ label: 'Fantasy Romance', value: 'fantasy-romance' },
|
||||
{ label: 'Game', value: 'game' },
|
||||
{ label: 'Gender Bender', value: 'gender-bender' },
|
||||
{ label: 'Harem', value: 'harem' },
|
||||
{ label: 'Historical', value: 'historical' },
|
||||
{ label: 'Horror', value: 'horror' },
|
||||
{ label: 'Josei', value: 'josei' },
|
||||
{ label: 'Lolicon', value: 'lolicon' },
|
||||
{ label: 'Magical Realism', value: 'magical-realism' },
|
||||
{ label: 'Martial Arts', value: 'martial-arts' },
|
||||
{ label: 'Mature', value: 'mature' },
|
||||
{ label: 'Mecha', value: 'mecha' },
|
||||
{ label: 'Mystery', value: 'mystery' },
|
||||
{ label: 'Psychological', value: 'psychological' },
|
||||
{ label: 'Romance', value: 'romance' },
|
||||
{ label: 'School Life', value: 'school-life' },
|
||||
{ label: 'Sci-fi', value: 'sci-fi' },
|
||||
{ label: 'Seinen', value: 'seinen' },
|
||||
{ label: 'Shoujo', value: 'shoujo' },
|
||||
{ label: 'Shounen', value: 'shounen' },
|
||||
{ label: 'Shounen Ai', value: 'shounen-ai' },
|
||||
{ label: 'Slice of Life', value: 'slice-of-life' },
|
||||
{ label: 'Smut', value: 'smut' },
|
||||
{ label: 'Sports', value: 'sports' },
|
||||
{ label: 'Supernatural', value: 'supernatural' },
|
||||
{ label: 'Tragedy', value: 'tragedy' },
|
||||
{ label: 'Video Games', value: 'video-games' },
|
||||
{ label: 'Wuxia', value: 'wuxia' },
|
||||
{ label: 'Xianxia', value: 'xianxia' },
|
||||
{ label: 'Xuanhuan', value: 'xuanhuan' },
|
||||
{ label: 'Yaoi', value: 'yaoi' },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
export { ReadwnScraper, NovelmtScraper, LtnovelScraper };
|
||||
|
||||
@@ -1,23 +1,59 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import QueryString from 'qs';
|
||||
|
||||
import { fetchHtml } from '@utils/fetch/fetch';
|
||||
|
||||
import { FilterInputs } from '../../types/filterTypes';
|
||||
|
||||
class ReadwnScraper {
|
||||
constructor(sourceId, baseUrl, sourceName) {
|
||||
constructor(sourceId, baseUrl, sourceName, genres) {
|
||||
this.sourceId = sourceId;
|
||||
this.baseUrl = baseUrl;
|
||||
this.sourceName = sourceName;
|
||||
this.filters = [
|
||||
{
|
||||
key: 'sort',
|
||||
label: 'Sort By',
|
||||
values: [
|
||||
{ label: 'New', value: 'newstime' },
|
||||
{ label: 'Popular', value: 'onclick' },
|
||||
{ label: 'Updates', value: 'lastdotime' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
values: [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Completed', value: 'Completed' },
|
||||
{ label: 'Ongoing', value: 'Ongoing' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'genres',
|
||||
label: genres.label,
|
||||
values: genres.values,
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async popularNovels(page) {
|
||||
async popularNovels(page, { showLatestNovels, filters }) {
|
||||
const baseUrl = this.baseUrl;
|
||||
const sourceId = this.sourceId;
|
||||
|
||||
const pageNo = page - 1;
|
||||
|
||||
const url = `${baseUrl}list/all/all-onclick-${pageNo}.html`;
|
||||
let url = baseUrl + 'list/';
|
||||
url += (filters?.genres || 'all') + '/';
|
||||
url += (filters?.status || 'all') + '-';
|
||||
url +=
|
||||
(showLatestNovels ? 'lastdotime' : filters?.sort || 'newstime') + '-';
|
||||
url += pageNo + '.html';
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -48,8 +84,7 @@ class ReadwnScraper {
|
||||
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -139,8 +174,7 @@ class ReadwnScraper {
|
||||
const url = baseUrl + chapterUrl;
|
||||
const sourceId = this.sourceId;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const body = await fetchHtml({ url, sourceId });
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
@@ -163,23 +197,25 @@ class ReadwnScraper {
|
||||
const sourceId = this.sourceId;
|
||||
const searchUrl = `${baseUrl}e/search/index.php`;
|
||||
|
||||
const result = await fetch(searchUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Referer: `${baseUrl}search.html`,
|
||||
Origin: baseUrl,
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
const body = await fetchHtml({
|
||||
url: searchUrl,
|
||||
init: {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Referer: `${baseUrl}search.html`,
|
||||
Origin: baseUrl,
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
},
|
||||
method: 'POST',
|
||||
body: QueryString.stringify({
|
||||
show: 'title',
|
||||
tempid: 1,
|
||||
tbname: 'news',
|
||||
keyboard: searchTerm,
|
||||
}),
|
||||
},
|
||||
method: 'POST',
|
||||
body: QueryString.stringify({
|
||||
show: 'title',
|
||||
tempid: 1,
|
||||
tbname: 'news',
|
||||
keyboard: searchTerm,
|
||||
}),
|
||||
});
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Арт', value: '1' },
|
||||
{ label: 'Боевик', value: '2' },
|
||||
{ label: 'Боевые искусства', value: '3' },
|
||||
{ label: 'Гарем', value: '5' },
|
||||
{ label: 'Гаремник', value: '5' },
|
||||
{ label: 'Героическое фэнтези', value: '7' },
|
||||
{ label: 'Детектив', value: '8' },
|
||||
{ label: 'Дзёсэй', value: '9' },
|
||||
@@ -65,8 +65,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
values: [
|
||||
{ label: '12+', value: '3316' },
|
||||
{ label: '16+', value: '83' },
|
||||
{ label: '18+', value: '1749' },
|
||||
{ label: '21+', value: '3191' },
|
||||
{ label: 'Абсурд', value: '7973' },
|
||||
{ label: 'Авантюристы', value: '222' },
|
||||
{ label: 'Автомат', value: '5272' },
|
||||
@@ -108,7 +106,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Америка', value: '4082' },
|
||||
{ label: 'Амнезия', value: '2207' },
|
||||
{ label: 'Аморальный герой', value: '18' },
|
||||
{ label: 'Анальный секс', value: '1942' },
|
||||
{ label: 'Анбу', value: '8129' },
|
||||
{ label: 'Ангелы', value: '857' },
|
||||
{ label: 'Ангелы и демоны', value: '1327' },
|
||||
@@ -139,19 +136,17 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Басейн', value: '6847' },
|
||||
{ label: 'Баскетбол', value: '7933' },
|
||||
{ label: 'Бастарды', value: '76' },
|
||||
{ label: 'Бдсм', value: '444' },
|
||||
{ label: 'Бедность', value: '1207' },
|
||||
{ label: 'Без культивации', value: '668' },
|
||||
{ label: 'Без любовной линий', value: '2225' },
|
||||
{ label: 'Без перерождений', value: '262' },
|
||||
{ label: 'Без попаданца', value: '8065' },
|
||||
{ label: 'Без системы', value: '747' },
|
||||
{ label: 'Без цензуры', value: '7525' },
|
||||
{ label: 'Без юмора', value: '763' },
|
||||
{ label: 'Бездна', value: '1026' },
|
||||
{ label: 'Безжалостное домашнее животное', value: '1245' },
|
||||
{ label: 'Безжалостные персонажи', value: '2986' },
|
||||
{ label: 'Беззаботные персонажи ', value: '3123' },
|
||||
{ label: 'Беззаботные персонажи', value: '3123' },
|
||||
{ label: 'Безответная любовь', value: '7815' },
|
||||
{ label: 'Безработный', value: '2202' },
|
||||
{ label: 'Безумие', value: '37' },
|
||||
@@ -186,10 +181,7 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Богоубийца', value: '1253' },
|
||||
{ label: 'Богохульство', value: '95' },
|
||||
{ label: 'Боевая академия', value: '2108' },
|
||||
{ label: 'Боевик', value: '529' },
|
||||
{ label: 'Боевые искусства', value: '2090' },
|
||||
{ label: 'Большая грудь', value: '4717' },
|
||||
{ label: 'Большой член', value: '5299' },
|
||||
{ label: 'Борос', value: '6533' },
|
||||
{ label: 'Борьба', value: '1925' },
|
||||
{ label: 'Борьба за власть', value: '8072' },
|
||||
@@ -209,7 +201,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Бэтмен', value: '4668' },
|
||||
{ label: 'В первый раз', value: '6212' },
|
||||
{ label: 'В этот же мир', value: '49' },
|
||||
{ label: 'Вагинальный секс', value: '4742' },
|
||||
{ label: 'Ваканда', value: '8331' },
|
||||
{ label: 'Валькирии', value: '4047' },
|
||||
{ label: 'Вампиры', value: '2206' },
|
||||
@@ -278,7 +269,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Гений', value: '1650' },
|
||||
{ label: 'Гермиона грейнджер', value: '4280' },
|
||||
{ label: 'Герои', value: '185' },
|
||||
{ label: 'Героическое фэнтези', value: '1148' },
|
||||
{ label: 'Гигаполис', value: '8393' },
|
||||
{ label: 'Гильгамеш', value: '6031' },
|
||||
{ label: 'Гильдии', value: '900' },
|
||||
@@ -312,7 +302,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Главный герой сильный с самого начала', value: '88' },
|
||||
{ label: 'Главный герой скрывает свою силу', value: '1059' },
|
||||
{ label: 'Главный герой эгоист', value: '8326' },
|
||||
{ label: 'Глотание спермы', value: '4491' },
|
||||
{ label: 'Гномы', value: '1870' },
|
||||
{ label: 'Гоблины', value: '2029' },
|
||||
{ label: 'Годзилла', value: '6990' },
|
||||
@@ -321,7 +310,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Головоломка', value: '1697' },
|
||||
{ label: 'Горничные', value: '8091' },
|
||||
{ label: 'Гробница', value: '388' },
|
||||
{ label: 'Групповой секс', value: '1314' },
|
||||
{ label: 'Дазай осаму', value: '4112' },
|
||||
{ label: 'Даньмэй', value: '3411' },
|
||||
{ label: 'Даосизм', value: '258' },
|
||||
@@ -337,7 +325,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Демон лорд', value: '153' },
|
||||
{ label: 'Демонология', value: '2024' },
|
||||
{ label: 'Демоны', value: '1406' },
|
||||
{ label: 'Детектив', value: '1265' },
|
||||
{ label: 'Дети', value: '580' },
|
||||
{ label: 'Джарвис', value: '933' },
|
||||
{ label: 'Джедай', value: '4442' },
|
||||
@@ -365,12 +352,10 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Драко малфой', value: '46' },
|
||||
{ label: 'Драконы', value: '282' },
|
||||
{ label: 'Драконы оборотни', value: '8082' },
|
||||
{ label: 'Драма', value: '611' },
|
||||
{ label: 'Древние времена', value: '7781' },
|
||||
{ label: 'Древний египет', value: '7614' },
|
||||
{ label: 'Древний китай', value: '473' },
|
||||
{ label: 'Древний мир', value: '4' },
|
||||
{ label: 'Дрочка', value: '4565' },
|
||||
{ label: 'Другая вселенная', value: '39' },
|
||||
{ label: 'Другие планеты', value: '2371' },
|
||||
{ label: 'Другой мир', value: '2248' },
|
||||
@@ -393,10 +378,8 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Женское доминирование', value: '4622' },
|
||||
{ label: 'Женщина в теле мужчины', value: '2748' },
|
||||
{ label: 'Женщина кошка', value: '7513' },
|
||||
{ label: 'Жесткий секс', value: '2238' },
|
||||
{ label: 'Жестокие персонажи ', value: '3153' },
|
||||
{ label: 'Жестокие персонажи', value: '3153' },
|
||||
{ label: 'Жестокий мир', value: '1532' },
|
||||
{ label: 'Жестокое обращение с детьми', value: '84' },
|
||||
{ label: 'Жестокость', value: '7880' },
|
||||
{ label: 'Животные', value: '7488' },
|
||||
{ label: 'Животные компаньоны', value: '2061' },
|
||||
@@ -426,13 +409,11 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Змей', value: '5721' },
|
||||
{ label: 'Знаменитости', value: '1341' },
|
||||
{ label: 'Знание медицины', value: '1411' },
|
||||
{ label: 'Золотой дождь', value: '1828' },
|
||||
{ label: 'Золушка', value: '2980' },
|
||||
{ label: 'Зомби', value: '1743' },
|
||||
{ label: 'Зомби апокалипсис', value: '912' },
|
||||
{ label: 'Зона 51', value: '3051' },
|
||||
{ label: 'Зооморфы', value: '375' },
|
||||
{ label: 'Зоофилия', value: '1165' },
|
||||
{ label: 'Зорро', value: '6736' },
|
||||
{ label: 'Зрелые женщины', value: '1458' },
|
||||
{ label: 'Игра', value: '663' },
|
||||
@@ -444,7 +425,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Изменение характера', value: '1113' },
|
||||
{ label: 'Изменения внешности', value: '259' },
|
||||
{ label: 'Изменения личности', value: '1080' },
|
||||
{ label: 'Изнасилование', value: '1283' },
|
||||
{ label: 'Изобретения', value: '126' },
|
||||
{ label: 'Изуку мидория', value: '6967' },
|
||||
{ label: 'Император', value: '867' },
|
||||
@@ -461,7 +441,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Интимные сцены', value: '3450' },
|
||||
{ label: 'Интрига', value: '317' },
|
||||
{ label: 'Интриги и заговоры', value: '1009' },
|
||||
{ label: 'Инцест', value: '1934' },
|
||||
{ label: 'Иные миры', value: '35' },
|
||||
{ label: 'Исекай', value: '2979' },
|
||||
{ label: 'Искусственный интеллект', value: '1900' },
|
||||
@@ -480,7 +459,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Кафе', value: '3934' },
|
||||
{ label: 'Кацуки бакуго', value: '7696' },
|
||||
{ label: 'Квесты', value: '1707' },
|
||||
{ label: 'Киберпанк', value: '1614' },
|
||||
{ label: 'Киберспорт', value: '1984' },
|
||||
{ label: 'Кино', value: '7126' },
|
||||
{ label: 'Китай', value: '3762' },
|
||||
@@ -490,17 +468,14 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Книги', value: '1068' },
|
||||
{ label: 'Книжный червь', value: '521' },
|
||||
{ label: 'Колдовство', value: '2681' },
|
||||
{ label: 'Колледж', value: '5058' },
|
||||
{ label: 'колледж', value: '5058' },
|
||||
{ label: 'Коллекционер', value: '2588' },
|
||||
{ label: 'Кольцо', value: '1628' },
|
||||
{ label: 'Комедия', value: '1430' },
|
||||
{ label: 'Коммунисты', value: '1042' },
|
||||
{ label: 'Коноха', value: '2827' },
|
||||
{ label: 'Контракт', value: '4304' },
|
||||
{ label: 'Контроль разума', value: '485' },
|
||||
{ label: 'Конфеты', value: '341' },
|
||||
{ label: 'Кончил внутрь', value: '4589' },
|
||||
{ label: 'Копрофилия', value: '2568' },
|
||||
{ label: 'Корейцы', value: '6535' },
|
||||
{ label: 'Корея', value: '2443' },
|
||||
{ label: 'Королевская власть', value: '55' },
|
||||
@@ -515,19 +490,16 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Кража навыков', value: '1159' },
|
||||
{ label: 'Красивая главная героиня', value: '186' },
|
||||
{ label: 'Красивые женщины', value: '66' },
|
||||
{ label: 'Красивые персонажи ', value: '5787' },
|
||||
{ label: 'Красивые персонажи', value: '5787' },
|
||||
{ label: 'Красивый главный герой', value: '895' },
|
||||
{ label: 'Крафт', value: '1599' },
|
||||
{ label: 'Кровь и расчлененка', value: '7752' },
|
||||
{ label: 'Кроссовер', value: '75' },
|
||||
{ label: 'Ксенофилия', value: '2265' },
|
||||
{ label: 'Ктулху', value: '920' },
|
||||
{ label: 'Кузнец', value: '853' },
|
||||
{ label: 'Кукла', value: '3085' },
|
||||
{ label: 'Кулинария', value: '2230' },
|
||||
{ label: 'Культивация', value: '1901' },
|
||||
{ label: 'Культивирование', value: '1860' },
|
||||
{ label: 'Кунилингус', value: '4651' },
|
||||
{ label: 'Курама', value: '5709' },
|
||||
{ label: 'Курилин', value: '6134' },
|
||||
{ label: 'Кушина узумаки', value: '5222' },
|
||||
@@ -538,7 +510,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Леон золдик', value: '7469' },
|
||||
{ label: 'Лес', value: '6645' },
|
||||
{ label: 'Лечебная магия', value: '562' },
|
||||
{ label: 'Литрпг', value: '505' },
|
||||
{ label: 'Локи', value: '1725' },
|
||||
{ label: 'Лоли', value: '1576' },
|
||||
{ label: 'Лопата', value: '4989' },
|
||||
@@ -587,7 +558,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Марс', value: '4013' },
|
||||
{ label: 'Марти стью', value: '594' },
|
||||
{ label: 'Мастер на все руки', value: '3189' },
|
||||
{ label: 'Мастурбация', value: '3078' },
|
||||
{ label: 'Матриархат', value: '7197' },
|
||||
{ label: 'Мать и сестры', value: '7946' },
|
||||
{ label: 'Мафия', value: '353' },
|
||||
@@ -596,13 +566,11 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Медицина', value: '441' },
|
||||
{ label: 'Медленная романтика', value: '8383' },
|
||||
{ label: 'Медленное развитие истории', value: '7462' },
|
||||
{ label: 'Межрасовый секс', value: '3489' },
|
||||
{ label: 'Мелиодас', value: '7224' },
|
||||
{ label: 'Мелодрама', value: '1507' },
|
||||
{ label: 'Менеджмент', value: '368' },
|
||||
{ label: 'Мерлин', value: '3977' },
|
||||
{ label: 'Месть', value: '1218' },
|
||||
{ label: 'Меха', value: '2303' },
|
||||
{ label: 'Меч', value: '48' },
|
||||
{ label: 'Мечники', value: '969' },
|
||||
{ label: 'Мечта', value: '527' },
|
||||
@@ -614,12 +582,10 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Мимик', value: '5706' },
|
||||
{ label: 'Мина ашидо', value: '7695' },
|
||||
{ label: 'Минато намикадзе', value: '5223' },
|
||||
{ label: 'Минет', value: '3289' },
|
||||
{ label: 'Мир будущего', value: '1770' },
|
||||
{ label: 'Мир земли', value: '8394' },
|
||||
{ label: 'Мир меча и магии', value: '1271' },
|
||||
{ label: 'Мистер пропер', value: '565' },
|
||||
{ label: 'Мистика', value: '510' },
|
||||
{ label: 'Мифические существа', value: '1878' },
|
||||
{ label: 'Мифы и легенды', value: '4463' },
|
||||
{ label: 'Младшая сестра', value: '1523' },
|
||||
@@ -638,7 +604,7 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Мортал комбат', value: '5861' },
|
||||
{ label: 'Мошенник', value: '959' },
|
||||
{ label: 'Мрачный мир', value: '1' },
|
||||
{ label: 'Мужчина протагонист', value: '31' },
|
||||
{ label: 'Мужчина в женском теле', value: '8405' },
|
||||
{ label: 'Музыка', value: '1183' },
|
||||
{ label: 'Мультивселенная', value: '7410' },
|
||||
{ label: 'Мурим', value: '7342' },
|
||||
@@ -653,12 +619,10 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Наивный главный герой', value: '159' },
|
||||
{ label: 'Наложница', value: '4867' },
|
||||
{ label: 'Наноботы', value: '2332' },
|
||||
{ label: 'Наркотики', value: '1436' },
|
||||
{ label: 'Наруто', value: '7177' },
|
||||
{ label: 'Наруто и хината', value: '5766' },
|
||||
{ label: 'Насилие и жестокость', value: '1440' },
|
||||
{ label: 'Наука', value: '2150' },
|
||||
{ label: 'Научная фантастика', value: '2244' },
|
||||
{ label: 'Национализм', value: '5556' },
|
||||
{ label: 'Нацу', value: '5826' },
|
||||
{ label: 'Нацуки субару', value: '6656' },
|
||||
@@ -674,7 +638,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Некромаг', value: '7441' },
|
||||
{ label: 'Некромант', value: '1914' },
|
||||
{ label: 'Некромантия', value: '754' },
|
||||
{ label: 'Некрофилия', value: '1285' },
|
||||
{ label: 'Ненормативная лексика', value: '160' },
|
||||
{ label: 'Необычные герои', value: '67' },
|
||||
{ label: 'Нет людей', value: '7486' },
|
||||
@@ -687,7 +650,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Ниндзюцу', value: '2354' },
|
||||
{ label: 'Ниндзя', value: '1330' },
|
||||
{ label: 'Новелла', value: '2105' },
|
||||
{ label: 'Нудизм', value: '2233' },
|
||||
{ label: 'Нэко', value: '147' },
|
||||
{ label: 'Оби ван кеноби', value: '6830' },
|
||||
{ label: 'Обито учиха', value: '2814' },
|
||||
@@ -699,8 +661,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Один в поле воин', value: '6498' },
|
||||
{ label: 'Онлайн игра', value: '1565' },
|
||||
{ label: 'Ооками теппей', value: '849' },
|
||||
{ label: 'Оральный секс', value: '605' },
|
||||
{ label: 'Оргия', value: '974' },
|
||||
{ label: 'Оригинальные персонажи', value: '7741' },
|
||||
{ label: 'Орки', value: '1597' },
|
||||
{ label: 'Оружие', value: '125' },
|
||||
@@ -719,7 +679,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Ояш', value: '8048' },
|
||||
{ label: 'Падчерица', value: '7023' },
|
||||
{ label: 'Падший ангел', value: '184' },
|
||||
{ label: 'Пайзури', value: '3560' },
|
||||
{ label: 'Палач', value: '852' },
|
||||
{ label: 'Палочка', value: '2682' },
|
||||
{ label: 'Пандорум', value: '4360' },
|
||||
@@ -768,7 +727,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Повествование от первого лица', value: '1054' },
|
||||
{ label: 'Повествование от разных лиц', value: '600' },
|
||||
{ label: 'Повествование от третьего лица', value: '2116' },
|
||||
{ label: 'Повседневность', value: '802' },
|
||||
{ label: 'Подавитель', value: '497' },
|
||||
{ label: 'Подземелье', value: '798' },
|
||||
{ label: 'Подростки', value: '636' },
|
||||
@@ -788,9 +746,7 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Попаданец в другой мир', value: '685' },
|
||||
{ label: 'Попаданец в игру', value: '6466' },
|
||||
{ label: 'Попадание в книгу', value: '6957' },
|
||||
{ label: 'Порно', value: '4023' },
|
||||
{ label: 'Постапокалипсис', value: '1079' },
|
||||
{ label: 'Потеря девственности', value: '5879' },
|
||||
{ label: 'Похищение', value: '7452' },
|
||||
{ label: 'Поэзия', value: '1120' },
|
||||
{ label: 'Правитель', value: '2261' },
|
||||
@@ -804,7 +760,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Призыв', value: '332' },
|
||||
{ label: 'Призыв в другой мир', value: '781' },
|
||||
{ label: 'Призыв существ', value: '412' },
|
||||
{ label: 'Приключения', value: '964' },
|
||||
{ label: 'Приложение на телефоне', value: '5345' },
|
||||
{ label: 'Принудительный брак', value: '2954' },
|
||||
{ label: 'Принуждение', value: '1442' },
|
||||
@@ -818,14 +773,13 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Проклятия', value: '1347' },
|
||||
{ label: 'Проработанный мир', value: '1711' },
|
||||
{ label: 'Пророчество', value: '7762' },
|
||||
{ label: 'Против наркотиков', value: '8406' },
|
||||
{ label: 'Псайкеры', value: '3659' },
|
||||
{ label: 'Псионика', value: '5216' },
|
||||
{ label: 'Психбольница', value: '6969' },
|
||||
{ label: 'Психические расстройства', value: '2445' },
|
||||
{ label: 'Психологические травмы', value: '3418' },
|
||||
{ label: 'Психология', value: '17' },
|
||||
{ label: 'Публичный дом', value: '7860' },
|
||||
{ label: 'Публичный секс', value: '6647' },
|
||||
{ label: 'Путешествие в другой мир', value: '10' },
|
||||
{ label: 'Пух', value: '4286' },
|
||||
{ label: 'Пушечное мясо', value: '7234' },
|
||||
@@ -839,7 +793,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Развитие персонажа', value: '1780' },
|
||||
{ label: 'Развитие поселения', value: '7203' },
|
||||
{ label: 'Развитие технологий', value: '1875' },
|
||||
{ label: 'Разврат', value: '1924' },
|
||||
{ label: 'Раздвоение личности', value: '2062' },
|
||||
{ label: 'Разные расы', value: '1188' },
|
||||
{ label: 'Разорванная помолвка', value: '2057' },
|
||||
@@ -848,7 +801,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Ранобэ', value: '1764' },
|
||||
{ label: 'Расизм', value: '3554' },
|
||||
{ label: 'Растение', value: '407' },
|
||||
{ label: 'Расчленение', value: '7125' },
|
||||
{ label: 'Реализм', value: '1180' },
|
||||
{ label: 'Ребенок', value: '2881' },
|
||||
{ label: 'Ревность', value: '293' },
|
||||
@@ -871,7 +823,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Роботы', value: '467' },
|
||||
{ label: 'Рок ли', value: '7002' },
|
||||
{ label: 'Рокси', value: '7380' },
|
||||
{ label: 'Романтика', value: '1025' },
|
||||
{ label: 'Рон уизли', value: '4279' },
|
||||
{ label: 'Ророноа зоро', value: '1590' },
|
||||
{ label: 'Росомаха', value: '1035' },
|
||||
@@ -887,7 +838,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Сайян', value: '6802' },
|
||||
{ label: 'Сакура', value: '2515' },
|
||||
{ label: 'Самец', value: '1592' },
|
||||
{ label: 'Самоубийство', value: '4312' },
|
||||
{ label: 'Самураи', value: '576' },
|
||||
{ label: 'Санса старк', value: '3925' },
|
||||
{ label: 'Сарказм', value: '4658' },
|
||||
@@ -896,31 +846,16 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Сборник', value: '7367' },
|
||||
{ label: 'Свадьба', value: '1524' },
|
||||
{ label: 'Сверхсила', value: '1055' },
|
||||
{ label: 'Сверхъестественное', value: '1074' },
|
||||
{ label: 'Свингеры', value: '514' },
|
||||
{ label: 'Свинцовые книги', value: '5322' },
|
||||
{ label: 'Связывание', value: '4506' },
|
||||
{ label: 'Священик', value: '61' },
|
||||
{ label: 'Северус снейп', value: '5783' },
|
||||
{ label: 'Сёдзе', value: '2219' },
|
||||
{ label: 'Секс', value: '2472' },
|
||||
{ label: 'Секс без проникновения', value: '151' },
|
||||
{ label: 'Секс игрушки', value: '1124' },
|
||||
{ label: 'Секс по принуждению', value: '8335' },
|
||||
{ label: 'Секс по согласию', value: '7307' },
|
||||
{ label: 'Секс рабыня', value: '7174' },
|
||||
{ label: 'Секс с близнецами', value: '7457' },
|
||||
{ label: 'Секс с монстрами', value: '6073' },
|
||||
{ label: 'Секс с учителем', value: '2570' },
|
||||
{ label: 'Секса будет много', value: '7265' },
|
||||
{ label: 'Сексуальные и психологические отклонения', value: '888' },
|
||||
{ label: 'Секты', value: '1234' },
|
||||
{ label: 'Селфцест', value: '7498' },
|
||||
{ label: 'Сельское хозяйство', value: '787' },
|
||||
{ label: 'Семейный конфликт', value: '935' },
|
||||
{ label: 'Семья', value: '728' },
|
||||
{ label: 'Сенджу', value: '6069' },
|
||||
{ label: 'Сёнэн', value: '2058' },
|
||||
{ label: 'Серийный убийца', value: '261' },
|
||||
{ label: 'Сестра', value: '2342' },
|
||||
{ label: 'Сила глаза', value: '1019' },
|
||||
@@ -933,13 +868,11 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Сирота', value: '297' },
|
||||
{ label: 'Система', value: '251' },
|
||||
{ label: 'Система уровней', value: '2174' },
|
||||
{ label: 'Сиськи', value: '16' },
|
||||
{ label: 'Ситх', value: '1618' },
|
||||
{ label: 'Сказка', value: '3570' },
|
||||
{ label: 'Скайрим', value: '4032' },
|
||||
{ label: 'Скайуокер', value: '6828' },
|
||||
{ label: 'Сквиб', value: '4101' },
|
||||
{ label: 'Сквирт', value: '4526' },
|
||||
{ label: 'Скелет', value: '1315' },
|
||||
{ label: 'Скрытые способности', value: '3417' },
|
||||
{ label: 'Скульптор', value: '1178' },
|
||||
@@ -974,7 +907,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Спокойная главная героиня', value: '236' },
|
||||
{ label: 'Спокойный главный герой', value: '1656' },
|
||||
{ label: 'Спор', value: '3484' },
|
||||
{ label: 'Спорт', value: '1387' },
|
||||
{ label: 'Спортивное тело', value: '8317' },
|
||||
{ label: 'Способность кражи', value: '7784' },
|
||||
{ label: 'Сражения', value: '23' },
|
||||
@@ -988,7 +920,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Стимпанк', value: '1894' },
|
||||
{ label: 'Стихи', value: '1309' },
|
||||
{ label: 'Стратегия', value: '569' },
|
||||
{ label: 'Стриптиз', value: '6914' },
|
||||
{ label: 'Строительство', value: '921' },
|
||||
{ label: 'Студент', value: '1464' },
|
||||
{ label: 'Студентка', value: '7518' },
|
||||
@@ -1005,11 +936,9 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Сценарий', value: '7489' },
|
||||
{ label: 'Счастливый конец', value: '3293' },
|
||||
{ label: 'Сын', value: '3297' },
|
||||
{ label: 'Сэйнэн', value: '345' },
|
||||
{ label: 'Сэм винчестер', value: '7461' },
|
||||
{ label: 'Сюаньхуа', value: '2560' },
|
||||
{ label: 'Сюжетные повороты', value: '7202' },
|
||||
{ label: 'Сянься', value: '1126' },
|
||||
{ label: 'Таблетки для развития', value: '589' },
|
||||
{ label: 'Таинственное прошлое', value: '769' },
|
||||
{ label: 'Тайная личность', value: '8096' },
|
||||
@@ -1018,7 +947,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Тайные отношения', value: '7450' },
|
||||
{ label: 'Тайцы', value: '6413' },
|
||||
{ label: 'Танки', value: '1787' },
|
||||
{ label: 'Твинцест', value: '2450' },
|
||||
{ label: 'Творчество', value: '20' },
|
||||
{ label: 'Тейлор эберт', value: '3526' },
|
||||
{ label: 'Телекинез', value: '2203' },
|
||||
@@ -1029,7 +957,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Темное фэнтези', value: '1675' },
|
||||
{ label: 'Темные эльфы', value: '7198' },
|
||||
{ label: 'Тенсейган', value: '4367' },
|
||||
{ label: 'Тентакли', value: '3236' },
|
||||
{ label: 'Тетя', value: '4194' },
|
||||
{ label: 'Техномагия', value: '508' },
|
||||
{ label: 'Тиран', value: '1174' },
|
||||
@@ -1042,14 +969,12 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Тор и локи', value: '2645' },
|
||||
{ label: 'Торговля', value: '1637' },
|
||||
{ label: 'Травничество', value: '221' },
|
||||
{ label: 'Трагедия', value: '470' },
|
||||
{ label: 'Трагическое прошлое', value: '644' },
|
||||
{ label: 'Трактир', value: '1918' },
|
||||
{ label: 'Трансмиграция', value: '1071' },
|
||||
{ label: 'Трансформация', value: '156' },
|
||||
{ label: 'Трансформеры', value: '4616' },
|
||||
{ label: 'Тревор филипс', value: '5427' },
|
||||
{ label: 'Триллер', value: '1676' },
|
||||
{ label: 'Тройняшки', value: '7169' },
|
||||
{ label: 'Троли', value: '4250' },
|
||||
{ label: 'Трудолюбивый главный герой', value: '248' },
|
||||
@@ -1062,7 +987,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Убийцы драконов', value: '1484' },
|
||||
{ label: 'Увечья', value: '3266' },
|
||||
{ label: 'Удача', value: '6575' },
|
||||
{ label: 'Ужасы', value: '1747' },
|
||||
{ label: 'Узумаки', value: '7579' },
|
||||
{ label: 'Укротитель', value: '367' },
|
||||
{ label: 'Умения', value: '531' },
|
||||
@@ -1078,7 +1002,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Усопп', value: '6737' },
|
||||
{ label: 'Устроенный брак', value: '2223' },
|
||||
{ label: 'Усыновление', value: '395' },
|
||||
{ label: 'Уся', value: '1172' },
|
||||
{ label: 'Утопия', value: '4677' },
|
||||
{ label: 'Уход за детьми', value: '7609' },
|
||||
{ label: 'Учеба в университете', value: '2797' },
|
||||
@@ -1089,9 +1012,7 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Фамильяры', value: '1471' },
|
||||
{ label: 'Фанатичная любовь', value: '6721' },
|
||||
{ label: 'Фанаты', value: '7739' },
|
||||
{ label: 'Фантастика', value: '286' },
|
||||
{ label: 'Фантастический мир', value: '2414' },
|
||||
{ label: 'Фанфик', value: '767' },
|
||||
{ label: 'Фармацевт', value: '1706' },
|
||||
{ label: 'Фашисты', value: '3118' },
|
||||
{ label: 'Феи', value: '7185' },
|
||||
@@ -1101,11 +1022,9 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Фенрир', value: '2740' },
|
||||
{ label: 'Феодализм', value: '280' },
|
||||
{ label: 'Ферма', value: '285' },
|
||||
{ label: 'Фетиш', value: '2398' },
|
||||
{ label: 'Фехтовальщик', value: '4685' },
|
||||
{ label: 'Филиппины', value: '128' },
|
||||
{ label: 'Философия', value: '1859' },
|
||||
{ label: 'Фистинг', value: '4206' },
|
||||
{ label: 'Флафф', value: '5048' },
|
||||
{ label: 'Флер делакур', value: '5191' },
|
||||
{ label: 'Флэш', value: '4667' },
|
||||
@@ -1114,7 +1033,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Фугаку', value: '6161' },
|
||||
{ label: 'Фурри', value: '6631' },
|
||||
{ label: 'Футбол', value: '1121' },
|
||||
{ label: 'Фэнтези', value: '174' },
|
||||
{ label: 'Фэнтезийный мир', value: '1556' },
|
||||
{ label: 'Хакер', value: '475' },
|
||||
{ label: 'Халк', value: '1196' },
|
||||
@@ -1123,7 +1041,6 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Хаос', value: '3117' },
|
||||
{ label: 'Харизматичный главный герой', value: '2350' },
|
||||
{ label: 'Хатаке какаши', value: '2820' },
|
||||
{ label: 'Хентай', value: '1682' },
|
||||
{ label: 'Хиддлстон', value: '5903' },
|
||||
{ label: 'Химавари', value: '2583' },
|
||||
{ label: 'Хината', value: '2513' },
|
||||
@@ -1161,21 +1078,15 @@ const RulateScraper = new MultiSrcScraper(
|
||||
{ label: 'Эволюция', value: '2040' },
|
||||
{ label: 'Экзорцизм', value: '676' },
|
||||
{ label: 'Экономика', value: '121' },
|
||||
{ label: 'Эксгибиционизм', value: '1643' },
|
||||
{ label: 'Экспансия', value: '110' },
|
||||
{ label: 'Эксперименты над людьми', value: '7408' },
|
||||
{ label: 'Экшен', value: '2859' },
|
||||
{ label: 'Элементальная магия', value: '1264' },
|
||||
{ label: 'Элементы бдсм', value: '1323' },
|
||||
{ label: 'Эльфы', value: '1362' },
|
||||
{ label: 'Эротика', value: '2053' },
|
||||
{ label: 'Этти', value: '1317' },
|
||||
{ label: 'Юмор', value: '7360' },
|
||||
{ label: 'Яды', value: '201' },
|
||||
{ label: 'Якудза', value: '4599' },
|
||||
{ label: 'Яндере', value: '7111' },
|
||||
{ label: 'Bl', value: '8282' },
|
||||
{ label: 'Gl', value: '8284' },
|
||||
{ label: 'Rpg', value: '90' },
|
||||
{ label: 'Star wars', value: '1985' },
|
||||
{ label: 'Time skip', value: '824' },
|
||||
@@ -1224,7 +1135,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Приключения', value: '38' },
|
||||
{ label: 'Публичный секс', value: '33' },
|
||||
{ label: 'Романтика', value: '25' },
|
||||
{ label: 'С изображениями ', value: '36' },
|
||||
{ label: 'С изображениями', value: '36' },
|
||||
{ label: 'Сверхъестественное', value: '34' },
|
||||
{ label: 'Смат', value: '37' },
|
||||
{ label: 'Тентакли', value: '26' },
|
||||
@@ -1267,6 +1178,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Бесстрашные персонажи', value: '339' },
|
||||
{ label: 'Библиотека', value: '265' },
|
||||
{ label: 'Бистиалити', value: '391' },
|
||||
{ label: 'Битва за трон', value: '404' },
|
||||
{ label: 'Близнецы', value: '198' },
|
||||
{ label: 'Блондинка', value: '91' },
|
||||
{ label: 'Богатые персонажи', value: '248' },
|
||||
@@ -1297,12 +1209,15 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Вирус', value: '203' },
|
||||
{ label: 'Внучка', value: '189' },
|
||||
{ label: 'Война', value: '387' },
|
||||
{ label: 'Воспоминания из прошлого', value: '405' },
|
||||
{ label: 'Враги становятся любовниками', value: '410' },
|
||||
{ label: 'Время', value: '81' },
|
||||
{ label: 'Второй шанс', value: '244' },
|
||||
{ label: 'Вуайеризм', value: '62' },
|
||||
{ label: 'Выживание', value: '135' },
|
||||
{ label: 'Гарем', value: '107' },
|
||||
{ label: 'Гарри поттер', value: '97' },
|
||||
{ label: 'Гвен', value: '402' },
|
||||
{ label: 'Гг имба', value: '87' },
|
||||
{ label: 'Генетические модификации', value: '314' },
|
||||
{ label: 'Гипноз', value: '143' },
|
||||
@@ -1361,6 +1276,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Золотой дождь', value: '146' },
|
||||
{ label: 'Зомби апокалипсис', value: '241' },
|
||||
{ label: 'Зоофилия', value: '144' },
|
||||
{ label: 'Зрелые женщины', value: '398' },
|
||||
{ label: 'Игровые элементы', value: '385' },
|
||||
{ label: 'Извращения', value: '192' },
|
||||
{ label: 'Измена', value: '71' },
|
||||
@@ -1376,6 +1292,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Интроверт', value: '38' },
|
||||
{ label: 'Инфекция', value: '207' },
|
||||
{ label: 'Инцест', value: '35' },
|
||||
{ label: 'Исторический роман', value: '406' },
|
||||
{ label: 'Камера', value: '92' },
|
||||
{ label: 'Колледж', value: '82' },
|
||||
{ label: 'Комикс', value: '286' },
|
||||
@@ -1393,7 +1310,9 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Кулинария', value: '362' },
|
||||
{ label: 'Культвация', value: '86' },
|
||||
{ label: 'Кунилингус', value: '152' },
|
||||
{ label: 'Кушина', value: '399' },
|
||||
{ label: 'Лактация', value: '11' },
|
||||
{ label: 'Лесби', value: '408' },
|
||||
{ label: 'Лишение девственности', value: '212' },
|
||||
{ label: 'Лоли', value: '218' },
|
||||
{ label: 'Лунатизм', value: '279' },
|
||||
@@ -1428,6 +1347,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Минет', value: '52' },
|
||||
{ label: 'Мистика', value: '250' },
|
||||
{ label: 'Младшая сестра', value: '154' },
|
||||
{ label: 'Много спермы', value: '409' },
|
||||
{ label: 'Модель', value: '324' },
|
||||
{ label: 'Монстры', value: '333' },
|
||||
{ label: 'Мошеничество', value: '44' },
|
||||
@@ -1435,6 +1355,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Мужчина протагонист', value: '108' },
|
||||
{ label: 'Музыка', value: '331' },
|
||||
{ label: 'Мэри съюха', value: '309' },
|
||||
{ label: 'Наруко', value: '400' },
|
||||
{ label: 'Наруто', value: '46' },
|
||||
{ label: 'Насилие', value: '128' },
|
||||
{ label: 'Насилие и жестокость', value: '193' },
|
||||
@@ -1495,6 +1416,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Постапокалипсис', value: '332' },
|
||||
{ label: 'Потеря девственности', value: '142' },
|
||||
{ label: 'Похищение', value: '161' },
|
||||
{ label: 'Преданные любовный интерес', value: '407' },
|
||||
{ label: 'Приключения', value: '134' },
|
||||
{ label: 'Принуждение', value: '66' },
|
||||
{ label: 'Психология', value: '238' },
|
||||
@@ -1506,6 +1428,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Разврат', value: '64' },
|
||||
{ label: 'Райзен', value: '95' },
|
||||
{ label: 'Раса инопланетных космических лесбиянок', value: '294' },
|
||||
{ label: 'Религия', value: '403' },
|
||||
{ label: 'Рестленг', value: '84' },
|
||||
{ label: 'Риас гремори', value: '389' },
|
||||
{ label: 'Романтика', value: '178' },
|
||||
@@ -1618,6 +1541,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Юмор', value: '105' },
|
||||
{ label: 'Яндере', value: '155' },
|
||||
{ label: 'Bl', value: '125' },
|
||||
{ label: 'Dc', value: '411' },
|
||||
{ label: 'Gl', value: '96' },
|
||||
{ label: 'Harry potter', value: '190' },
|
||||
{ label: 'Harry potter', value: '168' },
|
||||
@@ -1625,6 +1549,7 @@ const ErolateScraper = new MultiSrcScraper(
|
||||
{ label: 'Marvel', value: '194' },
|
||||
{ label: 'Marvel', value: '174' },
|
||||
{ label: 'Milf', value: '167' },
|
||||
{ label: 'Mj', value: '401' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
|
||||
@@ -81,11 +81,11 @@ class RulateScraper {
|
||||
async popularNovels(page, { showLatestNovels, filters }) {
|
||||
const baseUrl = this.baseUrl;
|
||||
const sourceId = this.sourceId;
|
||||
let url = baseUrl + '/search?t=&cat=2';
|
||||
url += '&sort=' + showLatestNovels ? '4' : filters?.sort || '6';
|
||||
url += '&type=' + filters?.type || '0';
|
||||
url += '&atmosphere=' + filters?.atmosphere || '0';
|
||||
url += '&adult=' + filters?.adult || '0';
|
||||
let url = baseUrl + '/search?t=&cat=0';
|
||||
url += '&type=' + (filters?.type || '0');
|
||||
url += '&sort=' + (showLatestNovels ? '4' : filters?.sort || '6');
|
||||
url += '&atmosphere=' + (filters?.atmosphere || '0');
|
||||
url += '&adult=' + (filters?.adult || '0');
|
||||
|
||||
if (filters?.genres?.length) {
|
||||
url += filters.genres.map(i => `&genres[]=${i}`).join('');
|
||||
@@ -145,45 +145,51 @@ class RulateScraper {
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('div[class="container"] > div > div > h1')
|
||||
novel.novelName = loadedCheerio(
|
||||
'div[class="container"] > div > div > h1, div.span8:nth-child(1) > h1:nth-child(2)',
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
novel.novelCover =
|
||||
this.baseUrl + loadedCheerio('div[class="images"] > div img').attr('src');
|
||||
novel.summary = loadedCheerio('#Info > div:nth-child(3)').html();
|
||||
novel.summary = loadedCheerio(
|
||||
'#Info > div:nth-child(3), .book-description',
|
||||
).html();
|
||||
let genre = [];
|
||||
|
||||
if (novel?.summary) {
|
||||
novel.summary = htmlToText(novel.summary);
|
||||
}
|
||||
|
||||
loadedCheerio('div[class="span5"] > p').each(function () {
|
||||
switch (loadedCheerio(this).find('strong').text()) {
|
||||
case 'Автор:':
|
||||
novel.author = loadedCheerio(this).find('em > a').text().trim();
|
||||
break;
|
||||
case 'Выпуск:':
|
||||
novel.status =
|
||||
loadedCheerio(this).find('em').text().trim() === 'продолжается'
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED;
|
||||
break;
|
||||
case 'Тэги:':
|
||||
loadedCheerio(this)
|
||||
.find('em > a')
|
||||
.each(function () {
|
||||
genre.push(loadedCheerio(this).text());
|
||||
});
|
||||
break;
|
||||
case 'Жанры:':
|
||||
loadedCheerio(this)
|
||||
.find('em > a')
|
||||
.each(function () {
|
||||
genre.push(loadedCheerio(this).text());
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
loadedCheerio('div.span5 > p, .span5 > div:nth-child(2) > p').each(
|
||||
function () {
|
||||
switch (loadedCheerio(this).find('strong').text()) {
|
||||
case 'Автор:':
|
||||
novel.author = loadedCheerio(this).find('em > a').text().trim();
|
||||
break;
|
||||
case 'Выпуск:':
|
||||
novel.status =
|
||||
loadedCheerio(this).find('em').text().trim() === 'продолжается'
|
||||
? Status.Ongoing
|
||||
: Status.Completed;
|
||||
break;
|
||||
case 'Тэги:':
|
||||
loadedCheerio(this)
|
||||
.find('em > a')
|
||||
.each(function () {
|
||||
genre.push(loadedCheerio(this).text());
|
||||
});
|
||||
break;
|
||||
case 'Жанры:':
|
||||
loadedCheerio(this)
|
||||
.find('em > a')
|
||||
.each(function () {
|
||||
genre.push(loadedCheerio(this).text());
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (genre.length > 0) {
|
||||
novel.genre = genre.reverse().join(',');
|
||||
|
||||
@@ -128,8 +128,9 @@ class WQMangaStreamScraper {
|
||||
let chapterText = loadedCheerio('.epcontent').html();
|
||||
|
||||
if (sourceId === 53) {
|
||||
let ignore = loadedCheerio('.epcontent > p').next().attr('class');
|
||||
loadedCheerio(`p.${ignore}`).remove();
|
||||
let ignore = loadedCheerio('article > style').text().trim().split(',');
|
||||
ignore.push(...ignore.pop().match(/^\.\w+/));
|
||||
ignore.map(tag => loadedCheerio(`p${tag}`).remove());
|
||||
chapterText = loadedCheerio('.epcontent').html();
|
||||
}
|
||||
|
||||
|
||||
@@ -91,16 +91,12 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}/search/books?keyword=${searchTerm}`;
|
||||
const url = `https://api.bookriver.ru/api/v1/search/autocomplete?keyword=${searchTerm}&page=1&perPage=10`;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
let json = loadedCheerio('#__NEXT_DATA__').html();
|
||||
json = JSON.parse(json);
|
||||
const json = await result.json();
|
||||
|
||||
let novels = [];
|
||||
json.props.pageProps.state.catalog.books.books.forEach(novel =>
|
||||
json?.data?.books?.forEach(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.name,
|
||||
|
||||
+2499
-69
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import { FilterInputs } from '../types/filterTypes';
|
||||
import {
|
||||
SourceChapter,
|
||||
SourceChapterItem,
|
||||
SourceNovel,
|
||||
SourceNovelItem,
|
||||
} from '../types';
|
||||
|
||||
const sourceId = 168;
|
||||
const sourceName = 'LitSpace';
|
||||
|
||||
const baseUrl = 'https://freedlit.space';
|
||||
|
||||
const popularNovels = async (page, { showLatestNovels, filters }) => {
|
||||
let url = baseUrl + '/books/';
|
||||
url += (filters?.genre || 'all') + '?sort=';
|
||||
url += showLatestNovels ? 'recent' : filters?.sort || 'popular';
|
||||
url += '&status=' + (filters?.status || 'all');
|
||||
url += '&access=' + (filters?.access || 'all');
|
||||
url += '&adult=' + (filters?.adult || 'hide');
|
||||
url += '&page=' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novels: SourceNovelItem[] = [];
|
||||
loadedCheerio('#bookListBlock > div > div').each(function () {
|
||||
const novelName = loadedCheerio(this).find('div > h4 > a').text()?.trim();
|
||||
const novelCover = loadedCheerio(this)
|
||||
.find('div > a > img')
|
||||
.attr('src')
|
||||
?.trim();
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find('div > h4 > a')
|
||||
.attr('href')
|
||||
?.trim();
|
||||
|
||||
novels.push({ sourceId, novelName, novelCover, novelUrl });
|
||||
});
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const result = await fetch(novelUrl);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novel: SourceNovel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
novelUrl,
|
||||
url: novelUrl,
|
||||
novelName: loadedCheerio('.book-info > h4').text(),
|
||||
novelCover: loadedCheerio('.book-cover > div > img').attr('src')?.trim(),
|
||||
summary: loadedCheerio('#nav-home').text()?.trim(),
|
||||
author: loadedCheerio('.book-info > h5 > a').text(),
|
||||
genre: loadedCheerio('.genre-list > a')
|
||||
.map((index, element) => loadedCheerio(element).text())
|
||||
.get()
|
||||
.join(','),
|
||||
};
|
||||
|
||||
let chapters: SourceChapterItem[] = [];
|
||||
|
||||
loadedCheerio('#nav-contents > div').each(function () {
|
||||
const chapterName = loadedCheerio(this).find('a').text();
|
||||
const releaseDate = loadedCheerio(this).find('span[class="date"]').text();
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
if (chapterName && chapterUrl) {
|
||||
chapters.push({ chapterName, releaseDate, chapterUrl });
|
||||
}
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const result = await fetch(chapterUrl);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
loadedCheerio('div[class="standart-block"]').remove();
|
||||
loadedCheerio('div[class="mobile-block"]').remove();
|
||||
|
||||
const chapter: SourceChapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName: loadedCheerio('li[class="active"]').text()?.trim(),
|
||||
chapterText: loadedCheerio('div[class="chapter"]').html(),
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}/search?query=${searchTerm}&type=all`;
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels: Novel.Item[] = [];
|
||||
loadedCheerio('#bookListBlock > div').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h4 > a').text()?.trim();
|
||||
const novelCover = loadedCheerio(this).find('a > img').attr('src')?.trim();
|
||||
const novelUrl = loadedCheerio(this).find('h4 > a').attr('href')?.trim();
|
||||
|
||||
novels.push({ sourceId, novelName, novelCover, novelUrl });
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const filters = [
|
||||
{
|
||||
key: 'sort',
|
||||
label: 'Сортировка:',
|
||||
values: [
|
||||
{ label: 'По популярности', value: 'popular' },
|
||||
{ label: 'По количеству комментариев', value: 'comments' },
|
||||
{ label: 'По количеству лайков', value: 'likes' },
|
||||
{ label: 'По новизне', value: 'recent' },
|
||||
{ label: 'По просмотрам', value: 'views' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'genre',
|
||||
label: 'Жанры:',
|
||||
values: [
|
||||
{ label: 'Любой жанр', value: 'all' },
|
||||
{ label: 'Альтернативная история', value: 'alternative-history' },
|
||||
{ label: 'Антиутопия', value: 'dystopia' },
|
||||
{ label: 'Бизнес-литература', value: 'business-literature' },
|
||||
{ label: 'Боевая фантастика', value: 'combat-fiction' },
|
||||
{ label: 'Боевик', value: 'action' },
|
||||
{ label: 'Боевое фэнтези', value: 'combat-fantasy' },
|
||||
{ label: 'Бояръ-Аниме', value: 'boyar-anime' },
|
||||
{ label: 'Героическая фантастика', value: 'heroic-fiction' },
|
||||
{ label: 'Героическое фэнтези', value: 'heroic-fantasy' },
|
||||
{ label: 'Городское фэнтези', value: 'urban-fantasy' },
|
||||
{ label: 'Гримдарк', value: 'grimdark' },
|
||||
{ label: 'Детектив', value: 'mystery' },
|
||||
{ label: 'Детская литература', value: 'kids-literature' },
|
||||
{ label: 'Документальная проза', value: 'biography' },
|
||||
{ label: 'Историческая проза', value: 'historical-fiction' },
|
||||
{ label: 'Исторический детектив', value: 'historical-mystery' },
|
||||
{
|
||||
label: 'Исторический любовный роман',
|
||||
value: 'historical-romantic-novel',
|
||||
},
|
||||
{ label: 'Историческое фэнтези', value: 'historical-fantasy' },
|
||||
{ label: 'Киберпанк', value: 'cyberpunk' },
|
||||
{ label: 'Космическая фантастика', value: 'cosmic-fiction' },
|
||||
{ label: 'ЛитРПГ', value: 'litrpg' },
|
||||
{ label: 'Лоу / Низкое фэнтези', value: 'low-fantasy' },
|
||||
{ label: 'Любовное фэнтези', value: 'romantic-fantasy' },
|
||||
{ label: 'Любовный роман', value: 'romantic-novel' },
|
||||
{ label: 'Мистика', value: 'mystic' },
|
||||
{ label: 'Мистический детектив', value: 'mystic-detective' },
|
||||
{ label: 'Научная фантастика', value: 'science-fiction' },
|
||||
{ label: 'Подростковая проза', value: 'young-adult' },
|
||||
{ label: 'Политический роман', value: 'political-romance' },
|
||||
{ label: 'Попаданцы', value: 'accidental-travel' },
|
||||
{ label: 'Попаданцы в магические миры', value: 'magic-worlds-travel' },
|
||||
{ label: 'Попаданцы во времени', value: 'time-travel' },
|
||||
{ label: 'Порнотика', value: 'pornotica' },
|
||||
{ label: 'Постапокалипсис', value: 'post-apocalypse' },
|
||||
{ label: 'Поэзия', value: 'poetry' },
|
||||
{ label: 'Приключения', value: 'adventure' },
|
||||
{ label: 'Публицистика', value: 'journalism' },
|
||||
{ label: 'Пьеса', value: 'play' },
|
||||
{ label: 'Развитие личности', value: 'how-to-book' },
|
||||
{ label: 'Разное', value: 'other' },
|
||||
{ label: 'Реализм', value: 'Realism' },
|
||||
{ label: 'РеалРПГ', value: 'realrpg' },
|
||||
{ label: 'Репликация', value: 'replication' },
|
||||
{ label: 'Романтическая эротика', value: 'romantic-erotic-fiction' },
|
||||
{ label: 'Сказка', value: 'fairy-tale' },
|
||||
{ label: 'Слэш', value: 'slash' },
|
||||
{ label: 'Современная проза', value: 'modern-prose' },
|
||||
{ label: 'Современный любовный роман', value: 'modern-romantic-novel' },
|
||||
{ label: 'Социальная фантастика', value: 'social-fiction' },
|
||||
{ label: 'Стимпанк', value: 'steampunk' },
|
||||
{ label: 'Сценарий', value: 'scenario' },
|
||||
{ label: 'Сюаньхуань', value: 'xuanhuan' },
|
||||
{ label: 'Сянься', value: 'xianxia' },
|
||||
{ label: 'Тёмное фэнтези', value: 'dark-fantasy' },
|
||||
{ label: 'Триллер', value: 'thriller' },
|
||||
{ label: 'Уся', value: 'wuxia' },
|
||||
{ label: 'Фантастика', value: 'fiction' },
|
||||
{ label: 'Фантастический детектив', value: 'mystery-fiction' },
|
||||
{ label: 'Фанфик', value: 'fan-fiction' },
|
||||
{ label: 'Фемслэш', value: 'femslash' },
|
||||
{ label: 'Фэнтези', value: 'fantasy' },
|
||||
{ label: 'Хоррор', value: 'horror' },
|
||||
{ label: 'Шпионский детектив', value: 'spy-crime' },
|
||||
{ label: 'Эпическое фэнтези', value: 'epic-fantasy' },
|
||||
{ label: 'Эротика', value: 'erotic-fiction' },
|
||||
{ label: 'Эротическая фантастика', value: 'erotic-fiction' },
|
||||
{ label: 'Эротический фанфик', value: 'erotic-fan-fiction' },
|
||||
{ label: 'Эротическое фэнтези', value: 'erotic-fantasy' },
|
||||
{ label: 'Этническое фэнтези', value: 'ethnic-fantasy' },
|
||||
{ label: 'Юмор', value: 'humor' },
|
||||
{ label: 'Юмористическая фантастика', value: 'humor-fiction' },
|
||||
{ label: 'Юмористическое фэнтези', value: 'humor-fantasy' },
|
||||
{ label: 'RPS', value: 'rps' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Статус:',
|
||||
values: [
|
||||
{ label: 'Любой статус', value: 'all' },
|
||||
{ label: 'В процессе', value: 'in-process' },
|
||||
{ label: 'Завершено', value: 'finished' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'access',
|
||||
label: 'Доступ:',
|
||||
values: [
|
||||
{ label: 'Любой доступ', value: 'all' },
|
||||
{ label: 'Бесплатные', value: 'free' },
|
||||
{ label: 'Платные', value: 'paid' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'adult',
|
||||
label: 'Возрастные ограничения:',
|
||||
values: [
|
||||
{ label: 'Скрыть 18+', value: 'hide' },
|
||||
{ label: 'Показать +18', value: 'show' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
];
|
||||
|
||||
const LitSpaceScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
filters,
|
||||
};
|
||||
|
||||
export default LitSpaceScraper;
|
||||
+118
-23
@@ -8,17 +8,26 @@ const sourceName = 'Jaomix';
|
||||
const baseUrl = 'https://jaomix.ru';
|
||||
|
||||
const popularNovels = async (page, { showLatestNovels, filters }) => {
|
||||
let url = baseUrl + '/?searchrn&sortby=';
|
||||
url += showLatestNovels ? 'upd' : filters?.sort || 'count';
|
||||
let url = baseUrl + '/?searchrn';
|
||||
|
||||
if (filters?.type?.length) {
|
||||
url += filters.type.map(i => `&lang[]=${i}`).join('');
|
||||
if (filters?.lang instanceof Array) {
|
||||
url += filters.lang.map((lang, idx) => `&lang[${idx}]=${lang}`).join('');
|
||||
}
|
||||
if (filters?.genre instanceof Array) {
|
||||
url += filters.genre
|
||||
.map((genre, idx) => `&genre[${idx}]=${genre}`)
|
||||
.join('');
|
||||
}
|
||||
if (filters?.delgenre instanceof Array) {
|
||||
url += filters.delgenre
|
||||
.map((genre, idx) => `&delgenre[${idx}]=del ${genre}`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
if (filters?.genres?.length) {
|
||||
url += filters.genres.map(i => `&genre[]=${i}`).join('');
|
||||
}
|
||||
url += `&page=${page}`;
|
||||
url += '&sortcountchapt=' + (filters?.sortcountchapt || '1');
|
||||
url += '&sortdaycreate=' + (filters?.sortdaycreate || '1');
|
||||
url += '&sortby=' + (showLatestNovels ? 'upd' : filters?.sortby || 'topweek');
|
||||
url += '&gpage=' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
let body = await result.text();
|
||||
@@ -136,30 +145,53 @@ const searchNovels = async searchTerm => {
|
||||
|
||||
const filters = [
|
||||
{
|
||||
key: 'sort',
|
||||
label: 'Сортировка',
|
||||
key: 'sortby',
|
||||
label: 'Сортировка:',
|
||||
values: [
|
||||
{ label: 'Имя', value: 'alphabet' },
|
||||
{ label: 'Просмотры', value: 'count' },
|
||||
{ label: 'Дате добавления', value: 'new' },
|
||||
{ label: 'Дате обновления', value: 'upd' },
|
||||
{ label: 'Топ недели', value: 'topweek' },
|
||||
{ label: 'По алфавиту', value: 'alphabet' },
|
||||
{ label: 'По дате обновления', value: 'upd' },
|
||||
{ label: 'По дате создания', value: 'new' },
|
||||
{ label: 'По просмотрам', value: 'count' },
|
||||
{ label: 'Топ года', value: 'topyear' },
|
||||
{ label: 'Топ дня', value: 'topday' },
|
||||
{ label: 'Топ за все время', value: 'alltime' },
|
||||
{ label: 'Топ месяца', value: 'topmonth' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: 'Тип',
|
||||
key: 'sortdaycreate',
|
||||
label: 'Дата добавления:',
|
||||
values: [
|
||||
{ label: 'Английский', value: 'Английский' },
|
||||
{ label: 'Китайский', value: 'Китайский' },
|
||||
{ label: 'Корейский', value: 'Корейский' },
|
||||
{ label: 'Японский', value: 'Японский' },
|
||||
{ label: 'Любое', value: '1' },
|
||||
{ label: 'От 120 до 180 дней', value: '1218' },
|
||||
{ label: 'От 180 до 365 дней', value: '1836' },
|
||||
{ label: 'От 30 до 60 дней', value: '3060' },
|
||||
{ label: 'От 365 дней', value: '365' },
|
||||
{ label: 'От 60 до 90 дней', value: '6090' },
|
||||
{ label: 'От 90 до 120 дней', value: '9012' },
|
||||
{ label: 'Послед. 30 дней', value: '30' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'genres',
|
||||
label: 'Жанры',
|
||||
key: 'sortcountchapt',
|
||||
label: 'Количество глав:',
|
||||
values: [
|
||||
{ label: 'Любое кол-во глав', value: '1' },
|
||||
{ label: 'До 500', value: '500' },
|
||||
{ label: 'От 1000 до 2000', value: '1020' },
|
||||
{ label: 'От 2000 до 3000', value: '2030' },
|
||||
{ label: 'От 3000 до 4000', value: '3040' },
|
||||
{ label: 'От 4000', value: '400' },
|
||||
{ label: 'От 500 до 1000', value: '510' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'genre',
|
||||
label: 'Жанры:',
|
||||
values: [
|
||||
{ label: 'Боевые Искусства', value: 'Боевые Искусства' },
|
||||
{ label: 'Виртуальный Мир', value: 'Виртуальный Мир' },
|
||||
@@ -209,6 +241,69 @@ const filters = [
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'delgenre',
|
||||
label: 'Исключить жанры:',
|
||||
values: [
|
||||
{ label: 'Боевые Искусства', value: 'Боевые Искусства' },
|
||||
{ label: 'Виртуальный Мир', value: 'Виртуальный Мир' },
|
||||
{ label: 'Гарем', value: 'Гарем' },
|
||||
{ label: 'Детектив', value: 'Детектив' },
|
||||
{ label: 'Драма', value: 'Драма' },
|
||||
{ label: 'Игра', value: 'Игра' },
|
||||
{ label: 'Истории из жизни', value: 'Истории из жизни' },
|
||||
{ label: 'Исторический', value: 'Исторический' },
|
||||
{ label: 'История', value: 'История' },
|
||||
{ label: 'Исэкай', value: 'Исэкай' },
|
||||
{ label: 'Комедия', value: 'Комедия' },
|
||||
{ label: 'Меха', value: 'Меха' },
|
||||
{ label: 'Мистика', value: 'Мистика' },
|
||||
{ label: 'Научная Фантастика', value: 'Научная Фантастика' },
|
||||
{ label: 'Повседневность', value: 'Повседневность' },
|
||||
{ label: 'Постапокалипсис', value: 'Постапокалипсис' },
|
||||
{ label: 'Приключения', value: 'Приключения' },
|
||||
{ label: 'Психология', value: 'Психология' },
|
||||
{ label: 'Романтика', value: 'Романтика' },
|
||||
{ label: 'Сверхъестественное', value: 'Сверхъестественное' },
|
||||
{ label: 'Сёнэн', value: 'Сёнэн' },
|
||||
{ label: 'Сёнэн-ай', value: 'Сёнэн-ай' },
|
||||
{ label: 'Спорт', value: 'Спорт' },
|
||||
{ label: 'Сэйнэн', value: 'Сэйнэн' },
|
||||
{ label: 'Сюаньхуа', value: 'Сюаньхуа' },
|
||||
{ label: 'Трагедия', value: 'Трагедия' },
|
||||
{ label: 'Триллер', value: 'Триллер' },
|
||||
{ label: 'Фантастика', value: 'Фантастика' },
|
||||
{ label: 'Фэнтези', value: 'Фэнтези' },
|
||||
{ label: 'Хоррор', value: 'Хоррор' },
|
||||
{ label: 'Школьная жизнь', value: 'Школьная жизнь' },
|
||||
{ label: 'Шоунен', value: 'Шоунен' },
|
||||
{ label: 'Экшн', value: 'Экшн' },
|
||||
{ label: 'Этти', value: 'Этти' },
|
||||
{ label: 'Юри', value: 'Юри' },
|
||||
{ label: 'Adult', value: 'Adult' },
|
||||
{ label: 'Ecchi', value: 'Ecchi' },
|
||||
{ label: 'Josei', value: 'Josei' },
|
||||
{ label: 'Lolicon', value: 'Lolicon' },
|
||||
{ label: 'Mature', value: 'Mature' },
|
||||
{ label: 'Shoujo', value: 'Shoujo' },
|
||||
{ label: 'Wuxia', value: 'Wuxia' },
|
||||
{ label: 'Xianxia', value: 'Xianxia' },
|
||||
{ label: 'Xuanhuan', value: 'Xuanhuan' },
|
||||
{ label: 'Yaoi', value: 'Yaoi' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'lang',
|
||||
label: 'Выбрать языки:',
|
||||
values: [
|
||||
{ label: 'Английский', value: 'Английский' },
|
||||
{ label: 'Китайский', value: 'Китайский' },
|
||||
{ label: 'Корейский', value: 'Корейский' },
|
||||
{ label: 'Японский', value: 'Японский' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
];
|
||||
|
||||
const JaomixScraper = {
|
||||
|
||||
+66
-49
@@ -13,22 +13,26 @@ const popularNovels = async (page, { showLatestNovels, filters }) => {
|
||||
url += filters?.order ? filters?.order?.replace('+', '') : '-';
|
||||
url += showLatestNovels ? 'chapter_date' : filters?.sort || 'rating';
|
||||
|
||||
if (filters?.type?.length) {
|
||||
url += filters.type.map(i => `&types=${i}`).join('');
|
||||
}
|
||||
|
||||
if (filters?.statuss?.length) {
|
||||
url += filters?.statuss.map(i => `&status=${i}`).join('');
|
||||
}
|
||||
|
||||
if (filters?.genres?.length) {
|
||||
if (filters?.genres instanceof Array) {
|
||||
url += filters.genres.map(i => `&genres=${i}`).join('');
|
||||
}
|
||||
|
||||
if (filters?.categories?.length) {
|
||||
if (filters?.status instanceof Array) {
|
||||
url += filters?.status.map(i => `&status=${i}`).join('');
|
||||
}
|
||||
|
||||
if (filters?.types instanceof Array) {
|
||||
url += filters.types.map(i => `&types=${i}`).join('');
|
||||
}
|
||||
|
||||
if (filters?.categories instanceof Array) {
|
||||
url += filters.categories.map(i => `&categories=${i}`).join('');
|
||||
}
|
||||
|
||||
if (filters?.age_limit instanceof Array) {
|
||||
url += filters.age_limit.map(i => `&age_limit=${i}`).join('');
|
||||
}
|
||||
|
||||
url += '&page=' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
@@ -38,8 +42,9 @@ const popularNovels = async (page, { showLatestNovels, filters }) => {
|
||||
body.content.forEach(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.rus_name,
|
||||
cover: baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
|
||||
novelName: novel.main_name || novel.secondary_name,
|
||||
novelCover:
|
||||
baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
|
||||
novelUrl: novel.dir,
|
||||
}),
|
||||
);
|
||||
@@ -56,13 +61,13 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
sourceName,
|
||||
url: baseUrl + '/novel/' + body.content.dir,
|
||||
novelUrl,
|
||||
novelName: body.content.rus_name,
|
||||
novelName: body.content.main_name || body.content.secondary_name,
|
||||
summary: htmlToText(body.content.description),
|
||||
novelCover:
|
||||
baseUrl +
|
||||
(body.content.img?.high || body.content.img?.mid || body.content.img.low),
|
||||
status:
|
||||
body.content.status.name === 'Продолжается'
|
||||
body.content?.status?.name === 'Продолжается'
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED,
|
||||
};
|
||||
@@ -75,14 +80,15 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
novel.genre = tags.join(',');
|
||||
}
|
||||
|
||||
let all = (body.content.count_chapters / 100 + 1) ^ 0;
|
||||
let all = Math.floor(body.content.count_chapters / 100 + 1);
|
||||
let branch_id = body.content.branches?.[0]?.id || body.content.id;
|
||||
let chapters = [];
|
||||
|
||||
for (let i = 0; i < all; i++) {
|
||||
let chapterResult = await fetch(
|
||||
baseUrl +
|
||||
'/api/titles/chapters/?branch_id=' +
|
||||
body.content.branches[0].id +
|
||||
branch_id +
|
||||
'&count=100&page=' +
|
||||
(i + 1),
|
||||
);
|
||||
@@ -128,8 +134,9 @@ const searchNovels = async searchTerm => {
|
||||
body.content.forEach(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.rus_name,
|
||||
cover: baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
|
||||
novelName: novel.main_name || novel.secondary_name,
|
||||
novelCover:
|
||||
baseUrl + (novel.img?.high || novel.img?.mid || novel.img.low),
|
||||
novelUrl: novel.dir,
|
||||
}),
|
||||
);
|
||||
@@ -155,41 +162,14 @@ const filters = [
|
||||
key: 'order',
|
||||
label: 'Порядок',
|
||||
values: [
|
||||
{ label: 'По убыванию', value: '-' }, // desc
|
||||
{ label: 'По возрастанию', value: '+' }, // asc
|
||||
{ label: 'По убыванию', value: '-' },
|
||||
{ label: 'По возрастанию', value: '+' },
|
||||
],
|
||||
inputType: FilterInputs.Picker,
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: 'Тип',
|
||||
values: [
|
||||
{ label: 'Авторское', value: '1' },
|
||||
{ label: 'Другое', value: '7' },
|
||||
{ label: 'Запад', value: '5' },
|
||||
{ label: 'Китай', value: '4' },
|
||||
{ label: 'Корея', value: '3' },
|
||||
{ label: 'Фанфики', value: '6' },
|
||||
{ label: 'Япония', value: '2' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'statuss',
|
||||
label: 'Статус тайтла',
|
||||
values: [
|
||||
{ label: 'Закончен', value: '0' },
|
||||
{ label: 'Продолжается', value: '1' },
|
||||
{ label: 'Заморожен', value: '2' },
|
||||
{ label: 'Нет переводчика', value: '3' },
|
||||
{ label: 'Анонс', value: '4' },
|
||||
{ label: 'Лицензировано', value: '5' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'genres',
|
||||
label: 'Жанры',
|
||||
label: 'Жанры:',
|
||||
values: [
|
||||
{ label: 'Боевик', value: '112' },
|
||||
{ label: 'Война', value: '123' },
|
||||
@@ -226,7 +206,7 @@ const filters = [
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
label: 'Категории',
|
||||
label: 'Тэги:',
|
||||
values: [
|
||||
{ label: '[Награжденная работа]', value: '648' },
|
||||
{ label: '18+', value: '423' },
|
||||
@@ -928,6 +908,43 @@ const filters = [
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'types',
|
||||
label: 'Типы:',
|
||||
values: [
|
||||
{ label: 'Авторское', value: '1' },
|
||||
{ label: 'Другое', value: '7' },
|
||||
{ label: 'Запад', value: '5' },
|
||||
{ label: 'Китай', value: '4' },
|
||||
{ label: 'Корея', value: '3' },
|
||||
{ label: 'Фанфики', value: '6' },
|
||||
{ label: 'Япония', value: '2' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Статус проекта:',
|
||||
values: [
|
||||
{ label: 'Анонс', value: '4' },
|
||||
{ label: 'Закончен', value: '0' },
|
||||
{ label: 'Заморожен', value: '2' },
|
||||
{ label: 'Лицензировано', value: '5' },
|
||||
{ label: 'Нет переводчика', value: '3' },
|
||||
{ label: 'Продолжается', value: '1' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
{
|
||||
key: 'age_limit',
|
||||
label: 'Возрастной рейтинг:',
|
||||
values: [
|
||||
{ label: '16+', value: '1' },
|
||||
{ label: '18+', value: '2' },
|
||||
{ label: 'Для всех', value: '0' },
|
||||
],
|
||||
inputType: FilterInputs.Checkbox,
|
||||
},
|
||||
];
|
||||
|
||||
const RenovelsScraper = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import EPubSource from './local/epubSource';
|
||||
import ComradeMaoScraper from './en/comrademao';
|
||||
import ReadLightNovelScraper from './en/readlightnovel';
|
||||
import fastNovelScraper from './en/fastnovel';
|
||||
@@ -18,6 +19,7 @@ import EinharjarProjectScraper from './es/einherjarproject';
|
||||
import TuNovelaLigeraScraper from './es/tunovelaligera';
|
||||
import SkyNovelsScraper from './es/skynovels';
|
||||
import NovelasLigeraScraper from './es/novelasligera';
|
||||
import NOVAScraper from './es/nova';
|
||||
import YuukiTlsScraper from './es/yuukitls';
|
||||
import NovelaWuxiaScraper from './es/novelawuxia';
|
||||
import OasisTranslationsScraper from './es/oasistranslations';
|
||||
@@ -78,6 +80,7 @@ import NovelUpdatesScraper from './en/novelupdates';
|
||||
import RanobesScraper from './en/ranobes';
|
||||
import RanobesruScraper from './ru/ranobes';
|
||||
import YushuboScraper from './ch/yushubo';
|
||||
import LinovelibScraper from './ch/linovelib';
|
||||
import {
|
||||
LiebeSchneeHiverNovelScraper,
|
||||
LightNovelsBrasilScraper,
|
||||
@@ -156,6 +159,8 @@ import {
|
||||
KodScraper,
|
||||
} from './multisrc/noveltl/NovelTlGenerator';
|
||||
import NovelsOnlineScraper from './en/NovelOnline';
|
||||
import SmakolykyTlScraper from './ua/smakolykytl';
|
||||
import LitSpaceScraper from './ru/freedlit';
|
||||
|
||||
interface PopularNovelsResponse {
|
||||
novels: SourceNovelItem[];
|
||||
@@ -183,6 +188,7 @@ interface Scraper {
|
||||
export const sourceManager = (sourceId: number): Scraper => {
|
||||
const scrapers: Record<number, Scraper> = {
|
||||
// @ts-ignore
|
||||
0: EPubSource, // @ts-ignore
|
||||
1: BoxNovelScraper, // @ts-ignore
|
||||
2: ReadLightNovelScraper, // @ts-ignore
|
||||
3: fastNovelScraper, // @ts-ignore
|
||||
@@ -326,6 +332,10 @@ export const sourceManager = (sourceId: number): Scraper => {
|
||||
162: KodScraper, // @ts-ignore
|
||||
163: NovelsOnlineScraper, // @ts-ignore
|
||||
164: BookRiverScraper, // @ts-ignore
|
||||
165: LinovelibScraper, // @ts-ignore
|
||||
166: NOVAScraper, // @ts-ignore
|
||||
167: SmakolykyTlScraper, // @ts-ignore
|
||||
168: LitSpaceScraper, // @ts-ignore
|
||||
};
|
||||
|
||||
return scrapers[sourceId];
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
},
|
||||
{
|
||||
"sourceId": 51,
|
||||
"url": "https://ranobes.net/",
|
||||
"url": "https://ranobes.top/",
|
||||
"sourceName": "Ranobes",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/icons/src/en/ranobes/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
@@ -386,7 +386,7 @@
|
||||
},
|
||||
{
|
||||
"sourceId": 68,
|
||||
"url": "https://www.readwn.com/",
|
||||
"url": "https://www.wuxiap.com/",
|
||||
"sourceName": "Readwn.com",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/icons/multisrc/readwn/icons/readwn.png?raw=true",
|
||||
"lang": "English"
|
||||
@@ -568,15 +568,15 @@
|
||||
},
|
||||
{
|
||||
"sourceId": 96,
|
||||
"url": "https://hizomanga.com/",
|
||||
"url": "https://hizomanga.net/",
|
||||
"sourceName": "HizoManga",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/icons/multisrc/madara/icons/hizomanga.png?raw=true",
|
||||
"lang": "Arabic"
|
||||
},
|
||||
{
|
||||
"sourceId": 97,
|
||||
"url": "https://novelfull.me/",
|
||||
"sourceName": "NovelFull.me",
|
||||
"url": "https://novelbuddy.io/",
|
||||
"sourceName": "Novel Buddy",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/icons/src/en/novelfullme/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
@@ -589,7 +589,7 @@
|
||||
},
|
||||
{
|
||||
"sourceId": 99,
|
||||
"url": "https://www.novelmt.com/",
|
||||
"url": "https://www.wuxiapub.com/",
|
||||
"sourceName": "Novelmt.com",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/icons/multisrc/readwn/icons/novelmt.png?raw=true",
|
||||
"lang": "English"
|
||||
@@ -999,5 +999,33 @@
|
||||
"url": "https://bookriver.ru/",
|
||||
"lang": "Russian",
|
||||
"icon": "https://bookriver.ru/android-chrome-512x512.png"
|
||||
},
|
||||
{
|
||||
"sourceId": 165,
|
||||
"sourceName": "Linovelib",
|
||||
"url": "https://w.linovelib.com",
|
||||
"lang": "Chinese",
|
||||
"icon": "https://w.linovelib.com/images/logo.png"
|
||||
},
|
||||
{
|
||||
"sourceId": 166,
|
||||
"sourceName": "NOVA",
|
||||
"url": "https://novelasligeras.net",
|
||||
"lang": "Spanish",
|
||||
"icon": "https://pbs.twimg.com/profile_images/1659040611507351552/DeLBdFep_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"sourceId": 167,
|
||||
"sourceName": "Смаколики",
|
||||
"url": "https://smakolykytl.site/",
|
||||
"lang": "Ukraine",
|
||||
"icon": "https://smakolykytl.site/img/favicon/apple-touch-icon.webp"
|
||||
},
|
||||
{
|
||||
"sourceId": 168,
|
||||
"sourceName": "LitSpace",
|
||||
"url": "https://freedlit.space/",
|
||||
"lang": "Russian",
|
||||
"icon": "https://freedlit.space/images/fav/apple-touch-icon.png"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import { Status } from '../helpers/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import { SourceChapter, SourceNovel, SourceNovelItem } from '../types';
|
||||
|
||||
const sourceId = 167;
|
||||
const sourceName = 'Смаколики';
|
||||
const baseUrl = 'https://smakolykytl.site/';
|
||||
|
||||
const popularNovels = async (page: number, { showLatestNovels }) => {
|
||||
const url = showLatestNovels
|
||||
? 'https://api.smakolykytl.site/api/user/updates'
|
||||
: 'https://api.smakolykytl.site/api/user/projects';
|
||||
|
||||
const result = await fetch(url);
|
||||
const json = (await result.json()) as response;
|
||||
|
||||
const novels: SourceNovelItem[] = [];
|
||||
|
||||
(json?.projects || json?.updates)?.forEach(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.title,
|
||||
novelCover: novel.image.url,
|
||||
novelUrl: baseUrl + 'titles/' + novel.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return { novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (novelUrl: string) => {
|
||||
const id = novelUrl.split('/').pop();
|
||||
const result = await fetch(
|
||||
'https://api.smakolykytl.site/api/user/projects/' + id,
|
||||
);
|
||||
const book = (await result.json()) as response;
|
||||
|
||||
const novel: SourceNovel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
novelUrl,
|
||||
url: novelUrl,
|
||||
novelName: book?.project?.title,
|
||||
novelCover: book?.project?.image?.url,
|
||||
summary: book?.project?.description,
|
||||
chapters: [],
|
||||
author: book?.project?.author,
|
||||
status: book?.project?.status_translate.includes('Триває')
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED,
|
||||
};
|
||||
let genre = [book?.project?.genres, book?.project?.tags]
|
||||
.flat()
|
||||
.map(tags => tags?.title)
|
||||
.filter(tags => tags);
|
||||
|
||||
if (genre.length > 0) {
|
||||
novel.genre = genre.join(', ');
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
'https://api.smakolykytl.site/api/user/projects/' + id + '/books',
|
||||
);
|
||||
const data = (await res.json()) as response;
|
||||
|
||||
data?.books?.forEach(volume =>
|
||||
volume?.chapters?.map(chapter =>
|
||||
novel.chapters.push({
|
||||
chapterName: volume.title + ' ' + chapter.title,
|
||||
releaseDate: dayjs(chapter.modifiedAt).format('LLL'),
|
||||
chapterUrl: baseUrl + 'read/' + chapter.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl: string, chapterUrl: string) => {
|
||||
const id = chapterUrl.split('/').pop();
|
||||
|
||||
const result = await fetch(
|
||||
'https://api.smakolykytl.site/api/user/chapters/' + id,
|
||||
);
|
||||
const json = (await result.json()) as response;
|
||||
const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || '[]');
|
||||
|
||||
const chapter: SourceChapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName: json?.chapter?.title,
|
||||
chapterText: jsonToHtml(chapterRaw),
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async (searchTerm: string) => {
|
||||
const result = await fetch('https://api.smakolykytl.site/api/user/projects');
|
||||
const json = (await result.json()) as response;
|
||||
const novels: SourceNovelItem[] = [];
|
||||
|
||||
json?.projects
|
||||
?.filter(
|
||||
novel =>
|
||||
novel.title.includes(searchTerm) || String(novel.id) === searchTerm,
|
||||
)
|
||||
?.forEach(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.title,
|
||||
novelCover: novel.image.url,
|
||||
novelUrl: baseUrl + 'titles/' + novel.id,
|
||||
}),
|
||||
);
|
||||
return novels;
|
||||
};
|
||||
|
||||
function jsonToHtml(json: HTML[], html: string = '') {
|
||||
json.forEach(element => {
|
||||
switch (element.type) {
|
||||
case 'hardBreak':
|
||||
html += '<br>';
|
||||
break;
|
||||
case 'horizontalRule':
|
||||
html += '<hr>';
|
||||
break;
|
||||
case 'image':
|
||||
if (element.attrs) {
|
||||
const attrs = Object.entries(element.attrs)
|
||||
.filter(attr => attr?.[1])
|
||||
.map(attr => `${attr[0]}="${attr[1]}"`);
|
||||
html += '<img ' + attrs.join('; ') + '>';
|
||||
}
|
||||
break;
|
||||
case 'paragraph':
|
||||
html +=
|
||||
'<p>' +
|
||||
(element.content ? jsonToHtml(element.content) : '<br>') +
|
||||
'</p>';
|
||||
break;
|
||||
case 'text':
|
||||
html += element.text;
|
||||
break;
|
||||
default:
|
||||
html += JSON.stringify(element, null, '\t'); //maybe I missed something.
|
||||
break;
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
interface response {
|
||||
projects?: TopLevelProject[];
|
||||
updates?: Update[];
|
||||
project?: TopLevelProject;
|
||||
books?: BookElement[];
|
||||
chapter?: TopLevelChapter;
|
||||
}
|
||||
|
||||
interface BookElement {
|
||||
id: number;
|
||||
rank: number;
|
||||
title: string;
|
||||
chapters: PurpleChapter[];
|
||||
}
|
||||
|
||||
interface PurpleChapter {
|
||||
id: number;
|
||||
title: string;
|
||||
rank: string;
|
||||
modifiedAt: Date;
|
||||
}
|
||||
|
||||
interface TopLevelChapter {
|
||||
id: number;
|
||||
title: string;
|
||||
rank: string;
|
||||
content: string;
|
||||
modifiedAt: Date;
|
||||
book: ChapterBook;
|
||||
}
|
||||
|
||||
interface ChapterBook {
|
||||
id: number;
|
||||
rank: number;
|
||||
title: string;
|
||||
chapters: FluffyChapter[];
|
||||
project: BookProject;
|
||||
}
|
||||
|
||||
interface FluffyChapter {
|
||||
id: number;
|
||||
rank: string;
|
||||
}
|
||||
|
||||
interface BookProject {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface TopLevelProject {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
translator: string;
|
||||
modifiedAt: Date;
|
||||
alternatives: string;
|
||||
release: string;
|
||||
nation: string;
|
||||
status: string;
|
||||
status_translate: string;
|
||||
image: Image;
|
||||
tags?: Genre[];
|
||||
genres?: Genre[];
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Image {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Update {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
translator: string;
|
||||
modifiedAt: Date;
|
||||
image: Image;
|
||||
}
|
||||
|
||||
interface HTML {
|
||||
type: string;
|
||||
content?: HTML[];
|
||||
attrs?: Attrs;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface Attrs {
|
||||
src: string;
|
||||
alt: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
const SmakolykyTlScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default SmakolykyTlScraper;
|
||||
@@ -0,0 +1,51 @@
|
||||
export type NovelRouteParams = {
|
||||
novelUrl: string;
|
||||
sourceId: number;
|
||||
novelName: string;
|
||||
};
|
||||
|
||||
export type ChapterRouteParams = {
|
||||
chapterId: number;
|
||||
chapterUrl: string;
|
||||
novelId: number;
|
||||
chapterName: string;
|
||||
bookmark: number;
|
||||
};
|
||||
|
||||
export type GetChapterScreenRouteParamsProps = NovelRouteParams &
|
||||
ChapterRouteParams;
|
||||
|
||||
export function getChapterScreenRouteParams(
|
||||
novel: NovelRouteParams,
|
||||
chapter: ChapterRouteParams,
|
||||
): GetChapterScreenRouteParamsProps {
|
||||
return {
|
||||
novelUrl: novel.novelUrl,
|
||||
sourceId: novel.sourceId,
|
||||
novelName: novel.novelName,
|
||||
chapterId: chapter.chapterId,
|
||||
chapterUrl: chapter.chapterUrl,
|
||||
novelId: chapter.novelId,
|
||||
chapterName: chapter.chapterName,
|
||||
bookmark: chapter.bookmark,
|
||||
};
|
||||
}
|
||||
export interface NovelScreenRouteParams {
|
||||
sourceId: number;
|
||||
novelId?: number;
|
||||
novelUrl: string;
|
||||
novelName: string;
|
||||
novelCover?: string;
|
||||
}
|
||||
|
||||
export function getNovelScreenRouteParams(
|
||||
novel: NovelScreenRouteParams,
|
||||
): NovelScreenRouteParams {
|
||||
return {
|
||||
sourceId: novel.sourceId,
|
||||
novelId: novel?.novelId,
|
||||
novelUrl: novel.novelUrl,
|
||||
novelName: novel.novelName,
|
||||
novelCover: novel?.novelCover,
|
||||
};
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
interface oCProps {
|
||||
novel: {
|
||||
novelUrl: string;
|
||||
sourceId: number;
|
||||
novelName: string;
|
||||
};
|
||||
chapter: {
|
||||
chapterId: number;
|
||||
chapterUrl: string;
|
||||
novelId: number;
|
||||
chapterName: string;
|
||||
bookmark: number;
|
||||
};
|
||||
}
|
||||
export type openChapterChapterTypes = oCProps['chapter'];
|
||||
export type openChapterNovelTypes = oCProps['novel'];
|
||||
export type openChapterFunctionTypes = oCProps['novel'] & oCProps['chapter'];
|
||||
|
||||
export function openChapter(
|
||||
novel: openChapterNovelTypes,
|
||||
chapter: openChapterChapterTypes,
|
||||
): openChapterFunctionTypes {
|
||||
return {
|
||||
novelUrl: novel.novelUrl,
|
||||
sourceId: novel.sourceId,
|
||||
novelName: novel.novelName,
|
||||
chapterId: chapter.chapterId,
|
||||
chapterUrl: chapter.chapterUrl,
|
||||
novelId: chapter.novelId,
|
||||
chapterName: chapter.chapterName,
|
||||
bookmark: chapter.bookmark,
|
||||
};
|
||||
}
|
||||
export interface openNovelProps {
|
||||
sourceId: number;
|
||||
novelId?: number;
|
||||
novelUrl: string;
|
||||
novelName: string;
|
||||
novelCover?: string;
|
||||
}
|
||||
export function openNovel(novel: openNovelProps): openNovelProps {
|
||||
return {
|
||||
sourceId: novel.sourceId,
|
||||
novelId: novel?.novelId,
|
||||
novelUrl: novel.novelUrl,
|
||||
novelName: novel.novelName,
|
||||
novelCover: novel?.novelCover,
|
||||
};
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export const appVersion = '1.1.15';
|
||||
export const releaseDate = '06/04/23 09:30 PM';
|
||||
export const appVersion = '1.1.18';
|
||||
export const releaseDate = '09/10/23 11:00 AM';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user