Compare commits

..

68 Commits

Author SHA1 Message Date
nguyd1 64ef4bae1f Fix Library Update Freeze (#898)
* fix updating library freeze

* fix updating library freeze

* refactor
2024-01-15 10:49:56 +05:30
Rider21 88a70cbcea Add Source ifreedom (#886)
* update

* fix

* genre

* test

* Update freewebnovel.ts

* status parsing

* test readwn

* fix typo

* Update IfreedomScraper.js

* fix ESlint

* Update ReadwnScraper.js

* deleted the build fix

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2024-01-15 10:48:10 +05:30
Isaac H cfcc4bd875 Update Contributing Guide (#876)
* added a common problems section in CONTRIBUTING.md

* added a common problems section in CONTRIBUTING.md and a new file listing common problems

* Update COMMON_ERRORS.md
2024-01-14 22:55:32 +05:30
nguyd1 02b058dac2 Remove MMKV Cookie Storage (#896)
* fix retry button

* remove cookie storage

* add cookie storage

* fix import

* remove all references of mmkv cookies
2024-01-14 22:52:57 +05:30
Patrick Loser 36cb7fc391 Horizontal Reading Mode (#733)
Closes #29
Closes #149
2024-01-14 22:52:26 +05:30
nguyd1 8613a088d2 Update Python Version in CONTRIBUTING.md (#889) 2024-01-13 14:38:13 +05:30
nguyd1 e600828bc2 Fix Cloudflare on NovelUpdates (#887)
* fix cloudflare

* fix import
2024-01-13 14:31:09 +05:30
K1ngfish3r e4364aea7c Add MTLNovel (ES) (#890)
Closes LNReader/lnreader-sources#682
2024-01-13 14:29:59 +05:30
nguyd1 df8da48c32 Fix NovelUpdates Covers (#880) 2024-01-08 14:17:24 +05:30
nguyd1 a83b4b1a32 Fix Retry Button (#879) 2024-01-08 14:17:03 +05:30
Rajarshee Chatterjee 8a85f4eaf1 Add Setting to Jump to Last Read Chapter 2024-01-07 18:38:57 +05:30
Rajarshee Chatterjee 2c1cce7fbe Reset Progress on Chapter End (Closes #791) 2024-01-07 18:18:40 +05:30
Rajarshee Chatterjee e4ca751fb0 Animated FAB in Novel Screen (Closes #839) 2024-01-07 17:58:46 +05:30
Rajarshee Chatterjee c3543f8186 Add Retry Button on Browse Screen (Closes #667) 2024-01-07 17:29:02 +05:30
Rajarshee Chatterjee 9253249ef6 Add Retry and WebView Button on Chapter Error Screen (Closes #818) 2024-01-07 17:25:02 +05:30
Rajarshee Chatterjee e0e593bf9f Filter Duplicates in Chapter Selection (Closes #847) 2024-01-04 22:58:18 +05:30
Rajarshee Chatterjee a298ed81d7 Add Exclude Downloads Filter (Closes #848) 2024-01-04 22:16:01 +05:30
K1ngfish3r 60cf358e57 Source Updates (#807)
* dev woopread

* check if input breaks chapterText

* AsuraLightNovel dev

* add Titan.wf to novelupdates

* rough fix novelpub

* remove userAgent check

* i forgot one

* lnmtl ChapterText fix

* fix author, genre and status lnmtl

* xperimental

* grounded quotation marks lnmtl

* fix woopread missing chapter

* refactor wuxia.blog

* copypasta lightnovelpub

* until stylesheets are added

* remove console.log

* revert armtl and wuxiablog search delete

* fix eslint check?

* fastnovel to novelbin

* Blossom Translation Dev

* skynovel fix chapter spacing

* oops

* linovelib fix?

* search fix linovelib

* update dictionary linovelib

* update dictionary 2: electric bugaloo

* skillgg dict and multipage chapter + search fix

* fix sakuranovel again

* The_Lazy_Guy Dev + TuNovelaLigera Dev

* TuNovelaLigera, ch cleanup

* under review

* remove green dictionary

* text()

* add delay

* cleanup

* allnovelfull cleanup

* update selector

* update selectors SUPER

* fix chapter name lightnovelpub

* update dictionary 3: revenge of the dict

* indowebnovel
2023-12-27 19:38:16 +05:30
Rajarshee Chatterjee 4548cdf8db chore: Update translations (#816)
* New translations strings.json (Odia)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]
2023-12-27 19:36:54 +05:30
NewbieAnimeManga ee9391bd9f fix(source): NovelOnline: Add Popular Novels (#832)
Closes LNReader/lnreader-sources#638

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2023-11-06 14:07:08 +05:30
Rajarshee Chatterjee f069a67660 feat: Save Covers Locally (Closes #780) 2023-11-01 20:29:36 +05:30
Rajarshee Chatterjee 7ee843f7b8 fix: Add Scrolling in Advanced Settings and More Header
Closes #806
Closes #800
2023-11-01 20:14:46 +05:30
Rajarshee Chatterjee 757c1eae5f chore: Update translations (#695)
* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Odia)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (Romanian)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (Afrikaans)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Catalan)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Danish)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Greek)
[ci skip]

* New translations strings.json (Finnish)
[ci skip]

* New translations strings.json (Hebrew)
[ci skip]

* New translations strings.json (Hungarian)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Japanese)
[ci skip]

* New translations strings.json (Korean)
[ci skip]

* New translations strings.json (Dutch)
[ci skip]

* New translations strings.json (Norwegian)
[ci skip]

* New translations strings.json (Polish)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Albanian)
[ci skip]

* New translations strings.json (Serbian (Cyrillic))
[ci skip]

* New translations strings.json (Swedish)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Chinese Simplified)
[ci skip]

* New translations strings.json (Chinese Traditional)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Hindi)
[ci skip]

* New translations strings.json (Odia)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Odia)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (Romanian)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (Afrikaans)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Catalan)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Danish)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Greek)
[ci skip]

* New translations strings.json (Finnish)
[ci skip]

* New translations strings.json (Hebrew)
[ci skip]

* New translations strings.json (Hungarian)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Japanese)
[ci skip]

* New translations strings.json (Korean)
[ci skip]

* New translations strings.json (Dutch)
[ci skip]

* New translations strings.json (Norwegian)
[ci skip]

* New translations strings.json (Polish)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Albanian)
[ci skip]

* New translations strings.json (Serbian (Cyrillic))
[ci skip]

* New translations strings.json (Swedish)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Chinese Simplified)
[ci skip]

* New translations strings.json (Chinese Traditional)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Hindi)
[ci skip]

* New translations strings.json (Odia)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Chinese Simplified)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Chinese Simplified)
[ci skip]
2023-10-14 11:01:54 +05:30
Rajarshee Chatterjee e8fe0fdc83 Release v1.1.18 2023-10-09 09:50:11 +05:30
Isaac H 5df5a2ccd2 fix: EPUB Parser (#795)
* Move changes to master branch

* Fixed errors stemming from improper move

* Add EPub to library

* Extract everything in root folder, don't create subfolders

* made changes to epubparser.py

* changed json metadata file and added some safeguards for different epub formats

* Correctly read metadata and convert relative paths

* Corrected path for EPUB3

* fixed cover not showing up

* Get missing chapters names

* fixed conflict with export to epub

---------

Co-authored-by: Danisty <danisty.nori@gmail.com>
2023-10-09 09:44:22 +05:30
Rider21 3f6bcb2a8f fix(source): Ficbook (#793)
* Update ficbook.js

* sorted
2023-10-07 13:20:46 +05:30
K1ngfish3r f8fef4cb2c fix(source): Novel Updates, FastNovel & Arnovel (#782)
* inoveltranslation update

* Oops

* fix eslint check?

* update fastnovel

* update for arnovel

* xperimental

* novel.sourceId
2023-10-06 07:52:20 +05:30
Rajarshee Chatterjee c15144f633 fix(source): Update HizoManga Domain (Closes LNReader/lnreader-sources#590) 2023-09-22 08:00:23 +05:30
Rajarshee Chatterjee a0096cdacb fix(source): Bypass Cloudflare in Readwn (Closes LNReader/lnreader-sources#584) 2023-09-22 07:59:43 +05:30
Rajarshee Chatterjee 7afe230ec3 fix: Crash in EPUB Modal 2023-09-22 07:47:29 +05:30
Rider21 4a49f32823 feat(source): Add Смаколики, LitSpace (#778)
* add smakolykytl

* Update smakolykytl.ts

* fix

* LitSpace

* fix

* fix renovels

* Update renovels.js
2023-09-22 07:19:21 +05:30
Rajarshee Chatterjee 925855a0dd fix(source): Bypass Cloudflare in NovelPub 2023-09-15 07:45:06 +05:30
Nick Ignoffo 6f4fdd22dd Quick novel screen polishing pass (#772)
* Animate description expansion

* Add animated (opacity) background to appbar

* Refresh control colors should match rest of app

* Fix chapter download not updating when deleting all (Closes #748)

Also closes the menu when you delete all chapters
2023-09-03 10:26:51 +05:30
Rajarshee Chatterjee 918edc4362 [skip ci] Update Issue Templates 2023-09-02 19:13:22 +05:30
Rajarshee Chatterjee e955069190 fix: Volume Button Scroll on Next Chapter (Closes #719) 2023-09-02 17:17:05 +05:30
Rajarshee Chatterjee 5037fa693a fix(source): Novel Buddy 2023-09-02 13:52:47 +05:30
Rajarshee Chatterjee 527289372b feat(source): [LTNovel] Support Latest Novels
Closes LNReader/lnreader-sources#431
2023-09-02 12:52:32 +05:30
Rajarshee Chatterjee c1cc32bcf1 fix(source): NovelTranslate (Closes LNReader/lnreader-sources#580) 2023-09-02 12:00:57 +05:30
Rajarshee Chatterjee 3b2ee3d78b refactor: Custom CSS/JS Modal (#705)
* refactor: Custom CSS/JS Modal

* Close Modal on Selecting a File
2023-09-02 11:06:07 +05:30
Rajarshee Chatterjee 929b183478 fix: Limit Custom CSS/JS Preview to 3 Lines (Closes #668) 2023-09-02 10:45:23 +05:30
Isaac H f53b0eac80 feat: Local EPUB Support (#754)
Closes #38 

* Move changes to master branch

* Fixed errors stemming from improper move

* Add EPub to library

* Extract everything in root folder, don't create subfolders

* made changes to epubparser.py

* changed json metadata file and added some safeguards for different epub formats

* Correctly read metadata and convert relative paths

* Corrected path for EPUB3

* fixed cover not showing up

* Get missing chapters names

---------

Co-authored-by: Danisty <danisty.nori@gmail.com>
2023-09-02 10:25:12 +05:30
Rajarshee Chatterjee 8258059adb Wrap Stats Cards (Closes #749) 2023-08-19 11:22:30 +05:30
Rajarshee Chatterjee 66c9b80374 fix(source): FastNovel (Closes LNReader/lnreader-sources#558) 2023-08-19 10:55:59 +05:30
Rajarshee Chatterjee 490c9ee495 Show Unread Chapters in Reader Drawer (Closes #732) 2023-08-19 10:13:59 +05:30
Rajarshee Chatterjee 4cb6251409 Tweak Chapter Card (Closes #725) 2023-08-19 10:10:55 +05:30
Skoppy2 6349c26a49 fix(Source): Update Novelmt URL (#746)
* Done work modifing novelmt

* build.yml
2023-08-19 09:47:45 +05:30
Rajarshee Chatterjee f7f02dc179 fix: Always Save EPUB Options (Close #744) 2023-08-19 09:46:34 +05:30
Rajarshee Chatterjee 28b65e35d2 Refactor App Update Checker 2023-08-17 22:24:09 +05:30
Rajarshee Chatterjee be2cca88af Minor Fixes 2023-08-17 21:19:02 +05:30
Rajarshee Chatterjee 0c42e93238 fix: Restore Backup Getting
Closes #422
Closes #720
2023-08-17 21:03:12 +05:30
Rajarshee Chatterjee 895f7c7fbf Add Missing Dependencies 2023-08-17 18:43:14 +05:30
Rajarshee Chatterjee de816e5de4 Add "Export to EPUB" Back (#738)
* Add "Export to EPUB" Back

* Cleanup
2023-08-17 14:43:44 +05:30
Rajarshee Chatterjee fbc128c786 Refactor NavigationUtils 2023-08-17 08:40:55 +05:30
1-minute-to-midnight 8e0ec4ccf8 fix(source): Update Readwn URL (#726) 2023-08-08 00:36:25 +05:30
K1ngfish3r e092ad9ba8 fix(source): Update Kolnovel and Agitoon (#715)
* kolnovel fix draft

* kolnovel fix draft v2

* cleanup

* fix Agitoon landing page
2023-08-03 15:59:06 +05:30
Rajarshee Chatterjee 5b8cfebe3d Revert "Export to EPUB" and Release v1.1.17 (#703)
* Revert "feat: EPUB Export (#646)"

This reverts commit c0b4610910.

* Revert "Export to EPUB"

* Release v1.1.17

* Revert React Native to 0.69.9

* Fix Update Checker
2023-08-03 07:43:26 +05:30
alistair3149 e760bd1a9b fix(source): add all pages in the same chapter for Linovelib (#722) 2023-08-02 23:31:14 +05:30
danisty d260eb1b5e Performance fixes, general fixes and addition of features (#723)
* fix: Fixed images proxied through Cloudflare not loading

* feat: Add utility to render images from Cloudflare proxied sites

* feat(source): Add NOVA (Spanish source)

* fix(source): NovelasLigera display of images and improved display of chapters text

* fix(source): NovelasLigera properly parse metadata

* fix(source): YuukiTls display of novels

* fix(source): TuNovelaLigera missing Cloudflare bypass and not properly getting covers

* fix(source): Other significant changes related to the request of popular novels from sources with no more than 1 page

* fix(source): NOVA not added to source manager

* fix: Remove loading Novel shimmer effect item when nothing is loading

* fix: Properly update chapters and metadata of novels that aren't saved in the local library

* fix: sanitizer doing unnecessary parsing which may add noticable delay to chapter loading

* refac: Add <center> tag to the sanitizer allowed tags

* fix: sanitizer being called each re-render of ReaderScreen plus unnecessary re-render of the WebView

* fix: memoized chapter html conversion to text for TTS, operation can be expensive

* refactor: Moved cloudflareImagesBypass to sources/helpers and fixed NOVA novels summary format
2023-08-02 23:30:44 +05:30
alistair3149 d59f95f13a fix(source): bypass image CDN in Linovelib (#716)
* fix(source): bypass image CDN in Linovelib

* fix(source): also check for src attribute in img in Linovelib
2023-07-31 09:07:32 +05:30
alistair3149 0d9da26eff fix(source): fix various lazyload issue with Linovelib (#714)
* fix(source): load lazyloaded images in Linovelib

* fix(source): recover original text in Linovelib
2023-07-29 20:53:51 +05:30
alistair3149 a769110c70 fix(source): fix incorrect property name (#713) 2023-07-28 10:35:47 +05:30
alistair3149 71ea318503 refactor(source): Use fetchHtml for Linovelib (#710)
* refactor(source): Use fetchHtml for Linovelib

* Update src/sources/ch/linovelib.js

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2023-07-28 09:46:12 +05:30
alistair3149 59d5f08c83 feat(source): Add Linovelib (#709)
* feat(source): Add Linovelib

* fix(source): use fetchHtml for searchNovels in Linovelib
2023-07-27 09:34:00 +05:30
Rajarshee Chatterjee 36230cebea fix(source): Update Ranobes URL 2023-07-27 08:34:14 +05:30
Rajarshee Chatterjee cbee29ea85 fix: Highlight on Novel Selection 2023-07-26 07:53:20 +05:30
Rhino 9b5a44fe59 fix(source): Update Ranobes URL (#690)
Update Ranobes URL
2023-07-16 17:29:21 +05:30
Rajarshee Chatterjee 9f87edaa7c build: Fix Artifact Name 2023-07-15 14:36:33 +05:30
Rajarshee Chatterjee e976c9793c Release v1.1.16 2023-07-15 13:21:46 +05:30
169 changed files with 9471 additions and 3377 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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'
+3 -1
View File
@@ -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>
+13
View File
@@ -0,0 +1,13 @@
# Common Problems with First Setup
### SDK Location not found. Define location with an ANDROID_SDK_ROOT environment variable. (expo modules error)
If you're on windows, you'll need to edit your system (system wide)/user (just you) environment variables. You'll want to add an environment variable named **ANDROID_SDK_ROOT** (legacy env variable name) and **ANDROID_HOME** that points to your android studio's sdk location. It'll most likely look something like this:
C:\Users\\**Username**\AppData\Local\Android\Sdk (copy paste this into the box)
Delete your node_modules folder, invalidate caches and restart in android studio and restart your machine. Follow the steps again in the [setup section](./CONTRIBUTING.md#setup).
### '.' is not recognized as an internal or external command, operable program or batch file. (npm run buildRelease error)
Run this line in the terminal instead: ```cd android ; ./gradlew clean ; ./gradlew assembleRelease```
+9 -4
View File
@@ -8,9 +8,10 @@ After forking to your own github org or account, do the following steps to get s
```bash
# prerequisites
node --version <= 16.13.1 (for version management, get nvm [recommended])
java sdk --version <= 11 (for version management, get jenv [optional])
android sdk (https://developer.android.com/studio)
node --version <= 16.13.1 (for version management, get nvm [recommended])
java sdk --version <= 17 (for version management, get jenv [optional])
android sdk (https://developer.android.com/studio)
python --version <= 3.11
# clone your fork to your local machine
git clone https://github.com/<your-account-name>/lnreader.git
@@ -25,6 +26,10 @@ npm install
npm run buildRelease
```
### Common Problems with First Setup
Please refer to this [file](./COMMON_ERRORS.md)
### Developing on Android
You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
@@ -56,4 +61,4 @@ codebase, however you can always check to see if the source code is compliant by
```bash
npm run lint
```
```
+17 -2
View File
@@ -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 {
+1
View File
@@ -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
}
}
@@ -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();
}
}
+268
View File
@@ -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
+2 -1
View File
@@ -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
}
+718 -709
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "lnreader",
"version": "1.1.15",
"version": "1.1.18",
"private": true,
"scripts": {
"android": "react-native run-android",
+45 -4
View File
@@ -1,21 +1,45 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTheme } from '@hooks/useTheme';
interface ErrorScreenProps {
error: string;
onRetry?: () => void;
retryIconColor?: string;
actions?: Array<{
iconName: string;
title: string;
onPress: () => void;
}>;
}
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error }) => {
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, actions }) => {
const theme = useTheme();
return (
<View style={styles.container}>
<Text style={[styles.icon, { color: theme.outline }]}>ಥ_ಥ</Text>
<Text style={[styles.error, { color: theme.outline }]}>{error}</Text>
{actions?.length ? (
<View style={styles.actionsCtn}>
{actions.map(action => (
<View style={styles.buttonWrapper}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
onPress={action.onPress}
style={styles.buttonCtn}
>
<MaterialCommunityIcons
name={action.iconName}
size={24}
color={theme.outline}
/>
<Text style={{ color: theme.outline }}>{action.title}</Text>
</Pressable>
</View>
))}
</View>
) : null}
</View>
);
};
@@ -36,4 +60,21 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
textAlign: 'center',
},
buttonWrapper: {
overflow: 'hidden',
borderRadius: 50,
marginHorizontal: 4,
flexDirection: 'row',
flex: 1 / 3,
},
buttonCtn: {
flex: 1,
paddingVertical: 8,
alignItems: 'center',
justifyContent: 'center',
},
actionsCtn: {
marginTop: 20,
flexDirection: 'row',
},
});
+1 -1
View File
@@ -81,7 +81,7 @@ const InfoItem = ({
theme,
}: {
title: string;
icon: string;
icon?: string;
theme: ThemeColors;
}) => (
<View style={styles.infoCtn}>
+3
View File
@@ -4,6 +4,8 @@ import { StyleSheet, View, Text, Pressable } from 'react-native';
import FastImage from 'react-native-fast-image';
import { coverPlaceholderColor } from '../theme/colors';
import { defaultUserAgentString } from '@utils/fetch/fetch';
import color from 'color';
const ListView = ({
@@ -31,6 +33,7 @@ const ListView = ({
<FastImage
source={{
uri: item.novelCover,
headers: { 'User-Agent': defaultUserAgentString },
}}
style={[styles.extensionIcon, inLibraryBadge && { opacity: 0.5 }]}
/>
+31 -27
View File
@@ -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,
+9 -7
View File
@@ -20,6 +20,8 @@ import { SourceNovelItem } from 'src/sources/types';
import { ThemeColors } from '@theme/types';
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
import { defaultUserAgentString } from '@utils/fetch/fetch';
interface NovelCoverProps {
item: LibraryNovelInfo;
onPress: () => void;
@@ -75,9 +77,9 @@ const NovelCover: React.FC<NovelCoverProps> = ({
},
styles.standardNovelCover,
isSelected && {
backgroundColor: theme.primary,
} &&
styles.selectedNovelCover,
backgroundColor: theme.primary,
opacity: 0.8,
},
]}
>
<Pressable
@@ -108,7 +110,10 @@ const NovelCover: React.FC<NovelCoverProps> = ({
)}
</View>
<FastImage
source={{ uri }}
source={{
uri,
headers: { 'User-Agent': defaultUserAgentString },
}}
style={[
{
height: coverHeight,
@@ -348,9 +353,6 @@ const styles = StyleSheet.create({
overflow: 'hidden',
margin: 2,
},
selectedNovelCover: {
opacity: 0.8,
},
compactTitle: {
color: 'rgba(255,255,255,1)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
+2 -1
View File
@@ -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) {
+25 -25
View File
@@ -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,
},
});
+1
View File
@@ -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';
+2 -2
View File
@@ -132,9 +132,9 @@ export const restoreLibrary = async novel => {
if (chapters.length) {
await insertChapters(insertId, chapters);
resolve();
}
resolve();
},
(txObj, error) => {
resolve();
-57
View File
@@ -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;
}
};
+1
View File
@@ -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';
-7
View File
@@ -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,
};
+65
View File
@@ -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;
};
-64
View File
@@ -1,64 +0,0 @@
import CookieManager from '@react-native-cookies/cookies';
import { useMMKVObject } from 'react-native-mmkv';
import { MMKVStorage } from '@utils/mmkv/mmkv';
import { showToast } from './showToast';
export const SOURCE_STORAGE = 'SOURCE_STORAGE';
interface SourceStorage {
cookies?: any;
}
export type SourceStorageMap = Record<number, Partial<SourceStorage>>;
const useSourceStorage = ({ sourceId = -1 }: { sourceId?: number }) => {
const [values, setValues] = useMMKVObject<SourceStorageMap>(
SOURCE_STORAGE,
MMKVStorage,
);
const setSourceStorage = (key: keyof SourceStorage, value: any): void => {
setValues({
...values,
[sourceId]: {
...values?.[sourceId],
[key]: value,
},
});
};
const clearCookies = (): void => {
// let tempStorage = {};
// if (values) {
// tempStorage = Object.fromEntries(
// Object.entries(values).map(([id, sourceData]) => {
// delete sourceData.cookies;
// return [id, sourceData];
// }),
// );
// }
CookieManager.clearAll();
CookieManager.clearAll(true); //clears cookies in webkit
setValues({});
showToast('Cookies cleared');
};
const sourceStorage = values?.[sourceId];
return {
...sourceStorage,
setSourceStorage,
clearCookies,
};
};
export default useSourceStorage;
export const getSourceStorage = (sourceId: number) => {
const rawSettings = MMKVStorage.getString(SOURCE_STORAGE) || '{}';
const parsedSettings: Partial<SourceStorageMap> = JSON.parse(rawSettings);
return parsedSettings[sourceId] || {};
};
+21
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 => ({
+12 -1
View File
@@ -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,
});
};
+11 -1
View File
@@ -1,5 +1,9 @@
import { Dimensions } from 'react-native';
import { SET_READER_SETTINGS, SET_APP_SETTINGS } from './settings.types';
import {
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_MULTIPLE_APP_SETTINGS,
} from './settings.types';
/**
* Display Mode
@@ -63,6 +67,7 @@ export const initialState = {
fullScreenMode: true,
swipeGestures: false,
readerPages: false,
showScrollPercentage: true,
useVolumeButtons: false,
scrollAmount: Math.round(Dimensions.get('window').height),
@@ -105,6 +110,11 @@ const settingsReducer = (state = initialState, action) => {
[payload.key]: payload.val,
},
};
case SET_MULTIPLE_APP_SETTINGS:
return {
...state,
...payload,
};
default:
return state;
}
+1
View File
@@ -1,2 +1,3 @@
export const SET_APP_SETTINGS = 'SET_APP_SETTINGS';
export const SET_READER_SETTINGS = 'SET_READER_SETTINGS';
export const SET_MULTIPLE_APP_SETTINGS = 'SET_MULTIPLE_APP_SETTINGS';
@@ -52,6 +52,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
filterValues,
setFilters,
clearFilters,
refetchNovels,
} = useBrowseSource(sourceId, showLatestNovels);
const { defaultCategoryId = 1 } = useCategorySettings();
@@ -126,10 +127,18 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
) : errorMessage || novelList.length === 0 ? (
<ErrorScreenV2
error={errorMessage || getString('sourceScreen.noResultsFound')}
actions={[
{
iconName: 'refresh',
title: getString('common.retry'),
onPress: refetchNovels,
},
]}
/>
) : (
<NovelList
data={novelList}
isFetchingNextPage={hasNextPage && !searchText}
inSource
renderItem={({ item }) => {
const inLibrary = novelInLibrary(item.novelUrl);
@@ -55,6 +55,9 @@ export const useBrowseSource = (
}, [fetchNovels, currentPage]);
const refetchNovels = () => {
isScreenMounted.current = true;
setError('');
setIsLoading(true);
setCurrentPage(1);
fetchNovels(1);
};
@@ -8,6 +8,8 @@ import { SourceNovelItem } from '../../../sources/types';
import { ThemeColors } from '../../../theme/types';
import { getString } from '@strings/translations';
import { defaultUserAgentString } from '@utils/fetch/fetch';
interface Props {
novel: SourceNovelItem;
navigateToNovel: (novel: SourceNovelItem) => void;
@@ -41,7 +43,10 @@ const GlobalSearchNovelItem: React.FC<Props> = ({
onLongPress={onLongPress}
>
<FastImage
source={{ uri: novel.novelCover }}
source={{
uri: novel.novelCover,
headers: { 'User-Agent': defaultUserAgentString },
}}
style={[styles.novelCover, { ...novelItemDimensions }]}
/>
{inLibrary ? (
+19 -19
View File
@@ -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',
},
+6 -13
View File
@@ -1,11 +1,10 @@
import React, { useEffect } from 'react';
import React from 'react';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import WebView from 'react-native-webview';
import CookieManager from '@react-native-cookies/cookies';
import { Appbar } from '@components';
import { useTheme } from '@hooks/useTheme';
import useSourceStorage from '@hooks/useSourceStorage';
import { defaultUserAgentString } from '@utils/fetch/fetch';
type ReaderScreenRouteProps = RouteProp<{
@@ -21,19 +20,12 @@ const WebviewScreen = () => {
const { goBack } = useNavigation();
const {
params: { name, sourceId, url },
params: { name, url },
} = useRoute<ReaderScreenRouteProps>();
const { setSourceStorage } = useSourceStorage({ sourceId });
useEffect(() => {
CookieManager.get(url, true).then(cookies => {
const cloudflareCookie = cookies?.cf_clearance;
if (cloudflareCookie) {
const cloudflareCookieString = `${cloudflareCookie.name}=${cloudflareCookie.value}`;
setSourceStorage('cookies', cloudflareCookieString);
}
});
}, []);
const syncCookies = () => {
CookieManager.flush();
};
return (
<>
@@ -42,6 +34,7 @@ const WebviewScreen = () => {
startInLoadingState
userAgent={defaultUserAgentString}
source={{ uri: url }}
onNavigationStateChange={syncCookies}
/>
</>
);
@@ -10,7 +10,7 @@ import { showToast } from '../../../hooks/showToast';
import { Button } from '@components';
import { getString } from '@strings/translations';
import { openNovel } from '@utils/handleNavigateParams';
import { getNovelScreenRouteParams } from '@utils/NavigationUtils';
const MigrationNovelList = ({ data, theme, library, navigation }) => {
const [selectedNovel, setSelectedNovel] = useState(false);
@@ -30,7 +30,7 @@ const MigrationNovelList = ({ data, theme, library, navigation }) => {
onLongPress={() =>
navigation.navigate(
'Novel',
openNovel({
getNovelScreenRouteParams({
...item,
sourceId: item.sourceId,
}),
+11 -12
View File
@@ -16,13 +16,12 @@ import { History } from '../../database/types';
import { getString } from '@strings/translations';
import ClearHistoryDialog from './components/ClearHistoryDialog';
import {
openChapter,
openChapterChapterTypes,
openChapterNovelTypes,
openChapterFunctionTypes,
openNovel,
openNovelProps,
} from '@utils/handleNavigateParams';
ChapterRouteParams,
NovelRouteParams,
getNovelScreenRouteParams,
getChapterScreenRouteParams,
NovelScreenRouteParams,
} from '@utils/NavigationUtils';
import HistorySkeletonLoading from './components/HistorySkeletonLoading';
const HistoryScreen = () => {
@@ -76,16 +75,16 @@ const HistoryScreen = () => {
};
const handleNavigateToChapter = (
novel: openChapterNovelTypes,
chapter: openChapterChapterTypes,
novel: NovelRouteParams,
chapter: ChapterRouteParams,
) =>
navigate(
'Chapter' as never,
openChapter(novel, chapter) as openChapterFunctionTypes as never,
getChapterScreenRouteParams(novel, chapter) as never,
);
const handleNavigateToNovel = (novel: openNovelProps) =>
navigate('Novel' as never, openNovel(novel) as openNovelProps as never);
const handleNavigateToNovel = (novel: NovelScreenRouteParams) =>
navigate('Novel' as never, getNovelScreenRouteParams(novel) as never);
const {
value: clearHistoryDialogVisible,
@@ -11,19 +11,21 @@ import { History } from '@database/types';
import { ThemeColors } from '@theme/types';
import { coverPlaceholderColor } from '@theme/colors';
import {
openChapterChapterTypes,
openChapterNovelTypes,
openNovelProps,
} from '@utils/handleNavigateParams';
ChapterRouteParams,
NovelRouteParams,
NovelScreenRouteParams,
} from '@utils/NavigationUtils';
import { defaultUserAgentString } from '@utils/fetch/fetch';
interface HistoryCardProps {
history: History;
handleNavigateToChapter: (
novel: openChapterNovelTypes,
chapter: openChapterChapterTypes,
novel: NovelRouteParams,
chapter: ChapterRouteParams,
) => void;
handleRemoveFromHistory: (historyId: number) => void;
handleNavigateToNovel: (novel: openNovelProps) => void;
handleNavigateToNovel: (novel: NovelScreenRouteParams) => void;
theme: ThemeColors;
}
@@ -85,7 +87,15 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
})
}
>
<FastImage source={{ uri: novelCover }} style={styles.cover} />
<FastImage
source={{
uri: novelCover,
headers: {
'User-Agent': defaultUserAgentString,
},
}}
style={styles.cover}
/>
</Pressable>
<View style={styles.detailsContainer}>
<Text
+2 -2
View File
@@ -31,7 +31,7 @@ import SetCategoryModal from '@screens/novel/components/SetCategoriesModal';
import useBoolean from '@hooks/useBoolean';
import { debounce, intersection } from 'lodash-es';
import { useBackHandler } from '@hooks/useBackHandler';
import { openChapter } from '@utils/handleNavigateParams';
import { getChapterScreenRouteParams } from '@utils/NavigationUtils';
import useHistory from '@hooks/useHistory';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSettings } from '@hooks/reduxHooks';
@@ -259,7 +259,7 @@ const LibraryScreen = () => {
onPress={() => {
navigate(
'Chapter' as never,
openChapter(history[0], history[0]) as never,
getChapterScreenRouteParams(history[0], history[0]) as never,
);
}}
/>
@@ -15,7 +15,7 @@ import { getString } from '@strings/translations';
import { useAppDispatch } from '@redux/hooks';
import { useTheme } from '@hooks/useTheme';
import { updateLibraryAction } from '@redux/updates/updates.actions';
import { openNovel } from '@utils/handleNavigateParams';
import { getNovelScreenRouteParams } from '@utils/NavigationUtils';
interface Props {
categoryId: number;
@@ -46,7 +46,7 @@ export const LibraryView: React.FC<Props> = ({
if (selectedNovelIds.length) {
setSelectedNovelIds(xor(selectedNovelIds, [item.novelId]));
} else {
navigate('Novel' as never, openNovel(item) as never);
navigate('Novel' as never, getNovelScreenRouteParams(item) as never);
dispatch(setNovel(item));
}
}}
+133 -135
View File
@@ -31,144 +31,142 @@ const MoreScreen = ({ navigation }) => {
setLibrarySettings({ incognitoMode: !incognitoMode });
return (
<>
<MoreHeader title="More" navigation={navigation} theme={theme} />
<ScrollView style={{ flex: 1 }}>
<List.Section>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 14,
flexDirection: 'row',
justifyContent: 'space-between',
}}
onPress={enableDownloadedOnlyMode}
>
<View style={{ flexDirection: 'row' }}>
<List.Icon theme={theme} icon="cloud-off-outline" />
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.onSurface,
fontSize: 16,
}}
>
Downloaded only
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>
Filters all novels in your library
</Text>
</View>
<ScrollView>
<MoreHeader navigation={navigation} theme={theme} />
<List.Section>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 14,
flexDirection: 'row',
justifyContent: 'space-between',
}}
onPress={enableDownloadedOnlyMode}
>
<View style={{ flexDirection: 'row' }}>
<List.Icon theme={theme} icon="cloud-off-outline" />
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.onSurface,
fontSize: 16,
}}
>
Downloaded only
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>
Filters all novels in your library
</Text>
</View>
<Switch
value={downloadedOnlyMode}
onValueChange={enableDownloadedOnlyMode}
color={theme.primary}
style={{ marginRight: 8 }}
/>
</Pressable>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 14,
flexDirection: 'row',
justifyContent: 'space-between',
}}
onPress={enableIncognitoMode}
>
<View style={{ flexDirection: 'row' }}>
<List.Icon theme={theme} icon="incognito" />
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.onSurface,
fontSize: 16,
}}
>
Incognito mode
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>
Pauses reading history
</Text>
</View>
</View>
<Switch
value={downloadedOnlyMode}
onValueChange={enableDownloadedOnlyMode}
color={theme.primary}
style={{ marginRight: 8 }}
/>
</Pressable>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 14,
flexDirection: 'row',
justifyContent: 'space-between',
}}
onPress={enableIncognitoMode}
>
<View style={{ flexDirection: 'row' }}>
<List.Icon theme={theme} icon="incognito" />
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.onSurface,
fontSize: 16,
}}
>
Incognito mode
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>
Pauses reading history
</Text>
</View>
<Switch
value={incognitoMode}
onValueChange={enableIncognitoMode}
color={theme.primary}
style={{ marginRight: 8 }}
/>
</Pressable>
<List.Divider theme={theme} />
<List.Item
title="Download queue"
description={
downloadQueue.length > 0 && downloadQueue.length + ' remaining'
}
icon="progress-download"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'DownloadQueue',
})
}
theme={theme}
</View>
<Switch
value={incognitoMode}
onValueChange={enableIncognitoMode}
color={theme.primary}
style={{ marginRight: 8 }}
/>
<List.Item
title="Downloads"
icon="folder-download"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'Downloads',
})
}
theme={theme}
/>
<List.Item
title={getString('common.categories')}
icon="label-outline"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'Categories',
})
}
theme={theme}
/>
<List.Item
title={getString('statsScreen.title')}
icon="chart-line"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'Statistics',
})
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.Item
title={getString('moreScreen.settings')}
icon="cog-outline"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'SettingsStack',
})
}
theme={theme}
/>
<List.Item
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'About',
})
}
theme={theme}
/>
</List.Section>
</ScrollView>
</>
</Pressable>
<List.Divider theme={theme} />
<List.Item
title="Download queue"
description={
downloadQueue.length > 0 && downloadQueue.length + ' remaining'
}
icon="progress-download"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'DownloadQueue',
})
}
theme={theme}
/>
<List.Item
title="Downloads"
icon="folder-download"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'Downloads',
})
}
theme={theme}
/>
<List.Item
title={getString('common.categories')}
icon="label-outline"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'Categories',
})
}
theme={theme}
/>
<List.Item
title={getString('statsScreen.title')}
icon="chart-line"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'Statistics',
})
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.Item
title={getString('moreScreen.settings')}
icon="cog-outline"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'SettingsStack',
})
}
theme={theme}
/>
<List.Item
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate('MoreStack', {
screen: 'About',
})
}
theme={theme}
/>
</List.Section>
</ScrollView>
);
};
+94 -40
View File
@@ -8,7 +8,14 @@ import {
Text,
} from 'react-native';
import { FlashList } from '@shopify/flash-list';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import Animated, {
FadeIn,
FadeOut,
useAnimatedStyle,
useSharedValue,
withTiming,
interpolateColor,
} from 'react-native-reanimated';
import {
Provider,
@@ -16,8 +23,8 @@ import {
Appbar,
IconButton,
Menu,
FAB,
Snackbar,
AnimatedFAB,
} from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import * as Haptics from 'expo-haptics';
@@ -49,18 +56,19 @@ import NovelInfoHeader from './components/Info/NovelInfoHeader';
import NovelBottomSheet from './components/NovelBottomSheet';
import TrackSheet from './components/Tracker/TrackSheet';
import { Row } from '../../components/Common';
import JumpToChapterModal from './components/Modal/JumpToChapterModal';
import JumpToChapterModal from './components/JumpToChapterModal';
import { Actionbar } from '../../components/Actionbar/Actionbar';
import EditInfoModal from './components/Modal/EditInfoModal';
import EditInfoModal from './components/EditInfoModal';
import { pickCustomNovelCover } from '../../database/queries/NovelQueries';
import DownloadCustomChapterModal from './components/Modal/DownloadCustomChapterModal';
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import useBoolean from '@hooks/useBoolean';
import { useCategorySettings } from '@hooks/useSettings';
import { openChapter } from '../../utils/handleNavigateParams';
import { getChapterScreenRouteParams } from '../../utils/NavigationUtils';
import NovelScreenLoading from './components/LoadingAnimation/NovelScreenLoading';
import { useTrackerReducer } from '@redux/hooks';
import EpubIconButton from './components/EpubIconButton';
import { uniqBy } from 'lodash-es';
const Novel = ({ route, navigation }) => {
const item = route.params;
@@ -78,6 +86,7 @@ const Novel = ({ route, navigation }) => {
const [selected, setSelected] = useState([]);
const [downloadMenu, showDownloadMenu] = useState(false);
const [extraMenu, showExtraMenu] = useState(false);
const [isFabExtended, setIsFabExtended] = useState(true);
let flatlistRef = useRef(null);
let novelBottomSheetRef = useRef(null);
@@ -89,6 +98,7 @@ const Novel = ({ route, navigation }) => {
useFabForContinueReading = false,
defaultChapterSort = 'ORDER BY chapterId ASC',
disableHapticFeedback = false,
jumpToLastReadChapter = false,
} = useSettings();
const {
@@ -104,6 +114,25 @@ const Novel = ({ route, navigation }) => {
const { defaultCategoryId = 1 } = useCategorySettings();
const headerOpacity = useSharedValue(0);
const headerOpacityStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
headerOpacity.value,
[0, 1],
['transparent', theme.surface],
);
return {
backgroundColor,
};
});
const onPageScroll = event => {
const y = event.nativeEvent.contentOffset.y;
headerOpacity.value = withTiming(y > 10 ? 1 : 0, { duration: 200 });
const currentScrollPosition = Math.floor(y) ?? 0;
setIsFabExtended(currentScrollPosition <= 0);
};
useEffect(() => {
dispatch(
getNovelAction(
@@ -129,8 +158,8 @@ const Novel = ({ route, navigation }) => {
progressViewOffset={progressViewOffset}
onRefresh={onRefresh}
refreshing={updating}
colors={[theme.primary]}
progressBackgroundColor={theme.onPrimary}
colors={[theme.onPrimary]}
progressBackgroundColor={theme.primary}
/>
);
@@ -267,25 +296,37 @@ const Novel = ({ route, navigation }) => {
if (lastSelectedChapter.chapterId !== chapter.chapterId) {
if (lastSelectedChapter.chapterId > chapter.chapterId) {
setSelected(sel => [
...sel,
chapter,
...chapters.filter(
chap =>
(chap.chapterId <= chapter.chapterId ||
chap.chapterId >= lastSelectedChapter.chapterId) === false,
setSelected(sel =>
uniqBy(
[
...sel,
chapter,
...chapters.filter(
chap =>
(chap.chapterId <= chapter.chapterId ||
chap.chapterId >= lastSelectedChapter.chapterId) ===
false,
),
],
'chapterId',
),
]);
);
} else {
setSelected(sel => [
...sel,
chapter,
...chapters.filter(
chap =>
(chap.chapterId >= chapter.chapterId ||
chap.chapterId <= lastSelectedChapter.chapterId) === false,
setSelected(sel =>
uniqBy(
[
...sel,
chapter,
...chapters.filter(
chap =>
(chap.chapterId >= chapter.chapterId ||
chap.chapterId <= lastSelectedChapter.chapterId) ===
false,
),
],
'chapterId',
),
]);
);
}
}
}
@@ -294,7 +335,7 @@ const Novel = ({ route, navigation }) => {
const navigateToChapter = chapter => {
navigation.navigate(
'Chapter',
openChapter({ sourceId, novelUrl, novelName }, chapter),
getChapterScreenRouteParams({ sourceId, novelUrl, novelName }, chapter),
);
};
@@ -340,7 +381,6 @@ const Novel = ({ route, navigation }) => {
const renderItem = ({ item: it, index }) => (
<ChapterItem
theme={theme}
chapter={it}
index={index}
showChapterTitles={showChapterTitles}
@@ -367,7 +407,7 @@ const Novel = ({ route, navigation }) => {
<View style={[styles.container, { backgroundColor: theme.background }]}>
<Portal>
{selected.length === 0 ? (
<View style={styles.row}>
<Animated.View style={[styles.row, headerOpacityStyle]}>
<IconButton
icon="arrow-left"
iconColor={theme.onBackground}
@@ -398,7 +438,11 @@ const Novel = ({ route, navigation }) => {
}}
onPress={() => showJumpToChapterModal(true)}
/>
<EpubIconButton
theme={theme}
style={{ marginTop: StatusBar.currentHeight + 8 }}
novel={novel}
/>
<Menu
visible={downloadMenu}
onDismiss={() => showDownloadMenu(false)}
@@ -531,16 +575,13 @@ const Novel = ({ route, navigation }) => {
titleStyle={{
color: theme.onSurface,
}}
onPress={() =>
dispatch(deleteAllChaptersAction(sourceId, chapters))
}
onPress={() => {
dispatch(deleteAllChaptersAction(sourceId, chapters));
showDownloadMenu(false);
}}
/>
</Menu>
<EpubIconButton
theme={theme}
style={{ marginTop: StatusBar.currentHeight + 8 }}
novel={novel}
/>
<Menu
visible={extraMenu}
onDismiss={() => showExtraMenu(false)}
@@ -578,7 +619,7 @@ const Novel = ({ route, navigation }) => {
/>
</Menu>
</Row>
</View>
</Animated.View>
) : (
<Animated.View
entering={FadeIn.duration(150)}
@@ -611,6 +652,18 @@ const Novel = ({ route, navigation }) => {
</Portal>
<View style={styles.flashlistContainer}>
<FlashList
onLayout={() => {
if (jumpToLastReadChapter) {
const lastReadChapterIdx = chapters.findIndex(
chapter => chapter.chapterId === lastReadChapter.chapterId,
);
flatlistRef.current.scrollToIndex({
animated: true,
index: lastReadChapterIdx,
});
}
}}
ref={flatlistRef}
estimatedItemSize={64}
data={chapters}
@@ -638,16 +691,17 @@ const Novel = ({ route, navigation }) => {
deleteDownloadsSnackbar={deleteDownloadsSnackbar}
/>
}
onScroll={onPageScroll}
refreshControl={refreshControl()}
/>
</View>
{useFabForContinueReading && chapters.length > 0 && lastReadChapter && (
<FAB
<AnimatedFAB
style={[
styles.fab,
{ backgroundColor: theme.primary, marginBottom: bottomInset },
]}
small
extended={isFabExtended}
color={theme.onPrimary}
uppercase={false}
label={novel.unread ? 'Start' : 'Resume'}
@@ -655,7 +709,7 @@ const Novel = ({ route, navigation }) => {
onPress={() => {
navigation.navigate(
'Chapter',
openChapter(novel, lastReadChapter),
getChapterScreenRouteParams(novel, lastReadChapter),
);
}}
/>
@@ -1,6 +1,6 @@
import { ChapterItemExtended } from '@database/types';
import { MD3ThemeType } from '@theme/types';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { IconButton, Menu, overlay } from 'react-native-paper';
@@ -27,6 +27,9 @@ export const DownloadButton: React.FC<DownloadButtonProps> = ({
deleteChapterMenuVisible,
}) => {
const [downloaded, setDownloaded] = useState(chapter.downloaded);
// React to external changes of chapter download state
useEffect(() => setDownloaded(chapter.downloaded ? 1 : 0), [chapter]);
if (downloadQueue.some(chap => chap.chapterId === chapter.chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (downloaded === 0) {
+60 -59
View File
@@ -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,
},
});
@@ -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',
},
});
@@ -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,
@@ -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';
+76 -55
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import React, { useMemo } from 'react';
import useBoolean from '@hooks/useBoolean';
import { IconButton, Portal } from 'react-native-paper';
import ChooseEpubLocationModal from './Modal/ChooseEpubLocationModal';
import ChooseEpubLocationModal from './ChooseEpubLocationModal';
import { StatusBar, StyleProp } from 'react-native';
import { IconProps } from 'react-native-paper/lib/typescript/src/components/MaterialCommunityIcon';
import { ThemeColors } from '@theme/types';
@@ -27,7 +27,11 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
style,
novel,
}) => {
const chooseEpubLocationModal = useBoolean(false);
const {
value: isVisible,
setTrue: showModal,
setFalse: hideModal,
} = useBoolean(false);
const readerSettings = useReaderSettings();
const {
epubUseAppTheme = false,
@@ -35,56 +39,73 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
epubUseCustomJS = false,
} = useSettings();
const epubStyle = `${
epubUseAppTheme
? `html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
word-wrap: break-word;
}
body {
padding-left: ${readerSettings.padding}%;
padding-right: ${readerSettings.padding}%;
padding-bottom: 40px;
font-size: ${readerSettings.textSize}px;
color: ${readerSettings.textColor};
text-align: ${readerSettings.textAlign};
line-height: ${readerSettings.lineHeight};
font-family: "${readerSettings.fontFamily}";
background-color: "${readerSettings.theme}";
}
hr {
margin-top: 20px;
margin-bottom: 20px;
}
a {
color: ${theme.primary};
}
img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}`
: ''
}
${
epubUseCustomCSS
? readerSettings.customCSS
.replace(RegExp(`\#sourceId-${novel.sourceId}\\s*\\{`, 'g'), 'body {')
.replace(RegExp(`\#sourceId-${novel.sourceId}[^\.\#A-Z]*`, 'gi'), '')
: ''
}`;
const epubStyle = useMemo(
() =>
`${
epubUseAppTheme
? `
html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
word-wrap: break-word;
}
body {
padding-left: ${readerSettings.padding}%;
padding-right: ${readerSettings.padding}%;
padding-bottom: 40px;
font-size: ${readerSettings.textSize}px;
color: ${readerSettings.textColor};
text-align: ${readerSettings.textAlign};
line-height: ${readerSettings.lineHeight};
font-family: "${readerSettings.fontFamily}";
background-color: "${readerSettings.theme}";
}
hr {
margin-top: 20px;
margin-bottom: 20px;
}
a {
color: ${theme.primary};
}
img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}`
: ''
}
${
epubUseCustomCSS
? readerSettings.customCSS
.replace(
RegExp(`\#sourceId-${novel.sourceId}\\s*\\{`, 'g'),
'body {',
)
.replace(
RegExp(`\#sourceId-${novel.sourceId}[^\.\#A-Z]*`, 'gi'),
'',
)
: ''
}`,
[novel, epubUseAppTheme, readerSettings, epubUseCustomCSS],
);
const epubJS = `let novelName = "${novel.novelName}";
let chapterName = "";
let sourceId =${novel.sourceId};
let chapterId ="";
let novelId =${novel.novelId};
let html = document.querySelector("chapter").innerHTML;
${readerSettings.customJS}
`;
const epubJS = useMemo(
() =>
`
let novelName = "${novel.novelName}";
let chapterName = "";
let sourceId =${novel.sourceId};
let chapterId ="";
let novelId =${novel.novelId};
let html = document.querySelector("chapter").innerHTML;
${readerSettings.customJS}
`,
[novel, readerSettings],
);
const createEpub = async (uri: string) => {
var epub = new EpubBuilder(
@@ -130,12 +151,12 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
iconColor={theme.onBackground}
size={21}
style={style}
onPress={chooseEpubLocationModal.setTrue}
onPress={showModal}
/>
<Portal>
<ChooseEpubLocationModal
modalVisible={chooseEpubLocationModal.value}
hideModal={chooseEpubLocationModal.setFalse}
isVisible={isVisible}
hideModal={hideModal}
onSubmit={createEpub}
/>
</Portal>
@@ -8,6 +8,7 @@ import {
Dimensions,
StatusBar,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import color from 'color';
import { IconButton, Portal } from 'react-native-paper';
import { LinearGradient } from 'expo-linear-gradient';
@@ -16,6 +17,7 @@ import FastImage from 'react-native-fast-image';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
import { Chip } from '../../../../components';
import { coverPlaceholderColor } from '../../../../theme/colors';
import { showToast } from '@hooks/showToast';
const NovelInfoContainer = ({ children }) => (
<View style={styles.novelInfoContainer}>{children}</View>
@@ -69,17 +71,41 @@ const NovelThumbnail = ({ source, theme, setCustomNovelCover }) => {
} else {
return (
<Portal>
<IconButton
icon="pencil-outline"
<View
style={{
position: 'absolute',
top: StatusBar.currentHeight + 10,
right: 10,
zIndex: 10,
flexDirection: 'row',
}}
iconColor={theme.onBackground}
onPress={setCustomNovelCover}
/>
>
<IconButton
icon="pencil-outline"
iconColor={theme.onBackground}
onPress={setCustomNovelCover}
/>
<IconButton
icon="content-save-outline"
iconColor={theme.onBackground}
onPress={() => {
RNFetchBlob.config({
fileCache: true,
addAndroidDownloads: {
useDownloadManager: true,
notification: true,
path:
RNFetchBlob.fs.dirs.DownloadDir +
'/' +
source.uri.split('/').pop(),
description: 'Image',
},
})
.fetch('GET', source.uri)
.then(() => showToast('Cover saved'));
}}
/>
</View>
<Pressable
style={{
position: 'absolute',
@@ -11,6 +11,8 @@ import { followNovelAction } from '../../../../redux/novel/novel.actions';
import { useSettings } from '../../../../hooks/reduxHooks';
import { showToast } from '../../../../hooks/showToast';
import { defaultUserAgentString } from '@utils/fetch/fetch';
import {
CoverImage,
NovelAuthor,
@@ -57,13 +59,23 @@ const NovelInfoHeader = ({
return (
<>
<CoverImage
source={{ uri: novel.novelCover }}
source={{
uri: novel.novelCover,
headers: {
'User-Agent': defaultUserAgentString,
},
}}
theme={theme}
hideBackdrop={hideBackdrop}
>
<NovelInfoContainer>
<NovelThumbnail
source={{ uri: novel.novelCover }}
source={{
uri: novel.novelCover,
headers: {
'User-Agent': defaultUserAgentString,
},
}}
theme={theme}
setCustomNovelCover={setCustomNovelCover}
/>
@@ -1,5 +1,5 @@
import { useNavigation } from '@react-navigation/native';
import { openChapter } from '@utils/handleNavigateParams';
import { getChapterScreenRouteParams } from '@utils/NavigationUtils';
import React from 'react';
import { Button } from '@components';
@@ -11,7 +11,7 @@ const ReadButton = ({ novel, chapters, lastRead }) => {
const { useFabForContinueReading = false } = useSettings();
const navigateToLastReadChapter = () =>
navigate('Chapter', openChapter(novel, lastRead));
navigate('Chapter', getChapterScreenRouteParams(novel, lastRead));
if (!useFabForContinueReading) {
return (
@@ -34,12 +34,24 @@ const ChaptersSettingsSheet = ({
theme={theme}
label="Downloaded"
color={theme.primary}
status={filter.match('AND downloaded=1')}
onPress={() =>
status={
filter.match('AND downloaded=1')
? filterChapters(filter.replace(' AND downloaded=1', ''))
: filterChapters(filter + ' AND downloaded=1')
? true
: filter.match('AND downloaded=0')
? 'indeterminate'
: false
}
onPress={() => {
if (filter.match('AND downloaded=1')) {
filterChapters(
filter.replace(' AND downloaded=1', ' AND downloaded=0'),
);
} else if (filter.match('AND downloaded=0')) {
filterChapters(filter.replace(' AND downloaded=0', ''));
} else {
filterChapters(filter + ' AND downloaded=1');
}
}}
/>
<Checkbox
theme={theme}
@@ -1,6 +1,12 @@
import { getString } from '@strings/translations';
import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
LayoutAnimation,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { ThemeColors } from '../../../../theme/types';
@@ -22,6 +28,10 @@ const NovelSummary: React.FC<NovelSummaryProps> = ({
const [expanded, setExpanded] = useState(isExpanded);
const toggleExpanded = () => {
if (summary) {
LayoutAnimation.configureNext({
...LayoutAnimation.Presets.linear,
duration: 100,
});
setExpanded(!expanded);
}
};
@@ -38,8 +48,13 @@ const NovelSummary: React.FC<NovelSummaryProps> = ({
onPress={toggleExpanded}
>
<Text
style={[styles.summaryText, { color: textColor }]}
numberOfLines={expanded ? Number.MAX_SAFE_INTEGER : 3}
style={[
styles.summaryText,
{
color: textColor,
height: expanded ? undefined : styles.summaryText.lineHeight * 3,
},
]}
>
{summary || getString('novelScreen.noSummary')}
</Text>
+118 -32
View File
@@ -1,4 +1,10 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from 'react';
import { Dimensions, NativeModules, NativeEventEmitter } from 'react-native';
import VolumeButtonListener from './../../utils/volumeButtonListener';
@@ -40,6 +46,8 @@ import { createDrawerNavigator } from '@react-navigation/drawer';
import { htmlToText } from '../../sources/helpers/htmlToText';
import ChapterLoadingScreen from './ChapterLoadingScreen/ChapterLoadingScreen';
import { ErrorScreenV2 } from '@components';
import { getString } from '@strings/translations';
import { useNavigation } from '@react-navigation/native';
const Chapter = ({ route }) => {
const DrawerNav = createDrawerNavigator();
@@ -76,9 +84,11 @@ const ChapterContent = ({ route, navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { navigate } = useNavigation();
const {
swipeGestures = false,
readerPages = false,
useVolumeButtons = false,
autoScroll = false,
autoScrollInterval = 10,
@@ -96,6 +106,7 @@ const ChapterContent = ({ route, navigation }) => {
const position = usePosition(novelId, chapterId);
const minScroll = useRef(0);
const pages = useRef(0);
const { tracker, trackedNovels } = useTrackerReducer();
const isTracked = trackedNovels.find(obj => obj.novelId === novelId);
@@ -145,7 +156,7 @@ const ChapterContent = ({ route, navigation }) => {
emmiter.current.removeAllListeners('VolumeDown');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useVolumeButtons, scrollAmount]);
}, [useVolumeButtons, scrollAmount, chapter]);
const onLayout = useCallback(() => {
if (useVolumeButtons) {
@@ -195,22 +206,51 @@ const ChapterContent = ({ route, navigation }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chapter]);
const [ttsStatus, startTts] = useTextToSpeech(
htmlToText(chapter?.chapterText).split('\n'),
() => {
if (!incognitoMode) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
},
const htmlText = useMemo(
() => htmlToText(chapter?.chapterText).split('\n'),
[chapter.chapterText],
);
const [ttsStatus, startTts] = useTextToSpeech(htmlText, () => {
if (!incognitoMode) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
});
useEffect(() => {
if (!readerPages) {
webViewRef?.current?.injectJavaScript(
"document.querySelector('chapter').style.transform = 'translate(0%)';",
);
}
}, [readerPages]);
const scrollTo = useCallback(
offsetY => {
offset => {
requestAnimationFrame(() => {
webViewRef?.current?.injectJavaScript(`(()=>{
window.scrollTo({top:${offsetY},behavior:'smooth'})
})()`);
webViewRef?.current?.injectJavaScript(
!readerPages
? `(()=>{
window.scrollTo({top:${offset},behavior:'smooth'})
})()`
: `(()=>{
document.querySelector('chapter').setAttribute('data-page', ${
offset / 100
});
document.querySelector("chapter").style.transform = 'translate(-${offset}%)';
window.ReactNativeWebView.postMessage(
JSON.stringify(
{
type:"scrollend",
data:{
offSetY: ${offset},
percentage: (${offset / 100 / pages.current} > 0 ? ${
offset / 100 / pages.current
} * 100 : 1),
}
}
)
);
})()`,
);
});
},
[webViewRef],
@@ -268,13 +308,15 @@ const ChapterContent = ({ route, navigation }) => {
);
const hideHeader = useCallback(() => {
if (!hidden) {
setImmersiveMode();
} else {
showStatusAndNavBar();
}
setHidden(!hidden);
}, [hidden]);
setHidden(h => {
if (!h) {
setImmersiveMode();
} else {
showStatusAndNavBar();
}
return !h;
});
}, []);
const navigateToChapterBySwipe = useCallback(
name => {
@@ -313,29 +355,69 @@ const ChapterContent = ({ route, navigation }) => {
}
}, []);
const scrollToSavedProgress = () => scrollTo(position?.position);
useEffect(() => {
return () => {
if (position?.percentage === 100) {
doSaveProgress(0, 0);
}
};
}, [position?.percentage]);
const chapterText = sanitizeChapterText(chapter.chapterText, {
removeExtraParagraphSpacing,
bionicReading,
sourceId: sourceId,
});
const scrollToSavedProgress = useCallback(
() => scrollTo(position?.position),
[],
);
const chapterText = useMemo(
() =>
sanitizeChapterText(chapter.chapterText, {
removeExtraParagraphSpacing,
bionicReading,
sourceId,
}),
[chapter.chapterText, removeExtraParagraphSpacing, bionicReading, sourceId],
);
const openDrawer = () => {
navigation.openDrawer();
hideHeader();
};
const bookmarkChapter = () =>
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
if (loading) {
return <ChapterLoadingScreen />;
}
if (error) {
return <ErrorScreenV2 error={error} />;
return (
<ErrorScreenV2
error={error}
actions={[
{
iconName: 'refresh',
title: getString('common.retry'),
onPress: () => {
setError('');
setLoading(true);
getChapter(chapterId);
},
},
{
iconName: 'earth',
title: 'WebView',
onPress: () =>
navigate('WebviewScreen', {
sourceId,
name: `${chapterName} | ${novelName}`,
url: chapterUrl,
}),
},
]}
/>
);
}
const bookmarkChapter = () =>
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
return (
<>
<WebViewReader
@@ -343,7 +425,9 @@ const ChapterContent = ({ route, navigation }) => {
html={chapterText}
chapterName={chapter.chapterName || chapterName}
swipeGestures={swipeGestures}
readerPages={readerPages}
minScroll={minScroll}
pages={pages}
nextChapter={nextChapter}
webViewRef={webViewRef}
onLayout={onLayout}
@@ -373,7 +457,9 @@ const ChapterContent = ({ route, navigation }) => {
/>
<ReaderSeekBar
theme={theme}
readerPages={readerPages}
minScroll={minScroll.current}
pages={pages.current}
verticalSeekbar={verticalSeekbar}
percentage={position?.percentage}
scrollTo={scrollTo}
+15 -4
View File
@@ -99,11 +99,24 @@ const ChapterDrawer = ({ state: st, navigation }) => {
onPress={() => changeChapter(item)}
style={styles.chapterCtn}
>
<Text numberOfLines={1} style={styles.chapterNameCtn}>
<Text
numberOfLines={1}
style={[
styles.chapterNameCtn,
{ color: item.read ? theme.outline : theme.onSurface },
]}
>
{item.chapterName}
</Text>
{item?.releaseDate ? (
<Text style={styles.releaseDateCtn}>{item.releaseDate}</Text>
<Text
style={[
styles.releaseDateCtn,
{ color: item.read ? theme.outline : theme.onSurfaceVariant },
]}
>
{item.releaseDate}
</Text>
) : null}
</Pressable>
</View>
@@ -226,11 +239,9 @@ const createStylesheet = theme => {
chapterNameCtn: {
fontSize: 12,
marginBottom: 2,
color: theme.onSurface,
},
releaseDateCtn: {
fontSize: 10,
color: theme.onSurfaceVariant,
},
drawerElementContainer: {
margin: 4,
@@ -8,7 +8,7 @@ import {
import React, { Ref, useMemo, useState } from 'react';
import color from 'color';
import { BottomSheetView, BottomSheetModal } from '@gorhom/bottom-sheet';
import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { defaultTo } from 'lodash-es';
import BottomSheet from '@components/BottomSheet/BottomSheet';
@@ -25,6 +25,8 @@ import ReaderTextAlignSelector from './ReaderTextAlignSelector';
import ReaderLineHeight from './ReaderLineHeight';
import ReaderFontPicker from './ReaderFontPicker';
import { overlay } from 'react-native-paper';
import { ScrollView } from 'react-native-gesture-handler';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const ReaderTab: React.FC = () => {
return (
@@ -50,13 +52,14 @@ const GeneralTab: React.FC = () => {
useVolumeButtons = false,
scrollAmount = 200,
swipeGestures = false,
readerPages = false,
removeExtraParagraphSpacing = false,
addChapterNameInReader = false,
bionicReading = false,
} = useSettingsV1();
return (
<View>
<ScrollView>
<ReaderSheetPreferenceItem
label={getString('readerScreen.bottomSheet.fullscreen')}
onPress={() =>
@@ -105,6 +108,12 @@ const GeneralTab: React.FC = () => {
value={swipeGestures}
theme={theme}
/>
<ReaderSheetPreferenceItem
label={getString('readerScreen.bottomSheet.readerPages')}
onPress={() => dispatch(setAppSettings('readerPages', !readerPages))}
value={readerPages}
theme={theme}
/>
<ReaderSheetPreferenceItem
label={getString('readerScreen.bottomSheet.removeExtraSpacing')}
onPress={() =>
@@ -161,7 +170,7 @@ const GeneralTab: React.FC = () => {
value={bionicReading}
theme={theme}
/>
</View>
</ScrollView>
);
};
interface ReaderBottomSheetV2Props {
@@ -198,31 +207,37 @@ const ReaderBottomSheetV2: React.FC<ReaderBottomSheetV2Props> = ({
[],
);
const renderTabBar = (props: any) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.primary }}
style={[
{
backgroundColor: tabHeaderColor,
borderBottomColor: theme.outline,
borderBottomWidth: 1,
},
styles.tabBar,
]}
renderLabel={({ route, color }) => (
<Text style={{ color }}>{route.title}</Text>
)}
inactiveColor={theme.onSurfaceVariant}
activeColor={theme.primary}
pressColor={color(theme.primary).alpha(0.12).string()}
/>
);
const renderTabBar = (props: any) => {
return (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.primary }}
style={[
{
backgroundColor: tabHeaderColor,
borderBottomColor: theme.outline,
borderBottomWidth: 1,
},
styles.tabBar,
]}
renderLabel={({ route, color }) => (
<Text style={{ color }}>{route.title}</Text>
)}
inactiveColor={theme.onSurfaceVariant}
activeColor={theme.primary}
pressColor={color(theme.primary).alpha(0.12).string()}
/>
);
};
const { bottom } = useSafeAreaInsets();
return (
<BottomSheet bottomSheetRef={bottomSheetRef} snapPoints={[360, 600]}>
<BottomSheetView
style={[styles.bottomSheetContainer, { backgroundColor }]}
<BottomSheetScrollView
contentContainerStyle={[
styles.bottomSheetContainer,
{ backgroundColor },
{ marginBottom: bottom },
]}
>
<TabView
navigationState={{ index, routes }}
@@ -232,7 +247,7 @@ const ReaderBottomSheetV2: React.FC<ReaderBottomSheetV2Props> = ({
initialLayout={{ width: layout.width }}
style={styles.tabView}
/>
</BottomSheetView>
</BottomSheetScrollView>
</BottomSheet>
);
};
@@ -5,19 +5,39 @@ import color from 'color';
import Slider from '@react-native-community/slider';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDeviceOrientation } from '@hooks/useDeviceOrientation';
import { ThemeColors } from '@theme/types';
const VerticalScrollbar = ({
interface IScrollbar {
theme: ThemeColors;
minScroll: number;
pages: number;
verticalSeekbar: boolean;
readerPages: boolean;
percentage?: number;
scrollTo: (offset: number) => void;
}
const VerticalScrollbar: React.FC<IScrollbar> = ({
theme,
minScroll = 0,
pages,
verticalSeekbar,
readerPages,
percentage,
scrollTo,
}) => {
const { bottom } = useSafeAreaInsets();
const onSlidingComplete = value => {
let offsetY =
((value - minScroll) * Dimensions.get('window').height) / minScroll;
scrollTo(offsetY);
const onSlidingComplete = (value: number) => {
let offset;
if (readerPages) {
let newPage = Math.round(pages * (value / 100)) * 100;
// pages = { ...pages, current: newPage };
offset = newPage;
} else {
offset =
((value - minScroll) * Dimensions.get('window').height) / minScroll;
}
scrollTo(offset);
};
const screenOrientation = useDeviceOrientation();
@@ -43,7 +63,7 @@ const VerticalScrollbar = ({
flex: 1,
height: 40,
}}
minimumValue={Math.round(minScroll)}
minimumValue={readerPages ? 1 : Math.round(minScroll)}
maximumValue={100}
step={1}
value={percentage || Math.round(minScroll)}
@@ -87,7 +107,7 @@ const VerticalScrollbar = ({
flex: 1,
height: 40,
}}
minimumValue={Math.round(minScroll)}
minimumValue={readerPages ? 1 : Math.round(minScroll)}
maximumValue={100}
step={1}
value={percentage || Math.round(minScroll)}
+65 -126
View File
@@ -10,6 +10,9 @@ import { useReaderSettings, useSettingsV1 } from '@redux/hooks';
import { getString } from '@strings/translations';
import { sourceManager } from '../../../sources/sourceManager';
import { createHorizontalReaderPages } from './stringCreators/horizontalReaderPages';
import { createStyles } from './stringCreators/createStyle';
import { createSwipeGestures } from './stringCreators/createSwipeGestures';
type WebViewPostEvent = {
type: string;
@@ -30,7 +33,9 @@ type WebViewReaderProps = {
html: string;
chapterName: string;
swipeGestures: boolean;
readerPages: boolean;
minScroll: React.MutableRefObject<number>;
pages: React.MutableRefObject<number>;
nextChapter: ChapterItem;
webViewRef: React.MutableRefObject<WebView>;
onPress(): void;
@@ -52,9 +57,11 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
html,
chapterName,
swipeGestures,
readerPages,
minScroll,
nextChapter,
webViewRef,
pages,
onPress,
onLayout,
doSaveProgress,
@@ -112,6 +119,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
if (event.data) {
const offSetY = Number(event.data?.offSetY);
const percentage = Math.round(Number(event.data?.percentage));
doSaveProgress(offSetY, percentage);
}
break;
@@ -121,6 +129,11 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
minScroll.current = (layoutHeight / contentHeight) * 100;
}
break;
case 'pages':
if (event.data) {
pages.current = Number(event.data);
}
break;
}
}}
source={{
@@ -128,85 +141,13 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>
html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
word-wrap: break-word;
}
body {
padding-left: ${readerSettings.padding}%;
padding-right: ${readerSettings.padding}%;
padding-bottom: 40px;
font-size: ${readerSettings.textSize}px;
color: ${readerSettings.textColor};
text-align: ${readerSettings.textAlign};
line-height: ${readerSettings.lineHeight};
font-family: ${readerSettings.fontFamily};
}
chapter{
display: block;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
}
a {
color: ${theme.primary};
}
img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}
img.load-icon {
display: block;
margin-inline: auto;
animation: rotation 1s infinite linear;
}
@keyframes rotation {
100% {
transform: rotate(360deg);
}
0% {
transform: rotate(0deg);
}
}
.nextButton,
.infoText {
width: 100%;
border-radius: 50px;
border-width: 1;
color: ${theme.onPrimary};
background-color: ${theme.primary};
font-family: ${readerSettings.fontFamily};
font-size: 16px;
border-width: 0;
}
.nextButton {
min-height: 40px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 0 16px;
}
.infoText {
background-color: transparent;
text-align:center;
border: none;
margin: 0px;
color: inherit;
padding-top: 16px;
padding-bottom: 16px;
}
.chapterCtn {
min-height: ${layoutHeight - 140};
margin-bottom: auto;
}
</style>
${createStyles(
StatusBar.currentHeight ?? 0,
readerSettings,
theme,
layoutHeight,
readerPages,
)}
<style>
${readerSettings.customCSS}
@@ -220,10 +161,22 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
</style>
</head>
<body id='sourceId-${chapterInfo.sourceId}'>
<div class="chapterCtn" ${onClickWebViewPostMessage({
type: 'hide',
})}>
<div class="chapterCtn" ${
!readerPages &&
onClickWebViewPostMessage({
type: 'hide',
})
}>
<div id="left"></div>
<div id="right"></div>
<div id="middle" ${
readerPages &&
onClickWebViewPostMessage({
type: 'hide',
})
}></div>
<chapter
data-page=0
data-novel-id='${chapterInfo.novelId}'
data-chapter-id='${chapterInfo.chapterId}'
>
@@ -233,7 +186,29 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
: ''
}
${html}
<p id="spacer"></p>
</chapter>
<div id="infoContainer" class="hide">
<div class="infoText">
${getString(
'readerScreen.finished',
)}: ${chapterName?.trim()}
</div>
${
nextChapter
? `<button class="nextButton"
${onClickWebViewPostMessage({
type: 'next',
})}>
Next: ${nextChapter.chapterName}
</button>`
: `<div class="infoText">${getString(
'readerScreen.noNextChapter',
)}</div>`
}
</div>
</div>
<script>
if(!document.querySelector("input[offline]") && ${!!headers}){
@@ -241,9 +216,8 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
window.ReactNativeWebView.postMessage(JSON.stringify({type:"imgfile",data:img.getAttribute("delayed-src")}));
});
}
var scrollTimeout;
window.addEventListener("scroll", (event) => {
${!readerPages} && window.addEventListener("scroll", (event) => {
window.clearTimeout( scrollTimeout );
scrollTimeout = setTimeout(() => {
window.ReactNativeWebView.postMessage(
@@ -259,65 +233,31 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
);
}, 100);
});
let sendHeightTimeout;
const sendHeight = (timeOut) => {
clearTimeout(sendHeightTimeout);
sendHeightTimeout = setTimeout(
window.ReactNativeWebView.postMessage(
JSON.stringify({type:"height",data:document.body.scrollHeight})
JSON.stringify({type:"height",data: document.body.scrollHeight})
), timeOut
);
}
sendHeight(1000);
</script>
<div class="infoText">
${getString(
'readerScreen.finished',
)}: ${chapterName?.trim()}
</div>
${
nextChapter
? `<button class="nextButton" ${onClickWebViewPostMessage(
{ type: 'next' },
)}>
Next: ${nextChapter.chapterName}
</button>`
: `<div class="infoText">${getString(
'readerScreen.noNextChapter',
)}</div>`
}
${
swipeGestures
? `
<script>
let initialX = null;
let initialY = null;
document.addEventListener("touchstart", e => {
initialX = e.changedTouches[0].screenX;
initialY = e.changedTouches[0].screenY;
});
document.addEventListener("touchend", e => {
let diffX = e.changedTouches[0].screenX - initialX;
let diffY = e.changedTouches[0].screenY - initialY;
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
e.preventDefault();
window.ReactNativeWebView.postMessage(JSON.stringify({type: diffX<0 ? "next" : "prev"}))
}
});
</script>`
: ''
}
${swipeGestures ? createSwipeGestures() : ''}
<script>
async function fn(){
// Position important to prevent layout bugs
${readerPages && createHorizontalReaderPages()}
let novelName = "${chapterInfo.novelName}";
let chapterName = "${chapterInfo.chapterName}";
let sourceId =${chapterInfo.sourceId};
let chapterId =${chapterInfo.chapterId};
let novelId =${chapterInfo.novelId};
let html = document.querySelector("chapter").innerHTML;
${readerSettings.customJS}
${readerSettings.customJS}
}
document.addEventListener("DOMContentLoaded", fn);
</script>
@@ -328,5 +268,4 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
/>
);
};
export default React.memo(WebViewReader, isEqual);
@@ -0,0 +1,167 @@
import { ThemeColors } from '@theme/types';
export const createStyles = (
currentHeight: number,
readerSettings: {
theme: string;
textColor: string;
textSize: number;
textAlign: string;
padding: number;
fontFamily: string;
lineHeight: number;
customCSS: string;
customJS: string;
},
theme: ThemeColors,
layoutHeight: number,
readerPages: boolean,
) => `
<style>
html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: ${currentHeight};
word-wrap: break-word;
}
body {
padding-bottom: 40px;
margin-left: 0;
margin-right: 0;
font-size: ${readerSettings.textSize}px;
color: ${readerSettings.textColor};
text-align: ${readerSettings.textAlign};
line-height: ${readerSettings.lineHeight};
font-family: ${readerSettings.fontFamily};
}
chapter{
display: block;
}
chapter > *{
padding-left: ${readerSettings.padding}%;
padding-right: ${readerSettings.padding}%;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
}
a {
color: ${theme.primary};
}
img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}
img.load-icon {
display: block;
margin-inline: auto;
animation: rotation 1s infinite linear;
}
@keyframes rotation {
100% {
transform: rotate(360deg);
}
0% {
transform: rotate(0deg);
}
}
#infoContainer {
padding-left: ${readerSettings.padding}%;
padding-right: ${readerSettings.padding}%;
}
.nextButton,
.infoText {
width: 100%;
border-radius: 50px;
border-width: 1;
color: ${theme.onPrimary};
background-color: ${theme.primary};
font-family: ${readerSettings.fontFamily};
font-size: 16px;
border-width: 0;
}
.nextButton {
min-height: 40px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 0 16px;
}
.infoText {
background-color: transparent;
text-align:center;
border: none;
margin: 0px;
color: inherit;
padding-top: 16px;
padding-bottom: 16px;
}
.chapterCtn {
min-height: ${layoutHeight - 140};
margin-bottom: auto;
}
${
readerPages &&
`
body {
overflow: hidden;
position: relative;
}
#left,
#right, #middle {
position: absolute;
height: 100%;
width: 35%;
top: 0;
z-index: 20
}
#middle {
left: 35%;
width: 30%;
}
#left {
left: 0;
}
#right {
right: 0;
}
#infoContainer {
position: absolute;
z-index: 1000;
bottom: 40;
width: calc(100% - 2*${readerSettings.padding}%)
}
.nextButton{
position: relative;
z-index: 100000 !important
}
chapter {
height: 100%;
display: flexbox;
flex-direction: column;
flex-wrap: wrap;
column-gap: 0;
column-width: 100vw;
transition: 200ms;
}
chapter > * {
width: calc(100% - 2*${readerSettings.padding}%) !important;
}
#spacer {
display: block;
min-height: 60px;
width: 100%;
}
.hide {
transform: translate(110%);
transition: 200ms
}
.show {
transform: translate(0%);
}
`
}
</style>`;
@@ -0,0 +1,18 @@
export function createSwipeGestures() {
return `<script>
let initialX = null;
let initialY = null;
document.addEventListener("touchstart", e => {
initialX = e.changedTouches[0].screenX;
initialY = e.changedTouches[0].screenY;
});
document.addEventListener("touchend", e => {
let diffX = e.changedTouches[0].screenX - initialX;
let diffY = e.changedTouches[0].screenY - initialY;
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
e.preventDefault();
window.ReactNativeWebView.postMessage(JSON.stringify({type: diffX<0 ? "next" : "prev"}))
}
});
</script>`;
}
@@ -0,0 +1,71 @@
export const createHorizontalReaderPages = () => {
return `
const id = (key) => {
return document.getElementById(key)
}
const select = (key) => {
return document.querySelector(key)
}
const chapter = select("chapter");
const clientWidth = document.documentElement.clientWidth;
const textWidth = chapter.scrollWidth;
const navLeft = id("left");
const navRight = id("right");
const infoBox = id("infoContainer");
infoBox.classList.add("hidden");
id("spacer").style.height = infoBox.scrollHeight + 'px';
const pages = (Math.ceil(textWidth / clientWidth) - 1);
let page = 0;
navRight.addEventListener("click", () => {
page = select("chapter").getAttribute('data-page');
if (isNaN(page)) {
page = 0;
}
if (page < pages ) {
page++;
movePage();
if (page === pages){
infoBox.classList.add("show")
}
}
});
navLeft.addEventListener("click", () => {
page = select("chapter").getAttribute('data-page');
if (page > 0) {
infoBox.classList.remove("show")
page--;
movePage();
}
})
function movePage(){
chapter.style.transform = 'translate(-'+page*100+'%)';
select('chapter').setAttribute('data-page',
page
);
window.ReactNativeWebView.postMessage(
JSON.stringify(
{
type:"scrollend",
data:{
offSetY: page * 100, percentage: page === 0 ? 1 : page / pages * 100,
}
}
)
);
}
let sendWidthTimeout;
const sendPages = (timeOut) => {
clearTimeout(sendHeightTimeout);
sendHeightTimeout = setTimeout(
window.ReactNativeWebView.postMessage(
JSON.stringify({type:"pages",data: pages})
), timeOut
);
}
sendPages(200);
;`;
};
+21 -17
View File
@@ -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 =
+111 -95
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { ScrollView } from 'react-native';
import { Dialog, Portal } from 'react-native-paper';
import { useTheme } from '@hooks/useTheme';
@@ -12,13 +12,18 @@ import { getString } from '@strings/translations';
import useBoolean from '@hooks/useBoolean';
import ConfirmationDialog from '@components/ConfirmationDialog/ConfirmationDialog';
import { deleteReadChaptersFromDb } from '../../database/queries/DownloadQueries';
import { openDirectory } from '../../native/epubParser';
import { Appbar, Button, List } from '@components';
import useSourceStorage from '@hooks/useSourceStorage';
import { ScreenContainer } from '@components/Common';
import CookieManager from '@react-native-cookies/cookies';
const AdvancedSettings = ({ navigation }) => {
const theme = useTheme();
const { clearCookies } = useSourceStorage({});
const clearCookies = () => {
CookieManager.clearAll();
showToast('Cookies cleared');
};
/**
* Confirm Clear Database Dialog
@@ -38,113 +43,124 @@ const AdvancedSettings = ({ navigation }) => {
} = useBoolean();
return (
<>
<ScreenContainer theme={theme}>
<Appbar
title="Advanced"
handleGoBack={() => navigation.goBack()}
theme={theme}
/>
<List.Section>
<List.SubHeader theme={theme}>Data Management</List.SubHeader>
<List.Item
title="Clear database"
description="Delete history for novels not in your library"
onPress={showClearDatabaseDialog}
theme={theme}
/>
<List.Item
title="Clear updates tab"
description="Clears chapter entries in updates tab"
onPress={showClearUpdatesDialog}
theme={theme}
/>
<List.Item
title="Clean up cached covers"
description="Delete all covers cached"
onPress={clearCoverCache}
theme={theme}
/>
<List.Item
title={getString('advancedSettings.deleteReadChapters')}
onPress={showDeleteReadChaptersDialog}
theme={theme}
/>
<List.Item title="Clear cookies" onPress={clearCookies} theme={theme} />
</List.Section>
<Portal>
<ConfirmationDialog
title={getString('advancedSettings.deleteReadChaptersDialogTitle')}
visible={deleteReadChaptersDialog}
onSubmit={deleteReadChaptersFromDb}
onDismiss={hideDeleteReadChaptersDialog}
theme={theme}
/>
<Dialog
visible={clearDatabaseDialog}
onDismiss={hideClearDatabaseDialog}
style={{
borderRadius: 28,
backgroundColor: theme.overlay3,
}}
>
<Dialog.Title
<ScrollView>
<List.Section>
<List.SubHeader theme={theme}>Data Management</List.SubHeader>
<List.Item
title="Clear database"
description="Delete history for novels not in your library"
onPress={showClearDatabaseDialog}
theme={theme}
/>
<List.Item
title="Clear updates tab"
description="Clears chapter entries in updates tab"
onPress={showClearUpdatesDialog}
theme={theme}
/>
<List.Item
title="Clean up cached covers"
description="Delete all covers cached"
onPress={clearCoverCache}
theme={theme}
/>
<List.Item
title={getString('advancedSettings.deleteReadChapters')}
onPress={showDeleteReadChaptersDialog}
theme={theme}
/>
<List.Item
title="Clear cookies"
onPress={clearCookies}
theme={theme}
/>
<List.Item
title="Import Epub"
onPress={() => openDirectory()}
theme={theme}
/>
</List.Section>
<Portal>
<ConfirmationDialog
title={getString('advancedSettings.deleteReadChaptersDialogTitle')}
visible={deleteReadChaptersDialog}
onSubmit={deleteReadChaptersFromDb}
onDismiss={hideDeleteReadChaptersDialog}
theme={theme}
/>
<Dialog
visible={clearDatabaseDialog}
onDismiss={hideClearDatabaseDialog}
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.onSurface,
borderRadius: 28,
backgroundColor: theme.overlay3,
}}
>
Are you sure? Read chapters and progress of non-library novels will
be lost.
</Dialog.Title>
<Dialog.Actions>
<Button onPress={hideClearDatabaseDialog}>
{getString('common.cancel')}
</Button>
<Button
onPress={() => {
deleteNovelCache();
hideClearDatabaseDialog();
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.onSurface,
}}
>
{getString('common.ok')}
</Button>
</Dialog.Actions>
</Dialog>
<Dialog
visible={clearUpdatesDialog}
onDismiss={hideClearUpdatesDialog}
style={{
borderRadius: 28,
backgroundColor: theme.overlay3,
}}
>
<Dialog.Title
Are you sure? Read chapters and progress of non-library novels
will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button onPress={hideClearDatabaseDialog}>
{getString('common.cancel')}
</Button>
<Button
onPress={() => {
deleteNovelCache();
hideClearDatabaseDialog();
}}
>
{getString('common.ok')}
</Button>
</Dialog.Actions>
</Dialog>
<Dialog
visible={clearUpdatesDialog}
onDismiss={hideClearUpdatesDialog}
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.onSurface,
borderRadius: 28,
backgroundColor: theme.overlay3,
}}
>
Are you sure? Updates tab will be cleared.
</Dialog.Title>
<Dialog.Actions>
<Button onPress={hideClearUpdatesDialog}>Cancel</Button>
<Button
onPress={() => {
clearUpdates();
showToast('Updates cleared.');
hideClearUpdatesDialog();
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.onSurface,
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</>
Are you sure? Updates tab will be cleared.
</Dialog.Title>
<Dialog.Actions>
<Button onPress={hideClearUpdatesDialog}>Cancel</Button>
<Button
onPress={() => {
clearUpdates();
showToast('Updates cleared.');
hideClearUpdatesDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</ScrollView>
</ScreenContainer>
);
};
@@ -25,7 +25,7 @@ import NovelBadgesModal from './modals/NovelBadgesModal';
import { RootState } from '@redux/store';
import { NavigationState } from '@react-navigation/native';
import { getString } from '@strings/translations';
import ChooseEpubLocationModal from '@screens/novel/components/Modal/ChooseEpubLocationModal';
import ChooseEpubLocationModal from '@screens/novel/components/ChooseEpubLocationModal';
interface GenralSettingsProps {
navigation: NavigationState;
@@ -62,6 +62,7 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
refreshNovelMetadata = false,
disableHapticFeedback = false,
useLibraryFAB = false,
jumpToLastReadChapter = false,
} = useSettings();
const { showLastUpdateTime = true } = useSelector(
@@ -105,10 +106,12 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
* Chapter Sort Modal
*/
const defaultChapterSortModal = useBoolean();
/**
* Convert to EPUB Modal
*/
const convertToEpubModal = useBoolean();
return (
<>
<Appbar
@@ -213,6 +216,18 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
onPress={defaultChapterSortModal.setTrue}
theme={theme}
/>
<SwitchSetting
label={getString(
'moreScreen.settingsScreen.generalSettingsScreen.jumpToLastReadChapter',
)}
value={jumpToLastReadChapter}
onPress={() =>
dispatch(
setAppSettings('jumpToLastReadChapter', !jumpToLastReadChapter),
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
{getString(
@@ -263,7 +278,6 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
{getString('moreScreen.settingsScreen.generalSettingsScreen.epub')}
</List.SubHeader>
@@ -343,9 +357,8 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
theme={theme}
/>
<ChooseEpubLocationModal
isVisible={convertToEpubModal.value}
hideModal={convertToEpubModal.setFalse}
modalVisible={convertToEpubModal.value}
dispatchConfig
/>
</>
);
@@ -1,118 +1,101 @@
import React, { useState } from 'react';
import { KeyboardAvoidingView, StyleSheet, Text, View } from 'react-native';
import { StyleSheet, Text, View } from 'react-native';
import { Modal, overlay, TextInput } from 'react-native-paper';
import { Button } from '@components/index';
import { MD3ThemeType } from '@theme/types';
import { getString } from '@strings/translations';
import { useAppDispatch } from '@redux/hooks';
import * as DocumentPicker from 'expo-document-picker';
import { StorageAccessFramework } from 'expo-file-system';
import { showToast } from '@hooks/showToast';
import { setReaderSettings } from '@redux/settings/settings.actions';
import * as DocumentPicker from 'expo-document-picker';
interface CustomCSSModalProps {
import { Button } from '@components/index';
import { showToast } from '@hooks/showToast';
import { useTheme } from '@hooks/useTheme';
import { getString } from '@strings/translations';
interface CustomFileModal {
visible: boolean;
onDismiss: () => void;
customFile: string;
setCustomFile: (val: string) => void;
theme: MD3ThemeType;
defaultValue: string;
onSave: (val: string) => void;
title: string;
type?: string;
openFileButtonLabel?: string;
mimeType: 'text/css' | 'application/javascript';
openFileLabel: string;
placeholder?: string;
description?: string;
}
const CustomFileModal: React.FC<CustomCSSModalProps> = ({
const CustomFileModal: React.FC<CustomFileModal> = ({
onDismiss,
visible,
customFile,
setCustomFile,
theme,
defaultValue,
onSave,
title,
type = 'CSS',
openFileButtonLabel = getString(
'moreScreen.settingsScreen.readerSettings.openCSSFile',
),
placeholder = '',
mimeType,
openFileLabel,
placeholder,
description,
}) => {
const [text, setText] = useState(customFile);
const dispatch = useAppDispatch();
let mimeType: string;
if (type === 'CSS') {
mimeType = 'text/css';
} else {
mimeType = 'application/javascript';
}
const theme = useTheme();
const [text, setText] = useState('');
const openCSS = async () => {
const openDocumentPicker = async () => {
try {
const rawCSS = await DocumentPicker.getDocumentAsync({
const file = await DocumentPicker.getDocumentAsync({
copyToCacheDirectory: false,
type: mimeType,
});
if (rawCSS.type === 'success') {
let css = await StorageAccessFramework.readAsStringAsync(rawCSS.uri);
setText(css);
if (file.type === 'success') {
const content = await StorageAccessFramework.readAsStringAsync(
file.uri,
);
onSave(content.trim());
onDismiss();
}
} catch (error: any) {
showToast(error.message);
}
};
const dismiss = () => {
setCustomFile(text.trim());
onDismiss();
};
return (
<Modal
visible={visible}
onDismiss={dismiss}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: overlay(2, theme.surface) },
]}
>
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={180}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{title}
</Text>
<Text style={[{ color: theme.onSurfaceVariant }]}>
{type === 'CSS'
? getString('moreScreen.settingsScreen.readerSettings.cssHint')
: getString('moreScreen.settingsScreen.readerSettings.jsHint')}
</Text>
<TextInput
theme={{ colors: { ...theme } }}
underlineColor={theme.outline}
value={text}
onChangeText={setText}
mode="outlined"
placeholder={placeholder}
placeholderTextColor={theme.onSurfaceVariant}
multiline
style={[
{ color: theme.onSurface },
styles.fontSizeL,
styles.customCSSContainer,
]}
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{title}
</Text>
<Text style={[{ color: theme.onSurfaceVariant }]}>{description}</Text>
<TextInput
multiline
mode="outlined"
defaultValue={defaultValue}
onChangeText={setText}
placeholder={placeholder}
placeholderTextColor={theme.onSurfaceVariant}
underlineColor={theme.outline}
style={[{ color: theme.onSurface }, styles.textInput]}
theme={{ colors: { ...theme } }}
/>
<View style={styles.customCSSButtons}>
<Button
onPress={() => {
onSave(text.trim());
onDismiss();
}}
style={styles.button}
title={getString('common.save')}
mode="contained"
/>
<View style={styles.customCSSButtons}>
<Button
onPress={() => {
dispatch(setReaderSettings(`custom${type}`, text.trim()));
dismiss();
}}
style={styles.button}
title={getString('common.save')}
mode="contained"
/>
<Button
style={styles.button}
onPress={openCSS}
title={openFileButtonLabel}
/>
</View>
</KeyboardAvoidingView>
<Button
style={styles.button}
onPress={openDocumentPicker}
title={openFileLabel}
/>
</View>
</Modal>
);
};
@@ -125,14 +108,12 @@ const styles = StyleSheet.create({
padding: 24,
borderRadius: 28,
},
fontSizeL: {
fontSize: 16,
},
customCSSContainer: {
textInput: {
height: 220,
borderRadius: 14,
marginTop: 16,
marginBottom: 8,
fontSize: 16,
},
modalTitle: {
fontSize: 24,
@@ -1,23 +1,24 @@
import { StyleSheet, View, TextInput } from 'react-native';
import React, { useState } from 'react';
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Portal } from 'react-native-paper';
import { Button, List } from '@components/index';
import { setReaderSettings } from '@redux/settings/settings.actions';
import { Button, List, ConfirmationDialog } from '@components/index';
import useBoolean from '@hooks/useBoolean';
import { useTheme } from '@hooks/useTheme';
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
import { setReaderSettings } from '@redux/settings/settings.actions';
import { getString } from '@strings/translations';
import CustomFileModal from '../Modals/CustomFileModal';
import useBoolean from '@hooks/useBoolean';
import { Portal } from 'react-native-paper';
const CustomCSSSettings = () => {
const theme = useTheme();
const readerSettings = useReaderSettings();
const [customCSS, setCustomCSS] = useState(readerSettings.customCSS);
const dispatch = useAppDispatch();
const cssModal = useBoolean();
const clearCSSModal = useBoolean();
return (
<>
@@ -25,22 +26,11 @@ const CustomCSSSettings = () => {
<List.SubHeader theme={theme}>
{getString('moreScreen.settingsScreen.readerSettings.customCSS')}
</List.SubHeader>
<List.SubHeader theme={theme}>
{readerSettings.customCSS !== customCSS
? getString('moreScreen.settingsScreen.readerSettings.notSaved')
: null}
</List.SubHeader>
</View>
<View style={styles.customCSSContainer}>
<TextInput
style={[{ color: theme.onSurface }, styles.fontSizeL]}
value={customCSS}
onChangeText={text => setCustomCSS(text)}
placeholderTextColor={theme.onSurfaceVariant}
placeholder="Example: body {margin: 10px;}"
multiline={true}
editable={false}
/>
<Text numberOfLines={3} style={[{ color: theme.onSurface }]}>
{readerSettings.customCSS || 'Example: body {margin: 10px;}'}
</Text>
<View style={styles.customCSSButtons}>
<Button
onPress={cssModal.setTrue}
@@ -48,28 +38,41 @@ const CustomCSSSettings = () => {
title={getString('common.edit')}
/>
<Button
onPress={() => {
setCustomCSS('');
dispatch(setReaderSettings('customCSS', ''));
}}
onPress={clearCSSModal.setTrue}
title={getString('common.clear')}
/>
</View>
</View>
{/*
Modals
*/}
{/* Modals */}
<Portal>
<CustomFileModal
visible={cssModal.value}
onDismiss={cssModal.setFalse}
defaultValue={readerSettings.customCSS}
mimeType="text/css"
title={getString(
'moreScreen.settingsScreen.readerSettings.customCSS',
)}
visible={cssModal.value}
onDismiss={cssModal.setFalse}
theme={theme}
customFile={customCSS}
setCustomFile={setCustomCSS}
description={getString(
'moreScreen.settingsScreen.readerSettings.cssHint',
)}
placeholder="Example: body {margin: 10px;}"
openFileLabel={getString(
'moreScreen.settingsScreen.readerSettings.openCSSFile',
)}
onSave={text => dispatch(setReaderSettings('customCSS', text))}
/>
<ConfirmationDialog
title={getString(
'moreScreen.settingsScreen.readerSettings.clearCustomCSS',
)}
visible={clearCSSModal.value}
onSubmit={() => {
dispatch(setReaderSettings('customCSS', ''));
}}
onDismiss={clearCSSModal.setFalse}
theme={theme}
/>
</Portal>
</>
@@ -1,15 +1,16 @@
import { StyleSheet, TextInput, View } from 'react-native';
import React, { useState } from 'react';
import { Button, List } from '@components/index';
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
import { useTheme } from '@hooks/useTheme';
import { getString } from '@strings/translations';
import { setReaderSettings } from '@redux/settings/settings.actions';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Portal } from 'react-native-paper';
import CustomFileModal from '../Modals/CustomFileModal';
import { Button, List, ConfirmationDialog } from '@components/index';
import useBoolean from '@hooks/useBoolean';
import { useTheme } from '@hooks/useTheme';
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
import { setReaderSettings } from '@redux/settings/settings.actions';
import { getString } from '@strings/translations';
import CustomFileModal from '../Modals/CustomFileModal';
interface CustomJSSettingsProps {
readerSettings: ReturnType<typeof useReaderSettings>;
@@ -21,8 +22,8 @@ const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
const theme = useTheme();
const dispatch = useAppDispatch();
const [customJS, setCustomJS] = useState(readerSettings.customJS);
const jsModal = useBoolean();
const clearJSModal = useBoolean();
return (
<>
@@ -30,21 +31,11 @@ const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
<List.SubHeader theme={theme}>
{getString('moreScreen.settingsScreen.readerSettings.customJS')}
</List.SubHeader>
<List.SubHeader theme={theme}>
{readerSettings.customJS !== customJS
? getString('moreScreen.settingsScreen.readerSettings.notSaved')
: null}
</List.SubHeader>
</View>
<View style={styles.customJSContainer}>
<TextInput
style={[{ color: theme.onSurface }, styles.fontSizeL]}
value={customJS}
placeholderTextColor={theme.onSurfaceVariant}
placeholder="Example: document.getElementById('example');"
multiline={true}
editable={false}
/>
<Text numberOfLines={3} style={[{ color: theme.onSurface }]}>
{readerSettings.customJS || 'Example: body {margin: 10px;}'}
</Text>
<View style={styles.customJSButtons}>
<Button
onPress={jsModal.setTrue}
@@ -52,30 +43,39 @@ const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
title={getString('common.edit')}
/>
<Button
onPress={() => {
setCustomJS('');
dispatch(setReaderSettings('customJS', ''));
}}
onPress={clearJSModal.setTrue}
title={getString('common.clear')}
/>
</View>
</View>
{/*
Modals
*/}
{/* Modals */}
<Portal>
<CustomFileModal
title={getString('moreScreen.settingsScreen.readerSettings.customJS')}
visible={jsModal.value}
onDismiss={jsModal.setFalse}
theme={theme}
customFile={customJS}
setCustomFile={setCustomJS}
type="JS"
openFileButtonLabel={getString(
'moreScreen.settingsScreen.readerSettings.openJSFile',
defaultValue={readerSettings.customJS}
mimeType="application/javascript"
title={getString('moreScreen.settingsScreen.readerSettings.customJS')}
description={getString(
'moreScreen.settingsScreen.readerSettings.jsHint',
)}
placeholder="Example: document.getElementById('example');"
openFileLabel={getString(
'moreScreen.settingsScreen.readerSettings.openJSFile',
)}
onSave={text => dispatch(setReaderSettings('customJS', text))}
/>
<ConfirmationDialog
title={getString(
'moreScreen.settingsScreen.readerSettings.clearCustomJS',
)}
visible={clearJSModal.value}
onSubmit={() => {
dispatch(setReaderSettings('customJS', ''));
}}
onDismiss={clearJSModal.setFalse}
theme={theme}
/>
</Portal>
</>
@@ -24,6 +24,7 @@ const GeneralSettings: React.FC = () => {
useVolumeButtons = false,
verticalSeekbar = true,
swipeGestures = false,
readerPages = false,
autoScroll = false,
autoScrollInterval = 10,
autoScrollOffset = null,
@@ -76,6 +77,12 @@ const GeneralSettings: React.FC = () => {
}
theme={theme}
/>
<SwitchItem
label={getString('readerScreen.bottomSheet.readerPages')}
value={readerPages}
onPress={() => dispatch(setAppSettings('readerPages', !readerPages))}
theme={theme}
/>
<SwitchItem
label={getString('readerScreen.bottomSheet.autoscroll')}
value={autoScroll}
@@ -6,11 +6,9 @@ import FastImage from 'react-native-fast-image';
import { List } from 'react-native-paper';
import { coverPlaceholderColor } from '../../../theme/colors';
import {
openChapter,
openChapterFunctionTypes,
openNovel,
openNovelProps,
} from '@utils/handleNavigateParams';
getChapterScreenRouteParams,
getNovelScreenRouteParams,
} from '@utils/NavigationUtils';
import {
deleteChapterAction,
downloadChapterAction,
@@ -23,16 +21,27 @@ import { useAppDispatch } from '@redux/hooks';
import { useTheme } from '@hooks/useTheme';
import { noop } from 'lodash-es';
import { defaultUserAgentString } from '@utils/fetch/fetch';
const NovelCover = ({
uri,
navigateToNovel,
}: {
uri: string;
navigateToNovel: () => void;
sourceId: number;
}) => {
return (
<Pressable onPress={navigateToNovel}>
<FastImage source={{ uri }} style={styles.cover} />
<Pressable
onPress={navigateToNovel}
style={{
justifyContent: 'center',
}}
>
<FastImage
source={{ uri, headers: { 'User-Agent': defaultUserAgentString } }}
style={styles.cover}
/>
</Pressable>
);
};
@@ -90,7 +99,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
(chapter: ChapterItemExtended) =>
navigate(
'Chapter' as never,
openChapter(chapter, chapter) as openChapterFunctionTypes as never,
getChapterScreenRouteParams(chapter, chapter) as never,
),
[],
);
@@ -98,26 +107,30 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
const navigateToNovel = () =>
navigate(
'Novel' as never,
openNovel(chapterList[0]) as openNovelProps as never,
getNovelScreenRouteParams(chapterList[0]) as never,
);
const { downloadQueue } = useSelector(
(state: RootState) => state.downloadsReducer,
);
const renderNovelCover = useCallback(
() => (
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
sourceId={chapterList[0].sourceId}
/>
),
[JSON.stringify(chapterList[0])],
);
if (chapterList.length > 1) {
return (
<List.Accordion
title={chapterList[0].novelName}
titleStyle={{ fontSize: 14, color: theme.onSurface }}
left={() => (
<View style={styles.listNovelCover}>
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
/>
</View>
)}
left={renderNovelCover}
descriptionStyle={{ fontSize: 12 }}
theme={{ colors: theme }}
style={[styles.container, styles.padding]}
@@ -145,6 +158,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
sourceId={chapterList[0].sourceId}
/>
</View>
}
@@ -172,6 +186,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
sourceId={chapterList[0].sourceId}
/>
</View>
}
@@ -186,7 +201,8 @@ export default UpdateNovelCard;
const styles = StyleSheet.create({
padding: {
paddingHorizontal: 16,
paddingVertical: 3,
paddingVertical: 0,
alignItems: 'center',
height: 64,
},
container: {
@@ -198,9 +214,6 @@ const styles = StyleSheet.create({
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
listNovelCover: {
justifyContent: 'center',
},
novelCover: {
marginRight: 16,
},
+11 -1
View File
@@ -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);
}
+35 -42
View File
@@ -42,56 +42,49 @@ const updateLibrary = async options => {
i++
) {
try {
if (BackgroundService.isRunning()) {
/**
* Update chapters
*/
await updateNovel(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId,
options,
);
/**
* Update chapters
*/
await updateNovel(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId,
options,
);
/**
* Update notification
*/
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
progressBar: { max: libraryNovels.length, value: i + 1 },
});
/**
* Update notification
*/
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
progressBar: { max: libraryNovels.length, value: i + 1 },
});
/**
* When updating library is finished
*/
if (libraryNovels.length === i + 1) {
resolve();
const nextNovelIndex = i + 1;
Notifications.scheduleNotificationAsync({
content: {
title: 'Library Updated',
body: libraryNovels.length + ' novels updated',
},
trigger: null,
});
}
const nextNovelIndex = i + 1;
if (
nextNovelIndex in libraryNovels &&
libraryNovels[nextNovelIndex].sourceId ===
libraryNovels[i].sourceId
) {
await sleep(taskData.delay);
}
if (
nextNovelIndex in libraryNovels &&
libraryNovels[nextNovelIndex].sourceId === libraryNovels[i].sourceId
) {
await sleep(taskData.delay);
}
} catch (error) {
showToast(libraryNovels[i].novelName + ': ' + error.message);
continue;
}
}
/**
* When updating library is finished
*/
resolve();
Notifications.scheduleNotificationAsync({
content: {
title: 'Library Updated',
body: libraryNovels.length + ' novels updated',
},
trigger: null,
});
});
if (libraryNovels.length > 0) {
+421
View File
@@ -0,0 +1,421 @@
import { showToast } from '@hooks/showToast';
import { fetchHtml } from '@utils/fetch/fetch';
import * as cheerio from 'cheerio';
const sourceId = 165;
const sourceName = 'Linovelib';
const baseUrl = 'https://w.linovelib.com';
const popularNovels = async page => {
const url = `${baseUrl}/top/monthvisit/${page}.html`;
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
const novels = [];
loadedCheerio('.module-rank-booklist .book-layout').each(function () {
let novelUrl = loadedCheerio(this).attr('href');
if (novelUrl) {
const novelName = loadedCheerio(this).find('.book-title').text();
const novelCover = loadedCheerio(this)
.find('img.book-cover')
.attr('data-src');
novelUrl = baseUrl + novelUrl;
const novel = {
sourceId,
novelUrl,
novelName,
novelCover,
};
novels.push(novel);
}
});
return { novels };
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
const novel = {
sourceId,
sourceName,
url: novelUrl,
novelUrl,
};
novel.novelName = loadedCheerio('#bookDetailWrapper .book-title').text();
novel.novelCover = loadedCheerio('#bookDetailWrapper img.book-cover').attr(
'src',
);
novel.summary = loadedCheerio('#bookSummary content').text();
novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a a').text();
novel.artist = null;
// TODO: Need some regex and dirty selector to get it
// Need to look into how to translate that message
novel.status = null;
let genres = [];
loadedCheerio(
'#bookDetailWrapper .book-cell .book-meta:last-child .tag-small.red a',
).each(function () {
genres.push(loadedCheerio(this).text());
});
if (genres.length) {
novel.genre = genres.join(', ');
}
// Table of Content is on a different page than the summary page
let chapters = [];
const idPattern = /\/(\d+)\.html/;
const novelId = url.match(idPattern)[1];
const chaptersUrl = baseUrl + loadedCheerio('#btnReadBook').attr('href');
const chaptersBody = await fetchHtml({ url: chaptersUrl, sourceId });
const chaptersLoadedCheerio = cheerio.load(chaptersBody);
let volumeName;
chaptersLoadedCheerio('#volumes .chapter-li').each(function () {
if (chaptersLoadedCheerio(this).hasClass('chapter-bar')) {
volumeName = chaptersLoadedCheerio(this).text();
} else {
const urlPart = chaptersLoadedCheerio(this)
.find('.chapter-li-a')
.attr('href');
const chapterIdMatch = urlPart.match(idPattern);
// Sometimes the href attribute does not contain the url, but javascript:cid(0).
// Increment the previous chapter ID should result in the right URL
if (chapterIdMatch) {
chapterId = chapterIdMatch[1];
} else {
chapterId++;
}
const chapterUrl = `${baseUrl}/novel/${novelId}/${chapterId}.html`;
if (chapterUrl) {
const chapterName =
volumeName +
' — ' +
chaptersLoadedCheerio(this).find('.chapter-index').text().trim();
const releaseDate = null;
chapters.push({
chapterName,
releaseDate,
chapterUrl,
});
}
}
});
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
let chapterName, chapterText, hasNextPage, pageHasNextPage, pageText, urlNext;
let pageNumber = 1;
const delay = ms => new Promise(res => setTimeout(res, ms));
/*
* TODO: Maybe there are other ways to get the translation table
* It is embed and encrypted inside readtool.js
* UPDATE: Decrypted, by skillgg
*/
const mapping_dict = {
'\u201c': '\u300c',
'\u201d': '\u300d',
'\u2018': '\u300e',
'\u2019': '\u300f',
'\ue82c': '\u7684',
'\ue836': '\u4e00',
'\ue852': '\u662f',
'\ue850': '\u4e86',
'\ue832': '\u6211',
'\ue812': '\u4e0d',
'\ue833': '\u4eba',
'\ue849': '\u5728',
'\ue821': '\u4ed6',
'\ue810': '\u6709',
'\ue84c': '\u8fd9',
'\ue815': '\u4e2a',
'\ue842': '\u4e0a',
'\ue82e': '\u4eec',
'\ue817': '\u6765',
'\ue835': '\u5230',
'\ue837': '\u65f6',
'\ue82d': '\u5927',
'\ue859': '\u5730',
'\ue85c': '\u4e3a',
'\ue82f': '\u5b50',
'\ue84d': '\u4e2d',
'\ue854': '\u4f60',
'\ue81e': '\u8bf4',
'\ue853': '\u751f',
'\ue80f': '\u56fd',
'\ue80e': '\u5e74',
'\ue813': '\u7740',
'\ue802': '\u5c31',
'\ue81a': '\u90a3',
'\ue83b': '\u548c',
'\ue851': '\u8981',
'\ue82a': '\u5979',
'\ue838': '\u51fa',
'\ue808': '\u4e5f',
'\ue83a': '\u5f97',
'\ue814': '\u91cc',
'\ue857': '\u540e',
'\ue855': '\u81ea',
'\ue800': '\u4ee5',
'\ue81b': '\u4f1a',
'\ue85f': '\u5bb6',
'\ue816': '\u53ef',
'\ue83e': '\u4e0b',
'\ue84f': '\u800c',
'\ue80b': '\u8fc7',
'\ue828': '\u5929',
'\ue843': '\u53bb',
'\ue806': '\u80fd',
'\ue81f': '\u5bf9',
'\ue834': '\u5c0f',
'\ue81c': '\u591a',
'\ue848': '\u7136',
'\ue830': '\u4e8e',
'\ue84b': '\u5fc3',
'\ue84a': '\u5b66',
'\ue85d': '\u4e48',
'\ue861': '\u4e4b',
'\ue809': '\u90fd',
'\ue80c': '\u597d',
'\ue84e': '\u770b',
'\ue858': '\u8d77',
'\ue840': '\u53d1',
'\ue85b': '\u5f53',
'\ue863': '\u6ca1',
'\ue839': '\u6210',
'\ue827': '\u53ea',
'\ue841': '\u5982',
'\ue805': '\u4e8b',
'\ue845': '\u628a',
'\ue820': '\u8fd8',
'\ue83c': '\u7528',
'\ue847': '\u7b2c',
'\ue819': '\u6837',
'\ue82b': '\u9053',
'\ue80a': '\u60f3',
'\ue822': '\u4f5c',
'\ue85e': '\u79cd',
'\ue801': '\u5f00',
'\ue856': '\u7f8e',
'\ue811': '\u4e73',
'\ue860': '\u9634',
'\ue80d': '\u6db2',
'\ue83f': '\u830e',
'\ue803': '\u6b32',
'\ue804': '\u547b',
'\ue825': '\u8089',
'\ue846': '\u4ea4',
'\ue85a': '\u6027',
'\ue831': '\u80f8',
'\ue81d': '\u79c1',
'\ue826': '\u7a74',
'\ue818': '\u6deb',
'\ue823': '\u81c0',
'\ue829': '\u8214',
'\ue807': '\u5c04',
'\ue862': '\u8131',
'\ue83d': '\u88f8',
'\ue824': '\u9a9a',
'\ue844': '\u5507',
};
const addPage = async pageCheerio => {
const formatPage = async () => {
// Remove JS
let style = pageCheerio('style:first').prop('innerHTML');
style = style.replace(/(.*?)\{.*?\}/g, '$1,').split(',');
style.push(style.pop().replace(/\}/, '.cgo'));
style.map(tag => pageCheerio(tag).remove());
// Load lazyloaded images
pageCheerio('.atitle')
.next()
.find('img.imagecontent')
.each(function () {
// Sometimes images are either in data-src or src
const imgSrc =
pageCheerio(this).attr('data-src') || pageCheerio(this).attr('src');
if (imgSrc) {
// The original CDN URL is locked behind a CF-like challenge, switch the URL to bypass that
// There are no react-native-url-polyfill lib, can't use URL API
const regex = /\/\/.+\.com\//;
const imgUrl = imgSrc.replace(regex, '//img.linovelib.com/');
// Clean up img element
pageCheerio(this)
.attr('src', imgUrl)
.removeAttr('data-src')
.removeClass('lazyload')
.addClass('delayed-src');
}
});
// Recover the original character
pageText = pageCheerio('.atitle').next().html();
pageText = pageText?.replace(/./g, char => mapping_dict[char] || char);
return Promise.resolve();
};
await formatPage();
chapterName =
pageCheerio('.atitle h3').text() + ' — ' + pageCheerio('#atitle').text();
if (chapterText === undefined) {
chapterText = '<h2>' + chapterName + '</h2>';
}
chapterText +=
pageText ||
'Chapter not found, Report at lnreader github/discord if you see this message';
};
const loadPage = async url => {
const body = await fetchHtml({ url, sourceId });
const pageCheerio = cheerio.load(body);
await addPage(pageCheerio);
urlNext = pageCheerio('#aread script:first')
.prop('innerHTML')
.replace(/.*next:'(.*?)'.*/g, '$1');
pageHasNextPage = urlNext.match(/_/) ? true : false;
return { pageCheerio, pageHasNextPage };
};
let url = chapterUrl;
do {
const page = await loadPage(url);
await delay(1000);
hasNextPage = page.pageHasNextPage;
if (hasNextPage === true) {
pageNumber++;
url = chapterUrl.replace(/\.html/gi, `_${pageNumber}` + '.html');
}
} while (hasNextPage === true);
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const searchUrl = `${baseUrl}/search/`;
const Term = encodeURI(searchTerm);
let nextPage, noNextPage, deadEnd;
let pageNumber = 1;
const novels = [];
const addPage = async (pageCheerio, redirect) => {
const loadSearchResults = function () {
pageCheerio('.book-ol .book-layout').each(function () {
let novelUrl = pageCheerio(this).attr('href');
if (novelUrl) {
const novelName = pageCheerio(this).find('.book-title').text();
const novelCover = pageCheerio(this)
.find('img.book-cover')
.attr('data-src');
novelUrl = baseUrl + novelUrl;
const novel = { sourceId, novelUrl, novelName, novelCover };
novels.push(novel);
}
});
};
const novelResults = pageCheerio('.book-ol a.book-layout');
if (novelResults.length === 0) {
showToast('Bypass check by searching in Webview');
} else {
loadSearchResults();
}
if (redirect.length) {
novels.length = 0;
const novelName = pageCheerio('#bookDetailWrapper .book-title').text();
const novelCover = pageCheerio('#bookDetailWrapper img.book-cover').attr(
'src',
);
const novelUrl =
baseUrl +
pageCheerio('#btnReadBook').attr('href').slice(0, -8) +
'.html';
const novel = { sourceId, novelUrl, novelName, novelCover };
novels.push(novel);
}
};
const loadPage = async url => {
const body = await fetchHtml({ url, sourceId });
const pageCheerio = cheerio.load(body);
const redirect = pageCheerio('div.book-layout');
await addPage(pageCheerio, redirect);
nextPage = pageCheerio('.next').attr('href');
if (!nextPage) {
noNextPage === true;
} else {
noNextPage = nextPage === '#' ? true : false;
}
return { pageCheerio, noNextPage };
};
let url = `${searchUrl}${Term}_${pageNumber}.html`;
do {
const page = await loadPage(url);
deadEnd = page.noNextPage;
if (deadEnd === false) {
pageNumber++;
url = `${searchUrl}${Term}_${pageNumber}.html`;
}
} while (deadEnd === false);
return novels;
};
const headers = {
Referer: 'https://w.linovelib.com',
};
const LinovelibScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
headers,
};
export default LinovelibScraper;
+6 -10
View File
@@ -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();
+25 -25
View File
@@ -1,16 +1,11 @@
import { fetchHtml } from '@utils/fetch/fetch';
import * as cheerio from 'cheerio';
import {
SourceChapter,
SourceChapterItem,
SourceNovel,
SourceNovelItem,
} from '../types';
import { SourceChapterItem, SourceNovel, SourceNovelItem } from '../types';
const sourceId = 163;
const sourceName = 'novelsOnline';
// const baseUrl = 'https://novelsonline.net';
const baseUrl = 'https://novelsonline.net';
const searchNovels = async (searchTerm: string) => {
const result = await fetchHtml({
@@ -101,32 +96,37 @@ const parseNovelAndChapters = async (novelUrl: string) => {
return novel;
};
const popularNovels = async () => {
return { novels: [] as SourceNovelItem[] }; /** TO DO */
};
const popularNovels = async (page: number) => {
const url = `${baseUrl}/top-novel/${page}`;
const result = await fetchHtml({ sourceId, url });
let $ = cheerio.load(result);
const parseChapter = async (novelUrl: string, chapterUrl: string) => {
const result = await fetchHtml({ url: chapterUrl });
let loadedCheerio = cheerio.load(result);
const headers = $('div.top-novel-block');
return headers
.map((i, h) => {
const novelName = $(h).find('h2').text();
const novelUrl = $(h).find('a').attr('href');
const novelCover = $(h).find('img').attr('src');
const chapterName = loadedCheerio('h1').text();
const chapterText = loadedCheerio('#contentall').html() || '';
if (!novelUrl) {
return null;
}
const chapter: SourceChapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
return {
sourceId,
novelName,
novelCover,
novelUrl,
} as SourceNovelItem;
})
.get()
.filter(sr => sr !== null);
};
const NovelsOnlineScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
// parseChapter,
searchNovels,
};
+1
View File
@@ -127,6 +127,7 @@ const parseChapter = async (novelUrl: string, chapterUrl: string) => {
const body = await result.text();
const loadedCheerio = cheerio.load(body);
loadedCheerio('#chapter-content div').remove();
const chapterName = loadedCheerio('.chapter-title').attr('title');
const chapterText = loadedCheerio('#chapter-content').html() || '';
-187
View File
@@ -1,187 +0,0 @@
import * as cheerio from 'cheerio';
const baseUrl = 'https://freewebnovel.com/';
const popularNovels = async page => {
let url = baseUrl + 'completed-novel/' + page;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.li-row').each(function () {
const novelName = loadedCheerio(this).find('.tit').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this)
.find('h3 > a')
.attr('href')
.replace('.html', '')
.slice(1);
const novel = {
sourceId: 13,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { novels };
};
const parseNovelAndChapters = async novelUrl => {
const url = `${baseUrl}${novelUrl.replace('/', '')}.html`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel = {};
novel.sourceId = 13;
novel.sourceName = 'FreeWebNovel';
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = loadedCheerio('h1.tit').text();
novel.novelCover = loadedCheerio('.pic > img').attr('src');
novel.genre = loadedCheerio('[title=Genre]')
.next()
.text()
.replace(/[\t\n]/g, '');
novel.author = loadedCheerio('[title=Author]')
.next()
.text()
.replace(/[\t\n]/g, '');
novel.artist = null;
novel.status = loadedCheerio('[title=Status]')
.next()
.text()
.replace(/[\t\n]/g, '');
let novelSummary = loadedCheerio('.inner').text().trim();
novel.summary = novelSummary;
let novelChapters = [];
let latestChapter;
loadedCheerio('h3.tit').each(function (res) {
if (loadedCheerio(this).find('a').text() === novel.novelName) {
latestChapter = loadedCheerio(this)
.next()
.find('span.s3')
.text()
.match(/\d+/);
}
});
latestChapter = latestChapter[0];
for (let i = 1; i <= parseInt(latestChapter, 10); i++) {
const chapterName = 'Chapter ' + i;
const releaseDate = null;
const chapterUrl = 'chapter-' + i;
const chapter = { chapterName, releaseDate, chapterUrl };
novelChapters.push(chapter);
}
novel.chapters = novelChapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
let novelId = novelUrl.replace('/', '');
const url = `${baseUrl}${novelId}/${chapterUrl}.html`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let chapterName = loadedCheerio('h1.tit').text();
let chapterText = loadedCheerio('div.txt').html();
const chapter = {
sourceId: 13,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = baseUrl + 'search/';
const formData = new FormData();
formData.append('searchkey', searchTerm);
const result = await fetch(url, {
method: 'POST',
body: formData,
});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.li-row > .li > .con').each(function () {
const novelName = loadedCheerio(this).find('.tit').text();
const novelCover = loadedCheerio(this)
.find('.pic > a > img')
.attr('data-cfsrc');
let novelUrl = loadedCheerio(this)
.find('h3 > a')
.attr('href')
.replace('.html', '')
.slice(1);
novelUrl += '/';
const novel = {
sourceId: 13,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const FreeWebNovelScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default FreeWebNovelScraper;
+122
View File
@@ -0,0 +1,122 @@
import { Status } from '../helpers/constants';
import * as cheerio from 'cheerio';
import {
SourceChapter,
SourceChapterItem,
SourceNovel,
SourceNovelItem,
} from '../types';
const sourceId = 13;
const sourceName = 'FreeWebNovel';
const baseUrl = 'https://freewebnovel.com';
const popularNovels = async (page: number, { showLatestNovels }) => {
const sort = showLatestNovels
? '/latest-release-novels/'
: '/completed-novels/';
const result = await fetch(baseUrl + sort + page).then(res => res.text());
const loadedCheerio = cheerio.load(result);
const novels: SourceNovelItem[] = loadedCheerio('.li-row')
.map((index, element) => ({
sourceId,
novelName: loadedCheerio(element).find('.tit').text(),
novelCover: loadedCheerio(element).find('img').attr('src'),
novelUrl: baseUrl + loadedCheerio(element).find('h3 > a').attr('href'),
}))
.get();
return { novels };
};
const parseNovelAndChapters = async (novelUrl: string) => {
const result = await fetch(novelUrl).then(res => res.text());
const loadedCheerio = cheerio.load(result);
const novel: SourceNovel = {
sourceId,
sourceName,
novelUrl,
url: novelUrl,
novelName: loadedCheerio('h1.tit').text(),
novelCover: loadedCheerio('.pic > img').attr('src'),
summary: loadedCheerio('.inner').text().trim(),
};
novel.genre = loadedCheerio('[title=Genre]')
.next()
.text()
.replace(/[\t\n]/g, '');
novel.author = loadedCheerio('[title=Author]')
.next()
.text()
.replace(/[\t\n]/g, '');
novel.status =
loadedCheerio('[title=Status]')
.next()
.text()
.replace(/[\t\n]/g, '') === 'OnGoing'
? Status.ONGOING
: Status.COMPLETED;
const chapters: SourceChapterItem[] = loadedCheerio('#idData > li > a')
.map((index, element) => ({
chapterName: loadedCheerio(element).attr('title') || 'Chapter ' + index,
releaseDate: null,
chapterUrl: baseUrl + loadedCheerio(element).attr('href'),
}))
.get();
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl: string, chapterUrl: string) => {
const result = await fetch(chapterUrl).then(res => res.text());
const loadedCheerio = cheerio.load(result);
const chapter: SourceChapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName: loadedCheerio('h1.tit').text(),
chapterText: loadedCheerio('div.txt').html(),
};
return chapter;
};
const searchNovels = async (searchTerm: string) => {
const result = await fetch(baseUrl + '/search/', {
method: 'POST',
body: 'searchkey=' + encodeURIComponent(searchTerm),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
}).then(res => res.text());
const loadedCheerio = cheerio.load(result);
const novels: SourceNovelItem[] = loadedCheerio('.li-row > .li > .con')
.map((index, element) => ({
sourceId,
novelName: loadedCheerio(element).find('.tit').text(),
novelCover: loadedCheerio(element).find('.pic > a > img').attr('src'),
novelUrl: baseUrl + loadedCheerio(element).find('h3 > a').attr('href'),
}))
.get();
return novels;
};
const FreeWebNovelScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default FreeWebNovelScraper;
+137
View File
@@ -0,0 +1,137 @@
import { fetchHtml } from '@utils/fetch/fetch';
import * as cheerio from 'cheerio';
import { FilterInputs } from '../types/filterTypes';
const sourceId = 170;
const sourceName = 'I cant read japanese tl';
const baseUrl = 'https://icantreadjapanese.wordpress.com/';
const popularNovels = async (page, { filters }) => {
let link = `${baseUrl}`;
if (page === 1) {
link += (filters?.storyStatus ? filters.storyStatus : 'projects') + '/';
}
const body = await fetchHtml({ url: link, sourceId });
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('figure.wp-block-image').each(function () {
const novelName = loadedCheerio(this).find('a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this).find('a').attr('href');
const novel = { sourceId, novelName, novelCover, novelUrl };
novels.push(novel);
});
return { novels };
};
const parseNovelAndChapters = async novelUrl => {
const body = await fetchHtml({ url: novelUrl, sourceId: sourceId });
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url: novelUrl,
novelUrl,
};
novel.novelName = loadedCheerio('h1.entry-title').text().trim();
novel.novelCover = loadedCheerio('figure.wp-block-image > img').attr('src');
novel.author = loadedCheerio('.has-text-align-left')
.text()
.replace(/.*Author: (.*) \|.*/g, '$1');
novel.summary = loadedCheerio('.entry-content > p:first')
.nextUntil('hr:first')
.text();
let chapters = [];
loadedCheerio('.wp-block-column li a').each(function () {
let chapterName = loadedCheerio(this).text();
const releaseDate = null;
const chapterUrl = loadedCheerio(this).attr('href');
const volumeName = loadedCheerio(this)
.parent()
.prop('innerHTML')
.replace(/<a.*>/g, '')
.replace(/<br>/g, ' ')
.trim();
if (volumeName.length) {
chapterName = volumeName + ' - ' + chapterName;
}
chapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const body = await fetchHtml({
url: chapterUrl,
sourceId: sourceId,
});
let loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h1.entry-title').text();
const chapterText = loadedCheerio('.entry-content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async () => {
showToast('Search is not implemented for this source');
return;
};
const filters = [
{
key: 'storyStatus',
label: 'Translation Status',
values: [
{ label: 'Ongoing Projects', value: 'projects' },
{
label: 'Maybe Will Read Again, Caught Up',
value: 'maybe-will-read-again-caught-up',
},
{ label: 'Dropped', value: 'dropped' },
{ label: 'Finished', value: 'finished' },
],
inputType: FilterInputs.Picker,
},
];
const ICantReadJPTLScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
filters,
};
export default ICantReadJPTLScraper;
-1
View File
@@ -111,7 +111,6 @@ const parseNovelAndChapters = async (novelUrl: string) => {
return novelChapters;
};
console.log(novelId);
if (novelId) {
novel.chapters = await getChapters(novelId);
+6 -4
View File
@@ -87,15 +87,17 @@ const parseNovelAndChapters = async novelUrl => {
const chaptersHtml = await fetchHtml({
url: chaptersUrl,
init: { headers },
sourceId,
});
loadedCheerio = cheerio.load(chaptersHtml);
loadedCheerio('.chapter-list li').each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter-title')
.text()
.trim();
const chapterName =
'Chapter ' +
loadedCheerio(this).find('.chapter-no').text().trim() +
' - ';
loadedCheerio(this).find('.chapter-title').text().trim();
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
+37 -35
View File
@@ -1,6 +1,5 @@
import * as cheerio from 'cheerio';
import { showToast } from '../../hooks/showToast';
import { htmlToText } from '../helpers/htmlToText';
const baseUrl = 'https://lnmtl.com/';
@@ -56,24 +55,26 @@ const parseNovelAndChapters = async novelUrl => {
novel.novelName = loadedCheerio('.novel-name').text();
novel.novelCover = loadedCheerio('div.novel').find('img').attr('src');
novel.novelCover = loadedCheerio('.novel').find('img').attr('src');
novel.summary = loadedCheerio('div.description').text().trim();
novel.summary = loadedCheerio('.description').text().trim();
novel.author = loadedCheerio(
'main > div:nth-child(3) > div > div.col-lg-3.col-md-4 > div:nth-child(2) > div.panel-body > dl:nth-child(1) > dd > a > span',
).text();
loadedCheerio('.panel-body > dl').each(function () {
let detailName = loadedCheerio(this).find('dt').text().trim();
let detail = loadedCheerio(this).find('dd').text().trim();
novel.status = loadedCheerio(
'main > div:nth-child(3) > div > div.col-lg-3.col-md-4 > div:nth-child(2) > div.panel-body > dl:last-child > dd',
)
.text()
.trim();
console.log(novel.status);
switch (detailName) {
case 'Authors':
novel.author = detail;
break;
case 'Current status':
novel.status = detail;
break;
}
});
novel.genre = loadedCheerio(
'main > div.container > div > div.col-lg-3.col-md-4 > div:nth-child(4) > div.panel-body > ul',
)
novel.genre = loadedCheerio('.panel-heading:contains(" Genres ")')
.next()
.text()
.trim()
.replace(/\s\s/g, ',');
@@ -129,17 +130,15 @@ const parseChapter = async (novelUrl, chapterUrl) => {
let chapterName = loadedCheerio('h3 > span.chapter-title').text().trim();
loadedCheerio('.original').remove();
loadedCheerio('.original, script').remove();
loadedCheerio('sentence.translated').wrap('<p></p>');
let chapterText = loadedCheerio('.chapter-body').html();
let chapterText = loadedCheerio('.chapter-body').html().replace(/„/g, '“');
if (!chapterText) {
chapterText = loadedCheerio('.alert.alert-warning').text();
}
chapterText =
chapterName + '\n\n' + htmlToText(chapterText, { removeLineBreaks: false });
const chapter = {
sourceId: 37,
novelUrl,
@@ -152,32 +151,35 @@ const parseChapter = async (novelUrl, chapterUrl) => {
};
const searchNovels = async searchTerm => {
const url = 'https://lnmtl.com/term';
const result = await fetch(url);
const result = await fetch(baseUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = loadedCheerio('footer')
const list = loadedCheerio('footer')
.next()
.next()
.html()
.match(/local: \[(.*?)\]/)[0]
.replace('local: ', '');
.match(/prefetch: '\/(.*json)/)[1];
novels = JSON.parse(novels);
const search = await fetch(`${baseUrl}${list}`);
const data = await search.json();
novels = novels.filter(novel =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase()),
let nov = data.filter(res =>
res.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
novels = novels.map(novel => ({
sourceId: 37,
novelName: novel.name,
novelUrl: novel.slug,
novelCover: novel.image,
}));
const novels = [];
nov.map(res => {
const novelName = res.name;
const novelUrl = res.slug;
const novelCover = res.image;
const novel = { sourceId: 37, novelName, novelUrl, novelCover };
novels.push(novel);
});
return novels;
};
@@ -2,8 +2,8 @@ import { fetchHtml } from '@utils/fetch/fetch';
import * as cheerio from 'cheerio';
const sourceId = 3;
const baseUrl = 'https://fastnovel.org';
const searchUrl = 'https://fastnovel.org/search/';
const baseUrl = 'https://novelbin.org';
const searchUrl = 'https://novelbin.org/search/';
const popularNovels = async page => {
const url = `${baseUrl}/sort/p/?page=${page}`;
@@ -14,7 +14,7 @@ const popularNovels = async page => {
let novels = [];
loadedCheerio('.list-novel .row').each(function () {
loadedCheerio('.col-novel-main .list-novel .row').each(function () {
const novelName = loadedCheerio(this).find('h3.novel-title > a').text();
const novelCover = loadedCheerio(this).find('img.cover').attr('src');
const novelUrl = loadedCheerio(this)
@@ -42,7 +42,7 @@ const parseNovelAndChapters = async novelUrl => {
let novel = {
sourceId,
sourceName: 'FastNovel',
sourceName: 'NovelBin',
url,
novelUrl,
};
@@ -53,44 +53,41 @@ const parseNovelAndChapters = async novelUrl => {
novel.summary = loadedCheerio('div.desc-text').text().trim();
novel.author = loadedCheerio('ul.info > li > h3')
.filter(function () {
return loadedCheerio(this).text().trim() === 'Author:';
})
.siblings()
.text();
novel.genre = loadedCheerio('ul.info > li')
.filter(function () {
return loadedCheerio(this).find('h3').text().trim() === 'Genre:';
})
.text()
.trim()
.replace('Genre:', '')
.trim();
novel.status = loadedCheerio('ul.info > li > h3')
.filter(function () {
return loadedCheerio(this).text().trim() === 'Status:';
})
.next()
.text();
loadedCheerio('ul.info > li > h3').each(function () {
let detailName = loadedCheerio(this).text();
let detail = loadedCheerio(this)
.siblings()
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
switch (detailName) {
case 'Author:':
novel.author = detail;
break;
case 'Status:':
novel.status = detail;
break;
case 'Genre:':
novel.genre = detail;
break;
}
});
const novelId = loadedCheerio('#rating').attr('data-novel-id');
const getChapters = async id => {
const chapterListUrl = baseUrl + '/ajax/chapter-archive?novelId=' + id;
const data = await fetch(chapterListUrl);
const chapters = await data.text();
const chapters = await fetchHtml({ url: chapterListUrl, sourceId });
loadedCheerio = cheerio.load(chapters);
let novelChapters = [];
loadedCheerio('select > option').each(function () {
let chapterName = loadedCheerio(this).text();
loadedCheerio('.list-chapter > li').each(function () {
let chapterName = loadedCheerio(this).find('span').text();
let releaseDate = null;
let chapterUrl = loadedCheerio(this).attr('value');
let chapterUrl = loadedCheerio(this).find('a').attr('href');
if (chapterUrl) {
novelChapters.push({
@@ -120,6 +117,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const chapterName = loadedCheerio('.chr-text').text();
loadedCheerio('#chr-content > div,h6,p[style="display: none;"]').remove();
let chapterText = loadedCheerio('#chr-content').html();
const chapter = {
sourceId,
@@ -134,21 +132,19 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const searchNovels = async searchTerm => {
const url = searchUrl + '?keyword=' + searchTerm;
const result = await fetch(url);
const body = await result.text();
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('div.col-novel-main > div.list-novel > .row').each(function () {
loadedCheerio('.col-novel-main .list-novel .row').each(function () {
const novelName = loadedCheerio(this).find('h3.novel-title > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this)
.find('h3.novel-title > a')
.attr('href');
const novelName = loadedCheerio(this).find('h3.novel-title > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
if (novelUrl) {
novels.push({
sourceId,
@@ -162,11 +158,11 @@ const searchNovels = async searchTerm => {
return novels;
};
const fastNovelScraper = {
const NovelBinScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default fastNovelScraper;
export default NovelBinScraper;
+41 -32
View File
@@ -1,4 +1,6 @@
import * as cheerio from 'cheerio';
import { fetchApi, fetchHtml } from '@utils/fetch/fetch';
const baseUrl = 'https://www.novelpub.com/';
const sourceName = 'NovelPub';
@@ -7,15 +9,12 @@ const sourceId = 94;
const headers = new Headers({
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
});
const popularNovels = async page => {
let url = baseUrl + 'browse/all/popular/all/' + page;
const result = await fetch(url, { method: 'GET', headers });
const body = await result.text();
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
@@ -41,45 +40,39 @@ const popularNovels = async page => {
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url, { method: 'GET', headers });
const body = await result.text();
const body = await fetchHtml({ url, sourceId });
let loadedCheerio = cheerio.load(body);
let novel = { url, novelUrl, sourceId, sourceName, genre: '' };
let novel = {
sourceId,
url: novelUrl,
novelUrl,
sourceName,
};
novel.novelName = loadedCheerio('h1.novel-title').text().trim();
novel.novelCover = loadedCheerio('figure.cover > img').attr('data-src');
loadedCheerio('div.categories > ul > li').each(function () {
novel.genre +=
loadedCheerio(this)
.text()
.replace(/[\t\n]/g, '') + ',';
});
novel.genre = loadedCheerio('.categories li')
.find('a')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
loadedCheerio('div.header-stats > span').each(function () {
if (loadedCheerio(this).find('small').text() === 'Status') {
novel.status = loadedCheerio(this).find('strong').text();
}
});
novel.genre = novel.genre.slice(0, -1);
novel.status = loadedCheerio('small:contains("Status")').prev().text().trim();
novel.author = loadedCheerio('.author > a > span').text();
loadedCheerio('.expand').remove();
novel.summary = loadedCheerio('.summary > .content').text().trim();
const delay = ms => new Promise(res => setTimeout(res, ms));
let lastPage = 1;
lastPage = loadedCheerio(
'#novel > header > div.header-body.container > div.novel-info > div.header-stats > span:nth-child(1) > strong',
)
.text()
?.trim();
lastPage = loadedCheerio('small:contains("Chapters")').prev().text().trim();
lastPage = Math.ceil(lastPage / 100);
@@ -89,8 +82,11 @@ const parseNovelAndChapters = async novelUrl => {
for (let i = 1; i <= lastPage; i++) {
const chaptersUrl = `${novelUrl}/chapters/page-${i}`;
const chaptersRequest = await fetch(chaptersUrl, { headers });
const chaptersHtml = await chaptersRequest.text();
const chaptersHtml = await fetchHtml({
url: chaptersUrl,
init: { headers },
sourceId,
});
loadedCheerio = cheerio.load(chaptersHtml);
@@ -125,9 +121,8 @@ const parseNovelAndChapters = async novelUrl => {
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url, { method: 'GET', headers });
const body = await fetchHtml({ url, sourceId });
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h2').text();
@@ -139,10 +134,24 @@ const parseChapter = async (novelUrl, chapterUrl) => {
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}lnwsearchlive?inputContent=${searchTerm}`;
const url = `${baseUrl}lnsearchlive`;
const link = `${baseUrl}search`;
const response = await fetchApi({ url: link, sourceId }).then(r => r.text());
const token = cheerio.load(response);
let verifytoken = token('#novelSearchForm > input').attr('value');
const result = await fetch(url, { method: 'GET', headers });
const body = await result.text();
let formData = new FormData();
formData.append('inputContent', searchTerm);
const body = await fetchHtml({
url,
init: {
method: 'POST',
headers: { 'LNRequestVerifyToken': verifytoken },
body: formData,
},
sourceId,
});
let loadedCheerio = cheerio.load(body);
+31 -11
View File
@@ -1,15 +1,20 @@
import * as cheerio from 'cheerio';
import { fetchApi, fetchHtml, cloudflareCheck } from '@utils/fetch/fetch';
import {
fetchApi,
fetchHtml,
cloudflareCheck,
defaultUserAgentString,
} from '@utils/fetch/fetch';
import { defaultTo } from 'lodash-es';
import { FilterInputs } from '../types/filterTypes';
const sourceId = 50;
const sourceName = 'Novel Updates';
const baseUrl = 'https://www.novelupdates.com/';
const userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36';
const userAgent = defaultUserAgentString;
const getPopularNovelsUrl = (page, { showLatestNovels, filters }) => {
let url = `${baseUrl}${
@@ -105,7 +110,9 @@ const parseNovelAndChapters = async novelUrl => {
novel.novelName = loadedCheerio('.seriestitlenu').text();
novel.novelCover = loadedCheerio('.seriesimg > img').attr('src');
novel.novelCover =
loadedCheerio('.seriesimg > img').attr('src') ||
loadedCheerio('.serieseditimg > img').attr('src');
novel.author = loadedCheerio('#showauthors').text().trim();
novel.genre = loadedCheerio('#seriesgenre')
@@ -204,6 +211,12 @@ const parseChapter = async (novelUrl, chapterUrl) => {
let isWattpad = result.url.toLowerCase().includes('wattpad');
let isAnomalously = result.url.toLowerCase().includes('anotivereads');
let isBlossomTranslation = result.url
.toLowerCase()
.includes('blossomtranslation');
let isLightNovelsTls = result.url
.toLowerCase()
.includes('lightnovelstranslations');
@@ -241,32 +254,39 @@ const parseChapter = async (novelUrl, chapterUrl) => {
if (isWuxiaWorld) {
chapterText = loadedCheerio('#chapter-content').html();
} else if (isRainOfSnow) {
chapterText = loadedCheerio('div.content').html();
chapterText = loadedCheerio('.content').html();
} else if (isTumblr) {
chapterText = loadedCheerio('.post').html();
} else if (isBlogspot) {
loadedCheerio('.post-share-buttons').remove();
chapterText = loadedCheerio('.entry-content').html();
chapterText =
loadedCheerio('.entry-content').html() ||
loadedCheerio('#post-body').html();
} else if (isHostedNovel) {
chapterText = loadedCheerio('.chapter').html();
} else if (isScribbleHub) {
chapterText = loadedCheerio('div.chp_raw').html();
chapterText = loadedCheerio('.chp_raw').html();
} else if (isWattpad) {
chapterText = loadedCheerio('.container pre').html();
} else if (isTravisTranslation) {
chapterText = loadedCheerio('.reader-content').html();
} else if (isLightNovelsTls) {
chapterText = loadedCheerio('.text_story').html();
} else if (isBlossomTranslation) {
chapterText = loadedCheerio('.manga-child-content').html();
} else if (isAnomalously) {
chapterText = loadedCheerio('#comic').html();
} else if (isiNovelTranslation) {
const link = 'https://api.' + result.url.slice(8);
const json = await fetchApi({
url: link,
sourceId,
}).then(r => r.json());
chapterText =
json.content.replace(/\n/g, '<br>') +
'<br><hr><br>TL Notes:<br>' +
json.notes.replace(/\n/g, '<br>');
chapterText = json.content.replace(/\n/g, '<br>');
if (json.notes) {
chapterText +=
'<br><hr><br>TL Notes:<br>' + json.notes.replace(/\n/g, '<br>');
}
} else if (isWordPress) {
/**
* Remove wordpress bloat tags
+1 -1
View File
@@ -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}/`;
+1 -1
View File
@@ -127,7 +127,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const loadedCheerio = cheerio.load(body);
let chapterName = loadedCheerio('div.chapter-content').find('strong').text();
let chapterName = loadedCheerio('h1.font-white').text();
let chapterText = loadedCheerio('div.chapter-content').html();
+7 -7
View File
@@ -112,15 +112,15 @@ const parseNovelAndChapters = async novelUrl => {
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}series/${novelUrl}/${chapterUrl}/`;
const url = `${baseUrl}series/${novelUrl}${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h1#chapter-heading').text();
const chapterName = loadedCheerio('.reading-content b:first').text();
loadedCheerio('input, .reading-content b:first').remove();
let chapterText = loadedCheerio('.reading-content').html();
const chapter = {
@@ -144,11 +144,11 @@ const searchNovels = async searchTerm => {
let novels = [];
loadedCheerio('.c-tabs-item__content').each(function () {
const novelName = loadedCheerio(this).find('.h4 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
loadedCheerio('.tab-thumb').each(function () {
const novelName = loadedCheerio(this).find('a').attr('title');
const novelCover = loadedCheerio(this).find('img').attr('data-src');
let novelUrl = loadedCheerio(this).find('.h4 > a').attr('href');
let novelUrl = loadedCheerio(this).find('a').attr('href');
novelUrl = novelUrl.replace(`${baseUrl}series/`, '');
const novel = {
+17 -25
View File
@@ -23,12 +23,12 @@ const popularNovels = async page => {
if (novelName) {
const novelId = loadedCheerio(this).find('td:nth-child(1)').text();
const novelCover = baseUrl + '/data/image/' + novelId + '.jpg';
const novelCover = baseUrl + 'data/image/' + novelId + '.jpg';
let novelUrl = loadedCheerio(this)
.find('td.novel > a')
.attr('href')
.replace('/novel/', '');
.replace('/book/', '');
const novel = {
sourceId,
@@ -45,7 +45,7 @@ const popularNovels = async page => {
};
const parseNovelAndChapters = async novelUrl => {
const url = `${baseUrl}novel/${novelUrl}/`;
const url = `${baseUrl}book/${novelUrl}/`;
const result = await fetch(url);
const body = await result.text();
@@ -59,25 +59,19 @@ const parseNovelAndChapters = async novelUrl => {
novelUrl,
};
novel.novelName = loadedCheerio(
'body > div.container-fluid.text-center > div.row.content > div.col-sm-8.text-left > div:nth-child(6) > div.panel-heading.clearfix > h4',
).text();
novel.novelName = loadedCheerio('h4[itemprop="name>"]:first').text();
novel.novelCover = loadedCheerio('img[itemprop="image"]').attr('src');
novel.novelCover = loadedCheerio('.img-thumbnail').attr('src');
novel.author = loadedCheerio(
'div.col-md-6 > div > div:nth-child(2) > div > div:nth-child(2) > a',
).text();
novel.author = loadedCheerio('h4:contains("Author:")').next().text();
novel.genre = loadedCheerio(
'div.row > div.col-md-6 > div > a:nth-child(4)',
).text();
novel.summary = loadedCheerio('div[itemprop="description"]')
.find('p')
.text()
.trim();
novel.genre = loadedCheerio('h4:contains("Genre:")')
.nextUntil('h4')
.map((i, el) => loadedCheerio(el).text())
.toArray()
.join(',');
novel.summary = loadedCheerio('h4:contains("Description:")').next().text();
let novelChapters = [];
loadedCheerio('table#chplist > #chapters > tr').each(function () {
@@ -137,20 +131,18 @@ const parseNovelAndChapters = async novelUrl => {
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}novel/${novelUrl}/${chapterUrl}`;
const url = `${baseUrl}book/${novelUrl}/${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let chapterName = loadedCheerio('div.panel-body.article').find('h4').text();
let chapterName = loadedCheerio('.panel-body.article').find('h4').text();
loadedCheerio('ul.pager').remove();
loadedCheerio('ul.pager, .panel-body.article div,button,span').remove();
let chapterText = loadedCheerio('div.panel-body.article')
.html()
.replace(/<span(.*?)>(.*?)<\/span>/g, '');
let chapterText = loadedCheerio('.panel-body.article').html();
const chapter = {
sourceId,
@@ -184,7 +176,7 @@ const searchNovels = async searchTerm => {
let novelUrl = loadedCheerio(this)
.find('a')
.attr('href')
.replace(baseUrl + 'novel/', '');
.replace(baseUrl + 'book/', '');
const novel = {
sourceId,
+193
View File
@@ -0,0 +1,193 @@
import * as cheerio from 'cheerio';
const baseUrl = 'https://es.mtlnovel.com';
const popularNovels = async page => {
const url = `${baseUrl}/alltime-rank/page/${page}`;
// console.log(url);
let headers = new Headers({
referer: 'https://es.mtlnovel.com/',
'User-Agent':
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const result = await fetch(url, {
method: 'GET',
headers: headers,
});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('div.box.wide').each(function () {
const novelName = loadedCheerio(this).find('a.list-title').text().slice(4);
const novelCover = loadedCheerio(this).find('amp-img').attr('src');
let novelUrl = loadedCheerio(this).find('a.list-title').attr('href');
novelUrl = novelUrl.replace('https://es.mtlnovel.com/', '');
const novel = {
sourceId: 173,
novelUrl,
novelName,
novelCover,
};
novels.push(novel);
});
return { novels };
};
const parseNovelAndChapters = async novelUrl => {
const url = `${baseUrl}/${novelUrl}`;
let headers = new Headers({
referer: 'https://es.mtlnovel.com/alltime-rank/',
'User-Agent':
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const result = await fetch(url, {
method: 'GET',
headers: headers,
});
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {};
novel.sourceId = 173;
novel.sourceName = 'Esmtlnovel';
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = loadedCheerio('h1.entry-title').text();
novel.novelCover = loadedCheerio('.nov-head > amp-img').attr('src');
novel.summary = loadedCheerio('div.desc > h2').next().text().trim();
novel.author = loadedCheerio('tr > td')
.filter(function () {
return loadedCheerio(this).prev().text().trim() === 'Author';
})
.next()
.text()
.replace('Auhtor:', '');
novel.status = loadedCheerio('tr > td')
.filter(function () {
return loadedCheerio(this).prev().text().trim() === 'Status';
})
.next()
.text()
.replace('Status:', '');
novel.genre = loadedCheerio('td')
.filter(function () {
return loadedCheerio(this).prev().text().trim() === 'Genre';
})
.next()
.text()
.replace('Genre:', '');
novel.artist = null;
const chapterListUrl = url + '/chapter-list/';
const getChapters = async () => {
const listResult = await fetch(chapterListUrl);
const listBody = await listResult.text();
loadedCheerio = cheerio.load(listBody);
let novelChapters = [];
loadedCheerio('div.ch-list')
.find('a.ch-link')
.each(function () {
const chapterName = loadedCheerio(this).text().replace('~ ', '');
const releaseDate = null;
let chapterUrl = loadedCheerio(this).attr('href');
chapterUrl = chapterUrl.replace(
`https://es.mtlnovel.com/${novelUrl}`,
'',
);
novelChapters.push({
chapterUrl,
chapterName,
releaseDate,
});
});
return novelChapters.reverse();
};
novel.chapters = await getChapters();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}/${novelUrl}${chapterUrl}`;
// console.log(url);
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h1.main-title').text();
let chapterText = loadedCheerio('div.par').html();
const chapter = {
sourceId: 173,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const searchUrl =
'https://es.mtlnovel.com/wp-admin/admin-ajax.php?action=autosuggest&q=' +
searchTerm +
'&__amp_source_origin=https%3A%2F%2Fes.mtlnovel.com';
const res = await fetch(searchUrl);
const result = await res.json();
let novels = [];
result.items[0].results.map(item => {
const novelName = item.title.replace(/<\/?strong>/g, '');
const novelCover = item.thumbnail;
const novelUrl = item.permalink.replace('https://es.mtlnovel.com/', '');
const novel = { sourceId: 173, novelName, novelCover, novelUrl };
novels.push(novel);
});
return novels;
};
const mtlNovelEsScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default mtlNovelEsScraper;
+6 -2
View File
@@ -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();
+193
View File
@@ -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;
+27 -28
View File
@@ -1,9 +1,14 @@
import * as cheerio from 'cheerio';
import { htmlToText } from '../helpers/htmlToText';
const baseUrl = 'https://novelasligera.com/';
const sourceName = 'Novelas Ligera';
const sourceId = 26;
const popularNovels = async page => {
if (page > 1) {
return { novels: [] };
}
let url = baseUrl;
const result = await fetch(url);
@@ -18,14 +23,14 @@ const popularNovels = async page => {
.find('.widget-image-caption.wp-caption-text')
.text();
if (novelName) {
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelCover = loadedCheerio(this).find('img').attr('data-lazy-src');
let novelUrl = loadedCheerio(this).find('a').attr('href');
novelUrl = novelUrl.replace(baseUrl, '');
novelUrl = novelUrl.replace('novela/', '');
const novel = {
sourceId: 26,
sourceId,
novelName,
novelCover,
novelUrl,
@@ -41,30 +46,24 @@ const popularNovels = async page => {
const parseNovelAndChapters = async novelUrl => {
const url = baseUrl + 'novela/' + novelUrl;
// console.log(url);
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {};
novel.sourceId = 26;
novel.sourceName = 'Novelas Ligera';
novel.url = url;
novel.novelUrl = novelUrl;
let novel = {
sourceId,
sourceName,
url,
novelUrl,
};
novel.novelName = loadedCheerio('h1').text();
novel.novelCover = loadedCheerio('.elementor-widget-container')
.find('img')
.attr('src');
.attr('data-lazy-src');
loadedCheerio('.elementor-row')
loadedCheerio('.elementor-widget-container')
.find('p')
.each(function () {
if (loadedCheerio(this).text().includes('Autor:')) {
@@ -83,11 +82,16 @@ const parseNovelAndChapters = async novelUrl => {
}
});
novel.artist = null;
novel.novelSummary = loadedCheerio(
'.elementor-text-editor.elementor-clearfix',
).text();
const summary = loadedCheerio(
'div[data-widget_type="text-editor.default"] .elementor-widget-container',
).first();
summary.find('div.yasr-visitor-votes').remove();
novel.summary = summary
.children()
.toArray()
.filter(e => e.childNodes[0]?.type === 'text')
.map(e => e.childNodes[0].data)
.join('\n\n');
let novelChapters = [];
@@ -115,7 +119,6 @@ const parseNovelAndChapters = async novelUrl => {
const parseChapter = async (novelUrl, chapterUrl) => {
const url = baseUrl + 'novela/' + chapterUrl;
// console.log(url);
const result = await fetch(url);
const body = await result.text();
@@ -131,7 +134,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
loadedCheerio('.wp-post-navigation').remove();
let chapterText = loadedCheerio('.entry-content').html();
chapterText = htmlToText(chapterText, { preserveNewlines: true });
const chapter = {
sourceId: 26,
@@ -146,7 +148,6 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const searchNovels = async searchTerm => {
const url = baseUrl + '?s=' + searchTerm + '&post_type=wp-manga';
// console.log(url);
const result = await fetch(url);
const body = await result.text();
@@ -156,7 +157,7 @@ const searchNovels = async searchTerm => {
let novels = [];
loadedCheerio('.inside-article').each(function () {
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelCover = loadedCheerio(this).find('img').attr('data-lazy-src');
let novelUrl = loadedCheerio(this).find('a').attr('href').split('/')[4];
let novelName;
@@ -179,8 +180,6 @@ const searchNovels = async searchTerm => {
novels.push(novel);
});
novels = [{ ...novels[1] }];
return novels;
};

Some files were not shown because too many files have changed in this diff Show More