Compare commits

...

13 Commits

Author SHA1 Message Date
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
43 changed files with 1059 additions and 682 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.17"
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.17](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'
+1 -1
View File
@@ -133,7 +133,7 @@ def reactNativeArchitectures() {
ext {
versionMajor = 1
versionMinor = 1
versionPatch = 15
versionPatch = 17
}
android {
+144 -146
View File
@@ -1,14 +1,13 @@
{
"name": "lnreader",
"version": "1.1.15",
"version": "1.1.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lnreader",
"version": "1.1.15",
"version": "1.1.17",
"dependencies": {
"@cd-z/react-native-epub-creator": "^2.0.4",
"@gorhom/bottom-sheet": "^4.4.5",
"@react-native-async-storage/async-storage": "^1.18.1",
"@react-native-community/clipboard": "^1.5.1",
@@ -54,7 +53,6 @@
"react-native-pager-view": "^6.2.0",
"react-native-paper": "^5.7.2",
"react-native-reanimated": "~2.9.1",
"react-native-saf-x": "^2.2.1",
"react-native-safe-area-context": "4.3.1",
"react-native-screens": "~3.15.0",
"react-native-shimmer-placeholder": "^2.0.9",
@@ -62,7 +60,6 @@
"react-native-tts": "^4.1.0",
"react-native-vector-icons": "^9.0.0",
"react-native-webview": "11.23.0",
"react-native-zip-archive": "^6.0.9",
"react-redux": "^7.2.6",
"redux-persist": "^6.0.0",
"rn-fetch-blob": "^0.12.0",
@@ -2086,40 +2083,6 @@
"react": ">=16.3.0"
}
},
"node_modules/@cd-z/epub-constructor": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@cd-z/epub-constructor/-/epub-constructor-2.2.2.tgz",
"integrity": "sha512-45/zouZcN7txT6spb8ddW3Wmo0MLdOhXqePOUVKODXSjnpYPEjSdkfIeu/Xt4p80q80lxGi52zeYAQ4XTVAAsw==",
"dependencies": {
"node-html-parser": "^4.1.4"
}
},
"node_modules/@cd-z/react-native-epub-creator": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@cd-z/react-native-epub-creator/-/react-native-epub-creator-2.0.4.tgz",
"integrity": "sha512-bVCE0J9lNjT2JaEVqfjU4kI5Fq4cI4KObWnNfo0NBeCjU89s6+3oHme1yKI/Xm2cq7QDWIklRW/WPXGAO4IE1A==",
"dependencies": {
"@cd-z/epub-constructor": "^2.1.0",
"expo-file-system": "^15.2.2",
"react-native-saf-x": "^2.2.1",
"react-native-zip-archive": "^6.0.3"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@cd-z/react-native-epub-creator/node_modules/expo-file-system": {
"version": "15.4.2",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-15.4.2.tgz",
"integrity": "sha512-WFaEWuFEuUpETiq85YlhKYJgedccWTjtCMnYGAgyNfCfvnIgfMCVH7dWudGuxhfAcTZqh36OcqtSckbtbhOtyg==",
"dependencies": {
"uuid": "^3.4.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -10001,14 +9964,6 @@
"node": ">=0.10.0"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/hermes-engine": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz",
@@ -13247,78 +13202,6 @@
"node": ">= 6.13.0"
}
},
"node_modules/node-html-parser": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-4.1.5.tgz",
"integrity": "sha512-NLgqUXtftqnBqIjlRjYSaApaqE7TTxfTiH4VqKCjdUJKFOtUzRwney83EHz2qYc0XoxXAkYdmLjENCuZHvsIFg==",
"dependencies": {
"css-select": "^4.1.3",
"he": "1.2.0"
}
},
"node_modules/node-html-parser/node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.0.1",
"domhandler": "^4.3.1",
"domutils": "^2.8.0",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/node-html-parser/node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/node-html-parser/node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"dependencies": {
"domelementtype": "^2.2.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/node-html-parser/node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
"dependencies": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/node-html-parser/node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -14823,14 +14706,15 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-native": {
"version": "0.69.12",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.12.tgz",
"integrity": "sha512-kHOj4V0wD/hvjyiiXPP96fMBSRNnx0MXGTTrL43KppsWvKnOtvnQ/qSTWR0d3s6Hi4n7mx3Un+NqCuBKsf+5yg==",
"version": "0.69.9",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.9.tgz",
"integrity": "sha512-I1xqIn47RWxBToO4E6yqyIPSaK9mZnMiscMfrFpWjQr3Gdkicr9y+twmtrRszxaLdQLjHzh/M3y4qOqc3hZnpg==",
"deprecated": "Issues and pull requests filed against this version are not supported. See the React Native release support policy to learn more: https://github.com/reactwg/react-native-releases#releases-support-policy",
"dependencies": {
"@jest/create-cache-key-function": "^27.0.1",
"@react-native-community/cli": "^8.0.7",
"@react-native-community/cli-platform-android": "^8.0.5",
"@react-native-community/cli-platform-ios": "^8.0.6",
"@react-native-community/cli": "^8.0.4",
"@react-native-community/cli-platform-android": "^8.0.4",
"@react-native-community/cli-platform-ios": "^8.0.4",
"@react-native/assets": "1.0.0",
"@react-native/normalize-color": "2.0.0",
"@react-native/polyfills": "2.0.0",
@@ -14842,9 +14726,9 @@
"invariant": "^2.2.4",
"jsc-android": "^250230.2.1",
"memoize-one": "^5.0.0",
"metro-react-native-babel-transformer": "0.70.4",
"metro-runtime": "0.70.4",
"metro-source-map": "0.70.4",
"metro-react-native-babel-transformer": "0.70.3",
"metro-runtime": "0.70.3",
"metro-source-map": "0.70.3",
"mkdirp": "^0.5.1",
"nullthrows": "^1.1.1",
"pretty-format": "^26.5.2",
@@ -15095,15 +14979,6 @@
"react-native": "*"
}
},
"node_modules/react-native-saf-x": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-native-saf-x/-/react-native-saf-x-2.2.1.tgz",
"integrity": "sha512-WL8EFpmjlTTVrHkIFmLzfux3ZpG+XM32VLjggEvGoz8rMq6bCFqH/SlqudHfnAUdFrWprQwypJ451xcQ5Tqwgw==",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-safe-area-context": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.3.1.tgz",
@@ -15267,15 +15142,6 @@
"node": ">=8"
}
},
"node_modules/react-native-zip-archive": {
"version": "6.0.9",
"resolved": "https://registry.npmjs.org/react-native-zip-archive/-/react-native-zip-archive-6.0.9.tgz",
"integrity": "sha512-IYf3yHJ7sHzj9ucO3Amx/TtsZcTgHdfj+Dar8p1e32E+wBsn7mv0PDp2X/oCLGci0nG9i2DBstrnCW74/7NLYQ==",
"peerDependencies": {
"react": ">=16.8.6",
"react-native": ">=0.60.0"
}
},
"node_modules/react-native/node_modules/@jest/types": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
@@ -15357,6 +15223,130 @@
"node": ">=8"
}
},
"node_modules/react-native/node_modules/metro-babel-transformer": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.70.3.tgz",
"integrity": "sha512-bWhZRMn+mIOR/s3BDpFevWScz9sV8FGktVfMlF1eJBLoX24itHDbXvTktKBYi38PWIKcHedh6THSFpJogfuwNA==",
"dependencies": {
"@babel/core": "^7.14.0",
"hermes-parser": "0.6.0",
"metro-source-map": "0.70.3",
"nullthrows": "^1.1.1"
}
},
"node_modules/react-native/node_modules/metro-react-native-babel-preset": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.3.tgz",
"integrity": "sha512-4Nxc1zEiHEu+GTdEMEsHnRgfaBkg8f/Td3+FcQ8NTSvs+xL3LBrQy6N07idWSQZHIdGFf+tTHvRfSIWLD8u8Tg==",
"dependencies": {
"@babel/core": "^7.14.0",
"@babel/plugin-proposal-async-generator-functions": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-export-default-from": "^7.0.0",
"@babel/plugin-syntax-flow": "^7.2.0",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-syntax-optional-chaining": "^7.0.0",
"@babel/plugin-transform-arrow-functions": "^7.0.0",
"@babel/plugin-transform-async-to-generator": "^7.0.0",
"@babel/plugin-transform-block-scoping": "^7.0.0",
"@babel/plugin-transform-classes": "^7.0.0",
"@babel/plugin-transform-computed-properties": "^7.0.0",
"@babel/plugin-transform-destructuring": "^7.0.0",
"@babel/plugin-transform-exponentiation-operator": "^7.0.0",
"@babel/plugin-transform-flow-strip-types": "^7.0.0",
"@babel/plugin-transform-function-name": "^7.0.0",
"@babel/plugin-transform-literals": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0",
"@babel/plugin-transform-parameters": "^7.0.0",
"@babel/plugin-transform-react-display-name": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/plugin-transform-react-jsx-self": "^7.0.0",
"@babel/plugin-transform-react-jsx-source": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/plugin-transform-shorthand-properties": "^7.0.0",
"@babel/plugin-transform-spread": "^7.0.0",
"@babel/plugin-transform-sticky-regex": "^7.0.0",
"@babel/plugin-transform-template-literals": "^7.0.0",
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"react-refresh": "^0.4.0"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/react-native/node_modules/metro-react-native-babel-transformer": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.70.3.tgz",
"integrity": "sha512-WKBU6S/G50j9cfmFM4k4oRYprd8u3qjleD4so1E2zbTNILg+gYla7ZFGCAvi2G0ZcqS2XuGCR375c2hF6VVvwg==",
"dependencies": {
"@babel/core": "^7.14.0",
"babel-preset-fbjs": "^3.4.0",
"hermes-parser": "0.6.0",
"metro-babel-transformer": "0.70.3",
"metro-react-native-babel-preset": "0.70.3",
"metro-source-map": "0.70.3",
"nullthrows": "^1.1.1"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/react-native/node_modules/metro-runtime": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.70.3.tgz",
"integrity": "sha512-22xU7UdXZacniTIDZgN2EYtmfau2pPyh97Dcs+cWrLcJYgfMKjWBtesnDcUAQy3PHekDYvBdJZkoQUeskYTM+w==",
"dependencies": {
"@babel/runtime": "^7.0.0"
}
},
"node_modules/react-native/node_modules/metro-source-map": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.70.3.tgz",
"integrity": "sha512-zsYtZGrwRbbGEFHtmMqqeCH9K9aTGNVPsurMOWCUeQA3VGyVGXPGtLMC+CdAM9jLpUyg6jw2xh0esxi+tYH7Uw==",
"dependencies": {
"@babel/traverse": "^7.14.0",
"@babel/types": "^7.0.0",
"invariant": "^2.2.4",
"metro-symbolicate": "0.70.3",
"nullthrows": "^1.1.1",
"ob1": "0.70.3",
"source-map": "^0.5.6",
"vlq": "^1.0.0"
}
},
"node_modules/react-native/node_modules/metro-symbolicate": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.70.3.tgz",
"integrity": "sha512-JTYkF1dpeDUssQ84juE1ycnhHki2ylJBBdJE1JHtfu5oC+z1ElDbBdPHq90Uvt8HbRov/ZAnxvv7Zy6asS+WCA==",
"dependencies": {
"invariant": "^2.2.4",
"metro-source-map": "0.70.3",
"nullthrows": "^1.1.1",
"source-map": "^0.5.6",
"through2": "^2.0.1",
"vlq": "^1.0.0"
},
"bin": {
"metro-symbolicate": "src/index.js"
},
"engines": {
"node": ">=8.3"
}
},
"node_modules/react-native/node_modules/ob1": {
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.70.3.tgz",
"integrity": "sha512-Vy9GGhuXgDRY01QA6kdhToPd8AkLdLpX9GjH5kpqluVqTu70mgOm7tpGoJDZGaNbr9nJlJgnipqHJQRPORixIQ=="
},
"node_modules/react-native/node_modules/pretty-format": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
@@ -15384,6 +15374,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-native/node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-native/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "lnreader",
"version": "1.1.15",
"version": "1.1.17",
"private": true,
"scripts": {
"android": "react-native run-android",
@@ -16,7 +16,6 @@
"prepare": "husky install"
},
"dependencies": {
"@cd-z/react-native-epub-creator": "^2.0.4",
"@gorhom/bottom-sheet": "^4.4.5",
"@react-native-async-storage/async-storage": "^1.18.1",
"@react-native-community/clipboard": "^1.5.1",
@@ -62,7 +61,6 @@
"react-native-pager-view": "^6.2.0",
"react-native-paper": "^5.7.2",
"react-native-reanimated": "~2.9.1",
"react-native-saf-x": "^2.2.1",
"react-native-safe-area-context": "4.3.1",
"react-native-screens": "~3.15.0",
"react-native-shimmer-placeholder": "^2.0.9",
@@ -70,7 +68,6 @@
"react-native-tts": "^4.1.0",
"react-native-vector-icons": "^9.0.0",
"react-native-webview": "11.23.0",
"react-native-zip-archive": "^6.0.9",
"react-redux": "^7.2.6",
"redux-persist": "^6.0.0",
"rn-fetch-blob": "^0.12.0",
+5
View File
@@ -4,6 +4,9 @@ import { StyleSheet, View, Text, Pressable } from 'react-native';
import FastImage from 'react-native-fast-image';
import { coverPlaceholderColor } from '../theme/colors';
import { getSourceStorage } from '@hooks/useSourceStorage';
import { defaultUserAgentString } from '@utils/fetch/fetch';
import color from 'color';
const ListView = ({
@@ -16,6 +19,7 @@ const ListView = ({
isSelected,
onLongPress,
}) => {
const { cookies = '' } = getSourceStorage(item.sourceId);
return (
<Pressable
android_ripple={{ color: theme.rippleColor }}
@@ -31,6 +35,7 @@ const ListView = ({
<FastImage
source={{
uri: item.novelCover,
headers: { Cookie: cookies, 'User-Agent': defaultUserAgentString },
}}
style={[styles.extensionIcon, inLibraryBadge && { opacity: 0.5 }]}
/>
+12 -7
View File
@@ -20,6 +20,9 @@ import { SourceNovelItem } from 'src/sources/types';
import { ThemeColors } from '@theme/types';
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
import { getSourceStorage } from '@hooks/useSourceStorage';
import { defaultUserAgentString } from '@utils/fetch/fetch';
interface NovelCoverProps {
item: LibraryNovelInfo;
onPress: () => void;
@@ -65,6 +68,8 @@ const NovelCover: React.FC<NovelCoverProps> = ({
const uri = item.novelCover;
const { cookies = '' } = getSourceStorage(item.sourceId);
return item.sourceId < 0 ? (
<SourceScreenSkeletonLoading theme={theme} completeRow={item.sourceId} />
) : displayMode !== DisplayModes.List ? (
@@ -75,9 +80,9 @@ const NovelCover: React.FC<NovelCoverProps> = ({
},
styles.standardNovelCover,
isSelected && {
backgroundColor: theme.primary,
} &&
styles.selectedNovelCover,
backgroundColor: theme.primary,
opacity: 0.8,
},
]}
>
<Pressable
@@ -108,7 +113,10 @@ const NovelCover: React.FC<NovelCoverProps> = ({
)}
</View>
<FastImage
source={{ uri }}
source={{
uri,
headers: { Cookie: cookies, 'User-Agent': defaultUserAgentString },
}}
style={[
{
height: coverHeight,
@@ -348,9 +356,6 @@ const styles = StyleSheet.create({
overflow: 'hidden',
margin: 2,
},
selectedNovelCover: {
opacity: 0.8,
},
compactTitle: {
color: 'rgba(255,255,255,1)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
+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) {
+2 -1
View File
@@ -25,7 +25,7 @@ const SwitchSetting: React.FC<SwitchProps> = ({
paddingHorizontal: 16,
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 12,
paddingVertical: description ? 16 : 12,
},
row: {
flexDirection: 'row',
@@ -39,6 +39,7 @@ const SwitchSetting: React.FC<SwitchProps> = ({
},
text: {
fontSize: 16,
flex: 1,
textAlignVertical: 'center',
},
switch: { marginLeft: 8 },
+1 -1
View File
@@ -31,7 +31,7 @@ export const useGithubUpdateChecker = () => {
const newSemVer = newVersion.split('.');
const oldSemVer = currentVersion.split('.');
oldSemVer?.map((version, index) => {
return oldSemVer?.some((version, index) => {
if (newSemVer[index] > version) {
return true;
}
@@ -130,6 +130,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
) : (
<NovelList
data={novelList}
isFetchingNextPage={hasNextPage && !searchText}
inSource
renderItem={({ item }) => {
const inLibrary = novelInLibrary(item.novelUrl);
@@ -8,6 +8,9 @@ import { SourceNovelItem } from '../../../sources/types';
import { ThemeColors } from '../../../theme/types';
import { getString } from '@strings/translations';
import { getSourceStorage } from '@hooks/useSourceStorage';
import { defaultUserAgentString } from '@utils/fetch/fetch';
interface Props {
novel: SourceNovelItem;
navigateToNovel: (novel: SourceNovelItem) => void;
@@ -32,6 +35,8 @@ const GlobalSearchNovelItem: React.FC<Props> = ({
[inLibrary],
);
const { cookies = '' } = getSourceStorage(novel.sourceId);
return (
<View style={styles.novelItem}>
<Pressable
@@ -41,7 +46,10 @@ const GlobalSearchNovelItem: React.FC<Props> = ({
onLongPress={onLongPress}
>
<FastImage
source={{ uri: novel.novelCover }}
source={{
uri: novel.novelCover,
headers: { Cookie: cookies, 'User-Agent': defaultUserAgentString },
}}
style={[styles.novelCover, { ...novelItemDimensions }]}
/>
{inLibrary ? (
@@ -16,6 +16,9 @@ import {
openNovelProps,
} from '@utils/handleNavigateParams';
import { getSourceStorage } from '@hooks/useSourceStorage';
import { defaultUserAgentString } from '@utils/fetch/fetch';
interface HistoryCardProps {
history: History;
handleNavigateToChapter: (
@@ -56,6 +59,8 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
[chapterName, historyTimeRead],
);
const { cookies = '' } = getSourceStorage(sourceId);
return (
<Pressable
style={styles.container}
@@ -85,7 +90,16 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
})
}
>
<FastImage source={{ uri: novelCover }} style={styles.cover} />
<FastImage
source={{
uri: novelCover,
headers: {
Cookie: cookies,
'User-Agent': defaultUserAgentString,
},
}}
style={styles.cover}
/>
</Pressable>
<View style={styles.detailsContainer}>
<Text
+4 -9
View File
@@ -49,18 +49,17 @@ 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 NovelScreenLoading from './components/LoadingAnimation/NovelScreenLoading';
import { useTrackerReducer } from '@redux/hooks';
import EpubIconButton from './components/EpubIconButton';
const Novel = ({ route, navigation }) => {
const item = route.params;
@@ -536,11 +535,7 @@ const Novel = ({ route, navigation }) => {
}
/>
</Menu>
<EpubIconButton
theme={theme}
style={{ marginTop: StatusBar.currentHeight + 8 }}
novel={novel}
/>
<Menu
visible={extraMenu}
onDismiss={() => showExtraMenu(false)}
+14 -31
View File
@@ -74,10 +74,10 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
onLongPress={() => onSelectLongPress?.(chapter)}
android_ripple={{ color: theme.rippleColor }}
>
{left && <View style={styles.flex}>{left}</View>}
{left}
<Row style={styles.row}>
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
<View style={styles.textContainer}>
<View>
{isUpdateCard && (
<Text
style={[
@@ -132,18 +132,16 @@ 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={hideDeleteChapterMenu}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenuVisible={deleteChapterMenuVisible}
/>
</Pressable>
);
};
@@ -158,28 +156,13 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
columnGab: 8,
},
text: {
fontSize: 12,
},
textRow: {
flexDirection: 'row',
marginTop: 5,
},
flex: {
flexBasis: 'auto',
flexShrink: 9,
},
textContainer: {
flex: 1,
marginVertical: '10%',
height: '80%',
justifyContent: 'space-evenly',
},
row: {
flexBasis: 10,
flexGrow: 1,
overflow: 'hidden',
height: '100%',
},
row: { flex: 1, overflow: 'hidden' },
});
@@ -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';
@@ -1,145 +0,0 @@
import * as React from 'react';
import useBoolean from '@hooks/useBoolean';
import { IconButton, Portal } from 'react-native-paper';
import ChooseEpubLocationModal from './Modal/ChooseEpubLocationModal';
import { StatusBar, StyleProp } from 'react-native';
import { IconProps } from 'react-native-paper/lib/typescript/src/components/MaterialCommunityIcon';
import { ThemeColors } from '@theme/types';
import EpubBuilder from '@cd-z/react-native-epub-creator';
import { ChapterItem, LibraryNovelInfo } from '@database/types';
import { showToast } from '@hooks/showToast';
import { getChapterFromDb } from '@database/queries/DownloadQueries';
import { useReaderSettings } from '@redux/hooks';
import { useSettings } from '@hooks/reduxHooks';
interface extendedNovelInfo extends LibraryNovelInfo {
chapters: ChapterItem[];
}
interface EpubIconButtonProps {
theme: ThemeColors;
style: StyleProp<IconProps>;
novel: extendedNovelInfo;
}
const EpubIconButton: React.FC<EpubIconButtonProps> = ({
theme,
style,
novel,
}) => {
const chooseEpubLocationModal = useBoolean(false);
const readerSettings = useReaderSettings();
const {
epubUseAppTheme = false,
epubUseCustomCSS = false,
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 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 createEpub = async (uri: string) => {
var epub = new EpubBuilder(
{
title: novel.novelName,
fileName: novel.novelName.replace(/\s/g, ''),
language: 'en',
cover: novel.novelCover,
description: novel.novelSummary,
author: novel.author,
bookId: novel.novelId.toString(),
stylesheet: epubStyle || undefined,
js: epubUseCustomJS ? epubJS : undefined,
},
uri,
);
try {
// save and create the .epub file
await epub.prepare();
for (let i = 0; i < novel.chapters.length; i++) {
const chapter = novel.chapters[i];
if (chapter.downloaded) {
const downloaded = await getChapterFromDb(chapter.chapterId);
await epub.addChapter({
title: downloaded.chapterName?.trim() ?? 'Chapter ' + i,
fileName: 'Chapter' + i,
htmlBody: `<chapter data-novel-id='${chapter.novelId}' data-chapter-id='${chapter.chapterId}'>${downloaded.chapterText}</chapter>`,
});
}
}
var epubFilePath = await epub.save();
showToast('Epub file saved at: ' + epubFilePath);
} catch (error) {
showToast('Cannot create because: ' + error);
await epub.discardChanges();
}
};
return (
<>
<IconButton
icon="book-arrow-down-outline"
iconColor={theme.onBackground}
size={21}
style={style}
onPress={chooseEpubLocationModal.setTrue}
/>
<Portal>
<ChooseEpubLocationModal
modalVisible={chooseEpubLocationModal.value}
hideModal={chooseEpubLocationModal.setFalse}
onSubmit={createEpub}
/>
</Portal>
</>
);
};
export default EpubIconButton;
@@ -11,6 +11,9 @@ import { followNovelAction } from '../../../../redux/novel/novel.actions';
import { useSettings } from '../../../../hooks/reduxHooks';
import { showToast } from '../../../../hooks/showToast';
import { getSourceStorage } from '@hooks/useSourceStorage';
import { defaultUserAgentString } from '@utils/fetch/fetch';
import {
CoverImage,
NovelAuthor,
@@ -54,16 +57,30 @@ const NovelInfoHeader = ({
[],
);
const { cookies = '' } = getSourceStorage(novel.sourceId);
return (
<>
<CoverImage
source={{ uri: novel.novelCover }}
source={{
uri: novel.novelCover,
headers: {
Cookie: cookies,
'User-Agent': defaultUserAgentString,
},
}}
theme={theme}
hideBackdrop={hideBackdrop}
>
<NovelInfoContainer>
<NovelThumbnail
source={{ uri: novel.novelCover }}
source={{
uri: novel.novelCover,
headers: {
Cookie: cookies,
'User-Agent': defaultUserAgentString,
},
}}
theme={theme}
setCustomNovelCover={setCustomNovelCover}
/>
@@ -1,172 +0,0 @@
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';
interface ChooseEpubLocationModalProps {
hideModal: () => void;
modalVisible: boolean;
onSubmit?: (uri: string) => void;
dispatchConfig?: boolean;
}
const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
hideModal,
modalVisible,
onSubmit: onSubmitProp,
dispatchConfig,
}) => {
const theme = useTheme();
const {
epubLocation = '',
epubUseAppTheme = false,
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));
}
onSubmitProp?.(uri);
hideModal();
};
const openFolderPicker = async () => {
try {
const resultUri = await openDocumentTree(true);
if (resultUri) {
setUri(resultUri.uri);
}
} catch (err) {
console.log(err);
}
};
return (
<Modal
visible={modalVisible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: theme.overlay3 },
]}
>
<View style={styles.modalHeaderCtn}>
<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>
</View>
<View style={styles.settings}>
<SwitchSetting
label={getString('novelScreen.convertToEpubModal.useReaderTheme')}
value={useAppTheme.value}
onPress={useAppTheme.toggle}
theme={theme}
/>
<SwitchSetting
label={getString('novelScreen.convertToEpubModal.useCustomCSS')}
value={useCustomCSS.value}
onPress={useCustomCSS.toggle}
theme={theme}
/>
<SwitchSetting
label={getString('novelScreen.convertToEpubModal.useCustomJS')}
description={getString(
'novelScreen.convertToEpubModal.useCustomJSWarning',
)}
value={useCustomJS.value}
onPress={useCustomJS.toggle}
theme={theme}
/>
</View>
<View style={styles.modalFooterCtn}>
<Button title={getString('common.submit')} onPress={onSubmit} />
<Button title={getString('common.cancel')} onPress={hideModal} />
</View>
</Modal>
);
};
export default ChooseEpubLocationModal;
const styles = StyleSheet.create({
modalContainer: {
margin: 30,
borderRadius: 32,
},
settings: {
marginTop: 12,
paddingLeft: 5,
},
icon: { marginVertical: 0, marginRight: 0 },
textInput: { flex: 1 },
modalHeaderCtn: {
padding: 20,
paddingTop: 32,
paddingBottom: 0,
},
modalFooterCtn: {
flexDirection: 'row-reverse',
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 20,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
});
+41 -25
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';
@@ -195,15 +201,16 @@ const ChapterContent = ({ route, navigation }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chapter]);
const [ttsStatus, startTts] = useTextToSpeech(
htmlToText(chapter?.chapterText).split('\n'),
() => {
if (!incognitoMode) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
},
const htmlText = useMemo(
() => htmlToText(chapter?.chapterText).split('\n'),
[chapter.chapterText],
);
const [ttsStatus, startTts] = useTextToSpeech(htmlText, () => {
if (!incognitoMode) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
});
const scrollTo = useCallback(
offsetY => {
@@ -268,13 +275,15 @@ const ChapterContent = ({ route, navigation }) => {
);
const hideHeader = useCallback(() => {
if (!hidden) {
setImmersiveMode();
} else {
showStatusAndNavBar();
}
setHidden(!hidden);
}, [hidden]);
setHidden(h => {
if (!h) {
setImmersiveMode();
} else {
showStatusAndNavBar();
}
return !h;
});
}, []);
const navigateToChapterBySwipe = useCallback(
name => {
@@ -313,18 +322,28 @@ const ChapterContent = ({ route, navigation }) => {
}
}, []);
const scrollToSavedProgress = () => scrollTo(position?.position);
const scrollToSavedProgress = useCallback(
() => scrollTo(position?.position),
[],
);
const chapterText = sanitizeChapterText(chapter.chapterText, {
removeExtraParagraphSpacing,
bionicReading,
sourceId: sourceId,
});
const chapterText = useMemo(
() =>
sanitizeChapterText(chapter.chapterText, {
removeExtraParagraphSpacing,
bionicReading,
sourceId,
}),
[chapter.chapterText, removeExtraParagraphSpacing, bionicReading, sourceId],
);
const openDrawer = () => {
navigation.openDrawer();
hideHeader();
};
const bookmarkChapter = () =>
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
if (loading) {
return <ChapterLoadingScreen />;
}
@@ -333,9 +352,6 @@ const ChapterContent = ({ route, navigation }) => {
return <ErrorScreenV2 error={error} />;
}
const bookmarkChapter = () =>
setChapter(prevVal => ({ ...prevVal, bookmark: !prevVal?.bookmark }));
return (
<>
<WebViewReader
+17 -17
View File
@@ -24,6 +24,7 @@ export const sanitizeChapterText = (
'em',
'b',
'a',
'center',
]),
allowedAttributes: {
'img': ['src', 'class'],
@@ -42,23 +43,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 =
@@ -25,7 +25,6 @@ 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';
interface GenralSettingsProps {
navigation: NavigationState;
@@ -105,10 +104,6 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
* Chapter Sort Modal
*/
const defaultChapterSortModal = useBoolean();
/**
* Convert to EPUB Modal
*/
const convertToEpubModal = useBoolean();
return (
<>
<Appbar
@@ -263,21 +258,6 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
{getString('moreScreen.settingsScreen.generalSettingsScreen.epub')}
</List.SubHeader>
<List.Item
title={getString(
'moreScreen.settingsScreen.generalSettingsScreen.epubLocation',
)}
description={getString(
'moreScreen.settingsScreen.generalSettingsScreen.epubLocationDescription',
)}
onPress={convertToEpubModal.setTrue}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
{getString(
'moreScreen.settingsScreen.generalSettingsScreen.autoDownload',
@@ -342,11 +322,6 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
hideNovelSortModal={novelSortModalRef.setFalse}
theme={theme}
/>
<ChooseEpubLocationModal
hideModal={convertToEpubModal.setFalse}
modalVisible={convertToEpubModal.value}
dispatchConfig
/>
</>
);
};
@@ -23,16 +23,24 @@ import { useAppDispatch } from '@redux/hooks';
import { useTheme } from '@hooks/useTheme';
import { noop } from 'lodash-es';
import { getSourceStorage } from '@hooks/useSourceStorage';
const NovelCover = ({
uri,
navigateToNovel,
sourceId,
}: {
uri: string;
navigateToNovel: () => void;
sourceId: number;
}) => {
const { cookies = '' } = getSourceStorage(sourceId);
return (
<Pressable onPress={navigateToNovel}>
<FastImage source={{ uri }} style={styles.cover} />
<FastImage
source={{ uri, headers: { 'cookie': cookies } }}
style={styles.cover}
/>
</Pressable>
);
};
@@ -105,19 +113,23 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
(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 +157,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
sourceId={chapterList[0].sourceId}
/>
</View>
}
@@ -172,6 +185,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
sourceId={chapterList[0].sourceId}
/>
</View>
}
@@ -198,11 +212,8 @@ const styles = StyleSheet.create({
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
listNovelCover: {
justifyContent: 'center',
},
novelCover: {
marginRight: 16,
marginRight: 8,
},
imageContainer: {
flex: 1,
+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);
}
+368
View File
@@ -0,0 +1,368 @@
import { fetchHtml } from '@utils/fetch/fetch';
import * as cheerio from 'cheerio';
const sourceId = 165;
const sourceName = 'Linovelib';
const baseUrl = 'https://w.linovelib.com';
const popularNovels = async page => {
const url = `${baseUrl}/top/monthvisit/${page}.html`;
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
const novels = [];
loadedCheerio('.module-rank-booklist .book-layout').each(function () {
let novelUrl = loadedCheerio(this).attr('href');
if (novelUrl) {
const novelName = loadedCheerio(this).find('.book-title').text();
const novelCover = loadedCheerio(this)
.find('img.book-cover')
.attr('data-src');
novelUrl = baseUrl + novelUrl;
const novel = {
sourceId,
novelUrl,
novelName,
novelCover,
};
novels.push(novel);
}
});
return { novels };
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
const novel = {
sourceId,
sourceName,
url: novelUrl,
novelUrl,
};
novel.novelName = loadedCheerio('#bookDetailWrapper .book-title').text();
novel.novelCover = loadedCheerio('#bookDetailWrapper img.book-cover').attr(
'src',
);
novel.summary = loadedCheerio('#bookSummary content').text();
novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a a').text();
novel.artist = null;
// TODO: Need some regex and dirty selector to get it
// Need to look into how to translate that message
novel.status = null;
let genres = [];
loadedCheerio(
'#bookDetailWrapper .book-cell .book-meta:last-child .tag-small.red a',
).each(function () {
genres.push(loadedCheerio(this).text());
});
if (genres && genres.length > 0) {
novel.genres = genres.join(', ');
}
// Table of Content is on a different page than the summary page
let chapters = [];
const idPattern = /\/(\d+)\.html/;
const novelId = url.match(idPattern)[1];
const chaptersUrl = baseUrl + loadedCheerio('#btnReadBook').attr('href');
const chaptersBody = await fetchHtml({ url: chaptersUrl, sourceId });
const chaptersLoadedCheerio = cheerio.load(chaptersBody);
let volumeName;
chaptersLoadedCheerio('#volumes .chapter-li').each(function () {
if (chaptersLoadedCheerio(this).hasClass('chapter-bar')) {
volumeName = chaptersLoadedCheerio(this).text();
} else {
const urlPart = chaptersLoadedCheerio(this)
.find('.chapter-li-a')
.attr('href');
const chapterIdMatch = urlPart.match(idPattern);
// Sometimes the href attribute does not contain the url, but javascript:cid(0).
// Increment the previous chapter ID should result in the right URL
if (chapterIdMatch) {
chapterId = chapterIdMatch[1];
} else {
chapterId++;
}
const chapterUrl = `${baseUrl}/novel/${novelId}/${chapterId}.html`;
if (chapterUrl) {
const chapterName =
volumeName +
' — ' +
chaptersLoadedCheerio(this).find('.chapter-index').text().trim();
const releaseDate = null;
chapters.push({
chapterName,
releaseDate,
chapterUrl,
});
}
}
});
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
let chapterName, chapterText, hasNextPage;
let pageNumber = 1;
/*
* TODO: Maybe there are other ways to get the translation table
* It is embed and encrypted inside readtool.js
*/
const mapping_dict = {
'“': '「',
'’': '』',
'': '是',
'': '不',
'': '他',
'': '个',
'': '来',
'': '大',
'': '子',
'': '说',
'': '年',
'': '那',
'': '她',
'': '得',
'': '自',
'': '家',
'': '而',
'': '去',
'': '小',
'': '于',
'': '么',
'': '好',
'': '发',
'': '成',
'': '事',
'': '用',
'': '道',
'': '种',
'': '乳',
'': '茎',
'': '肉',
'': '胸',
'': '淫',
'': '射',
'': '骚',
'”': '」',
'': '的',
'': '了',
'': '人',
'': '有',
'': '上',
'': '到',
'': '地',
'': '中',
'': '生',
'': '着',
'': '和',
'': '出',
'': '里',
'': '以',
'': '可',
'': '过',
'': '能',
'': '多',
'': '心',
'': '之',
'': '看',
'': '当',
'': '只',
'': '把',
'': '第',
'': '想',
'': '开',
'': '阴',
'': '欲',
'': '交',
'': '私',
'': '臀',
'': '脱',
'': '唇',
'‘': '『',
'': '一',
'': '我',
'': '在',
'': '这',
'': '们',
'': '时',
'': '为',
'': '你',
'': '国',
'': '就',
'': '要',
'': '也',
'': '后',
'': '会',
'': '下',
'': '天',
'': '对',
'': '然',
'': '学',
'': '都',
'': '起',
'': '没',
'': '如',
'': '还',
'': '样',
'': '作',
'': '美',
'': '液',
'': '呻',
'': '性',
'': '穴',
'': '舔',
'': '裸',
};
const addPage = async pageCheerio => {
let pageText;
const formatPage = async () => {
// Remove JS
pageCheerio('#acontent .cgo').remove();
// Load lazyloaded images
pageCheerio('#acontent img.imagecontent').each(function () {
// Sometimes images are either in data-src or src
const imgSrc =
pageCheerio(this).attr('data-src') || pageCheerio(this).attr('src');
if (imgSrc) {
// The original CDN URL is locked behind a CF-like challenge, switch the URL to bypass that
// There are no react-native-url-polyfill lib, can't use URL API
const regex = /\/\/.+\.com\//;
const imgUrl = imgSrc.replace(regex, '//img.linovelib.com/');
// Clean up img element
pageCheerio(this)
.attr('src', imgUrl)
.removeAttr('data-src')
.removeClass('lazyload');
}
});
// Recover the original character
pageText = pageCheerio('#acontent').html();
pageText = pageText.replace(/./g, char => mapping_dict[char] || char);
return Promise.resolve();
};
await formatPage();
chapterName ??=
pageCheerio('#atitle + h3').text() +
' — ' +
pageCheerio('#atitle').text();
chapterText += pageText;
};
const loadPage = async url => {
const body = await fetchHtml({ url, sourceId });
const pageCheerio = cheerio.load(body);
addPage(pageCheerio);
pageHasNextPage =
pageCheerio('#footlink > a[onclick$=ReadParams.url_next;]').text() ===
'下一页'
? true
: false;
return { pageCheerio, pageHasNextPage };
};
let url = chapterUrl;
do {
const page = await loadPage(url);
hasNextPage = page.pageHasNextPage;
if (hasNextPage === true) {
pageNumber++;
url = chapterUrl.replace(/\.html/gi, `_${pageNumber}` + '.html');
}
} while (hasNextPage === true);
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}/search.html?searchkey=` + encodeURI(searchTerm);
const body = await fetchHtml({ url, sourceId });
const loadedCheerio = cheerio.load(body);
const novels = [];
const loadSearchResults = function () {
loadedCheerio('.book-ol .book-layout').each(function () {
let novelUrl = loadedCheerio(this).attr('href');
if (novelUrl) {
const novelName = loadedCheerio(this).find('.book-title').text();
const novelCover = loadedCheerio(this)
.find('img.book-cover')
.attr('data-src');
novelUrl = baseUrl + novelUrl;
const novel = {
url: novelUrl,
name: novelName,
cover: novelCover,
};
novels.push(novel);
}
});
};
const novelResults = loadedCheerio('.book-ol .book-layout');
if (novelResults.length === 0) {
// console.log('Challenge');
} else {
loadSearchResults();
}
return novels;
};
const LinovelibScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default LinovelibScraper;
-1
View File
@@ -111,7 +111,6 @@ const parseNovelAndChapters = async (novelUrl: string) => {
return novelChapters;
};
console.log(novelId);
if (novelId) {
novel.chapters = await getChapters(novelId);
-1
View File
@@ -69,7 +69,6 @@ const parseNovelAndChapters = async novelUrl => {
)
.text()
.trim();
console.log(novel.status);
novel.genre = loadedCheerio(
'main > div.container > div > div.col-lg-3.col-md-4 > div:nth-child(4) > div.panel-body > ul',
+1 -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}/`;
+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;
};
+4
View File
@@ -6,6 +6,10 @@ function getNovelName(y) {
}
const popularNovels = async page => {
if (page !== 1) {
return { novels: [] };
}
let url = baseUrl + 'p/todas-las-novelas.html';
let headers = new Headers({
+4
View File
@@ -2,6 +2,10 @@ import * as cheerio from 'cheerio';
const baseUrl = 'https://oasistranslations.wordpress.com/';
const popularNovels = async page => {
if (page !== 1) {
return { novels: [] };
}
let url = baseUrl;
const result = await fetch(url);
+4
View File
@@ -1,6 +1,10 @@
const baseUrl = 'https://www.skynovels.net/';
const popularNovels = async page => {
if (page !== 1) {
return { novels: [] };
}
const url = 'https://api.skynovels.net/api/novels?&q';
const result = await fetch(url);
+21 -18
View File
@@ -1,5 +1,6 @@
import * as cheerio from 'cheerio';
import { defaultCoverUri, Status } from '../helpers/constants';
import { fetchHtml } from '@utils/fetch/fetch';
const sourceId = 23;
const sourceName = 'TuNovelaLigera';
@@ -7,10 +8,9 @@ const sourceName = 'TuNovelaLigera';
const baseUrl = 'https://tunovelaligera.com/';
const popularNovels = async page => {
let url = baseUrl + 'novelas/page/' + page + '/?m_orderby=rating';
let url = `${baseUrl}novelas/page/${page}/?m_orderby=rating`;
const result = await fetch(url);
const body = await result.text();
const body = await fetchHtml({ url });
let loadedCheerio = cheerio.load(body);
@@ -18,7 +18,9 @@ const popularNovels = async page => {
loadedCheerio('.page-item-detail').each(function () {
const novelName = loadedCheerio(this).find('.h5 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelCoverImg = loadedCheerio(this).find('img');
const novelCover =
novelCoverImg.attr('src') || novelCoverImg.attr('data-cfsrc');
let novelUrl = loadedCheerio(this)
.find('.h5 > a')
@@ -42,8 +44,7 @@ const popularNovels = async page => {
const parseNovelAndChapters = async novelUrl => {
const url = `${baseUrl}novelas/${novelUrl}`;
const result = await fetch(url);
const body = await result.text();
const body = await fetchHtml({ url });
let loadedCheerio = cheerio.load(body);
@@ -58,10 +59,13 @@ const parseNovelAndChapters = async novelUrl => {
novel.novelName = loadedCheerio('.post-title > h1').text().trim();
let novelCover = loadedCheerio('.summary_image > a > img');
let novelCover = loadedCheerio('.summary_image img');
novel.novelCover =
novelCover.attr('data-src') || novelCover.attr('src') || defaultCoverUri;
novelCover.attr('data-src') ||
novelCover.attr('src') ||
novelCover.attr('data-cfsrc') ||
defaultCoverUri;
loadedCheerio('.post-content_item').each(function () {
const detailName = loadedCheerio(this)
@@ -98,14 +102,13 @@ const parseNovelAndChapters = async novelUrl => {
formData.append('action', 'manga_get_chapters');
formData.append('manga', novelId);
const data = await fetch(
'https://tunovelaligera.com/wp-admin/admin-ajax.php',
{
const text = await fetchHtml({
url: 'https://tunovelaligera.com/wp-admin/admin-ajax.php',
init: {
method: 'POST',
body: formData,
},
);
const text = await data.text();
});
loadedCheerio = cheerio.load(text);
@@ -135,8 +138,7 @@ const parseNovelAndChapters = async novelUrl => {
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}novelas/${novelUrl}/${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
const body = await fetchHtml({ url });
let loadedCheerio = cheerio.load(body);
@@ -159,8 +161,7 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const searchNovels = async searchTerm => {
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga`;
const result = await fetch(url);
const body = await result.text();
const body = await fetchHtml({ url });
let loadedCheerio = cheerio.load(body);
@@ -168,7 +169,9 @@ const searchNovels = async searchTerm => {
loadedCheerio('.c-tabs-item__content').each(function () {
const novelName = loadedCheerio(this).find('.h4 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelCoverImg = loadedCheerio(this).find('img');
const novelCover =
novelCoverImg.attr('src') ?? novelCoverImg.attr('data-cfsrc');
let novelUrl = loadedCheerio(this).find('.h4 > a').attr('href');
novelUrl = novelUrl.replace(`${baseUrl}novelas/`, '');
+20 -16
View File
@@ -2,6 +2,10 @@ import * as cheerio from 'cheerio';
const baseUrl = 'https://yuukitls.com/';
const popularNovels = async page => {
if (page !== 1) {
return { novels: [] };
}
let url = baseUrl;
const result = await fetch(url);
@@ -11,25 +15,25 @@ const popularNovels = async page => {
let novels = [];
loadedCheerio('.menu-item-2869')
.find('.menu-item.menu-item-type-post_type.menu-item-object-post')
.each(function () {
const novelName = loadedCheerio(this).text();
const novelCover = loadedCheerio(this).find('img').attr('src');
loadedCheerio(
'li.quadmenu-item.quadmenu-item-object-post.quadmenu-item-level-2',
).each(function () {
const novelName = loadedCheerio(this).find('span.quadmenu-text').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find('a').attr('href');
novelUrl = novelUrl.split('/');
novelUrl = novelUrl[novelUrl.length - 2] + '/';
let novelUrl = loadedCheerio(this).find('a').attr('href');
novelUrl = novelUrl.split('/');
novelUrl = novelUrl[novelUrl.length - 2] + '/';
const novel = {
sourceId: 28,
novelName,
novelCover,
novelUrl,
};
const novel = {
sourceId: 28,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
novels.push(novel);
});
return { novels };
};
@@ -0,0 +1,58 @@
import { getSourceStorage } from '@hooks/useSourceStorage';
import { defaultUserAgentString, fetchApi } from '@utils/fetch/fetch';
import { Cheerio, CheerioAPI, Element } from 'cheerio';
export const bypassImages = async (
loadedCheerio: CheerioAPI,
element: Cheerio<Element>,
sourceId: number,
): Promise<string> => {
const { cookies = '' } = getSourceStorage(sourceId);
let promises: Promise<number>[] = [];
element.find('noscript').each(function () {
loadedCheerio(this).remove();
});
element.find('img').each(function () {
const attr: any = this.attributes.find(
a => a.name.includes('src') && !a.value.includes(', '),
);
if (!attr) {
loadedCheerio(this).remove();
return;
}
const promise: Promise<number> = new Promise(async r => {
const response = await fetchApi({
url: attr.value,
init: {
headers: { Cookie: cookies, 'User-Agent': defaultUserAgentString },
},
});
const blob = await response.blob();
const encodedImage: string = await new Promise((onSuccess, onError) => {
try {
const reader = new FileReader();
reader.onload = function () {
onSuccess(this.result as string);
};
reader.readAsDataURL(blob);
} catch (e) {
onError(e);
}
});
loadedCheerio(this).replaceWith(`<img src="${encodedImage}"/>`);
r(0);
});
promises.push(promise);
});
await Promise.all(promises);
return element.html()!!;
};
+4
View File
@@ -18,6 +18,7 @@ import EinharjarProjectScraper from './es/einherjarproject';
import TuNovelaLigeraScraper from './es/tunovelaligera';
import SkyNovelsScraper from './es/skynovels';
import NovelasLigeraScraper from './es/novelasligera';
import NOVAScraper from './es/nova';
import YuukiTlsScraper from './es/yuukitls';
import NovelaWuxiaScraper from './es/novelawuxia';
import OasisTranslationsScraper from './es/oasistranslations';
@@ -78,6 +79,7 @@ import NovelUpdatesScraper from './en/novelupdates';
import RanobesScraper from './en/ranobes';
import RanobesruScraper from './ru/ranobes';
import YushuboScraper from './ch/yushubo';
import LinovelibScraper from './ch/linovelib';
import {
LiebeSchneeHiverNovelScraper,
LightNovelsBrasilScraper,
@@ -326,6 +328,8 @@ export const sourceManager = (sourceId: number): Scraper => {
162: KodScraper, // @ts-ignore
163: NovelsOnlineScraper, // @ts-ignore
164: BookRiverScraper, // @ts-ignore
165: LinovelibScraper, // @ts-ignore
166: NOVAScraper, // @ts-ignore
};
return scrapers[sourceId];
+15 -1
View File
@@ -281,7 +281,7 @@
},
{
"sourceId": 51,
"url": "https://ranobes.net/",
"url": "https://ranobes.top/",
"sourceName": "Ranobes",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/icons/src/en/ranobes/icon.png?raw=true",
"lang": "English"
@@ -999,5 +999,19 @@
"url": "https://bookriver.ru/",
"lang": "Russian",
"icon": "https://bookriver.ru/android-chrome-512x512.png"
},
{
"sourceId": 165,
"sourceName": "Linovelib",
"url": "https://w.linovelib.com",
"lang": "Chinese",
"icon": "https://w.linovelib.com/images/logo.png"
},
{
"sourceId": 166,
"sourceName": "NOVA",
"url": "https://novelasligeras.net",
"lang": "Spanish",
"icon": "https://pbs.twimg.com/profile_images/1659040611507351552/DeLBdFep_400x400.jpg"
}
]
+2 -2
View File
@@ -1,2 +1,2 @@
export const appVersion = '1.1.15';
export const releaseDate = '06/04/23 09:30 PM';
export const appVersion = '1.1.17';
export const releaseDate = '25/07/23 11:00 AM';
-3
View File
@@ -131,9 +131,6 @@ export interface StringMap {
'moreScreen.settingsScreen.generalSettingsScreen.refreshMetadataDescription': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.updateTime': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.autoDownload': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.epub': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.epubLocation': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.epubLocationDescription': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.downloadNewChapters': 'string';
'moreScreen.settingsScreen.generalSettingsScreen.disableHapticFeedback': 'string';
'moreScreen.settingsScreen.readerSettings.title': 'string';