Compare commits

..

85 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 4b515502bf fix(source): Fix ComradeMao (Closes LNReader/lnreader-sources#215) 2022-07-30 08:53:25 +05:30
Rajarshee Chatterjee baba8814f0 fix(source): Fix TravisTranslations Chapter Selector (Closes LNReader/lnreader-sources#339) 2022-07-30 08:52:59 +05:30
Rajarshee Chatterjee 5b01f61c9f feat(source): Add Novelstic (Closes LNReader/lnreader-sources#345) 2022-07-30 08:46:35 +05:30
Rajarshee Chatterjee c02a72018d fix: Remove Text Selectable Option (Closes #332) 2022-07-30 08:09:32 +05:30
Rajarshee Chatterjee 4d446d7c0b fix: Chapter not available when navigating from History/Updates (Closes #336) 2022-07-30 07:59:18 +05:30
Marcin Majewski e580f8f151 fix(wv-reader): Remove unnecesarry console.logs() (#331) 2022-07-21 21:23:26 +05:30
Rajarshee Chatterjee 92f164fc23 feat(source): Add Riwyat (Closes LNReader/lnreader-sources#332) 2022-07-21 20:35:08 +05:30
Rajarshee Chatterjee 44c805b6f2 feat(source): Add Light Novels Brasil (Closes LNReader/lnreader-sources#335) 2022-07-21 20:22:48 +05:30
Rajarshee Chatterjee d3f5619276 feat: Add option to disable Haptic Feedback (Closes #326) 2022-07-21 20:15:34 +05:30
Rajarshee Chatterjee 0261c6373d chore: Add Strings 2022-07-21 20:05:46 +05:30
Rajarshee Chatterjee fd5b9a7c17 chore: Update translations (#329)
* New translations strings.json (Russian)
[ci skip]

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

* New translations strings.json (Portuguese, Brazilian)
[ci skip]
2022-07-21 20:04:23 +05:30
Rajarshee Chatterjee b884600dfb chore: Update translations (#296)
* New translations strings.json (Portuguese)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: add french translation (#315)

Some small hesitations on how to translate :
    - "Offset" is difficult to translate with this context
    - "Badges" For me, it's "a small piece of metal, plastic, or cloth bearing a design or words, typically worn to identify a person or to indicate membership of an organization or support for a cause." (I know it can be virtual but I'm not sure I have the context of the application but it is a transparent word according to this definition)
    - "grid" translated into "grille" is a bit weird
    - Should I translate "Novel" by its translation ("roman") ?

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

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

Co-authored-by: Raphael <67842535+Snowad14@users.noreply.github.com>
2022-07-21 19:28:09 +05:30
dependabot[bot] 8355907a76 chore(deps): bump moment from 2.29.2 to 2.29.4 (#327)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-21 19:27:45 +05:30
Marcin Majewski b391e09e53 feat(img-dl): Downloading the chapter now downloads all img files (#321)
* fix(lint): fixed ChapterQueries.ts linting

* feat(img_download): Download images for offline reading

* fix(dl): imgs in Downloads/LNReader/source/novel/chapter

* fix(dl): deleteChapter now requires novelId/sourceId to remove images
2022-07-20 16:41:38 +05:30
Marcin Majewski a4c27924f8 fix(source): Fix NovelUpdates genres (#318) 2022-07-05 20:57:10 +05:30
Rider21 221c77d2cb feat(source): Add NovelTop1 (#313)
* test

* add NovelTop1

* ok
2022-07-01 22:22:02 +05:30
Rider21 3a0047ee25 feat: Moment Sync (#310)
* moment locale sync

* da
2022-06-26 17:45:52 +05:30
Rajarshee Chatterjee 3afbb5c45d feat: Add Option to Delete Read Chapters 2022-06-19 09:16:09 +05:30
Rajarshee Chatterjee 47102ff65e feat(source): Add More Filters 2022-06-19 08:12:22 +05:30
Rider21 c10eac6cd6 feat(source): Source Updates (#306)
* update

* sort

* removed the special characters
2022-06-19 06:27:34 +05:30
Rajarshee Chatterjee b4c234ef93 feat(source): Add Madara Sources
Closes LNReader/lnreader-sources#247, Closes LNReader/lnreader-sources#271, Closes LNReader/lnreader-sources#273, Closes LNReader/lnreader-sources#284
2022-06-18 13:57:39 +05:30
Rajarshee Chatterjee 3487bbd4cf Minor fixes 2022-06-18 13:37:52 +05:30
Rajarshee Chatterjee bece88663f refactor: Global Search Screen
Closes #302
2022-06-18 12:32:15 +05:30
dependabot[bot] 054124a004 chore(deps): bump jpeg-js from 0.4.3 to 0.4.4 (#301)
Bumps [jpeg-js](https://github.com/eugeneware/jpeg-js) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/eugeneware/jpeg-js/releases)
- [Commits](https://github.com/eugeneware/jpeg-js/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: jpeg-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 08:39:26 +05:30
Rider21 d2b62c0e8b feat: Source Updates (#297)
* update

* xm
2022-06-14 06:19:06 +05:30
Rajarshee Chatterjee e98980bdb9 chore: Update translations (#279)
* New translations strings.json (German)

* New translations strings.json (Russian)

* New translations strings.json (Ukrainian)

* New translations strings.json (Arabic)

* New translations strings.json (German)

* New translations strings.json (Hungarian)

* New translations strings.json (Vietnamese)

* New translations strings.json (Chinese Traditional)

* New translations strings.json (Swedish)

* New translations strings.json (Serbian (Cyrillic))

* New translations strings.json (Albanian)

* New translations strings.json (Portuguese)

* New translations strings.json (Polish)

* New translations strings.json (Norwegian)

* New translations strings.json (Dutch)

* New translations strings.json (Korean)

* New translations strings.json (Japanese)

* New translations strings.json (Hebrew)

* New translations strings.json (Italian)

* New translations strings.json (Finnish)

* New translations strings.json (Greek)

* New translations strings.json (Danish)

* New translations strings.json (Czech)

* New translations strings.json (Catalan)

* New translations strings.json (Afrikaans)

* New translations strings.json (Spanish)

* New translations strings.json (French)

* New translations strings.json (Romanian)

* New translations strings.json (Arabic)

* New translations strings.json (Ukrainian)

* New translations strings.json (Turkish)

* New translations strings.json (Russian)

* New translations strings.json (Portuguese, Brazilian)

* New translations strings.json (Turkish)

* New translations strings.json (German)

* New translations strings.json (Hungarian)

* New translations strings.json (Vietnamese)

* New translations strings.json (Chinese Traditional)

* New translations strings.json (Swedish)

* New translations strings.json (Serbian (Cyrillic))

* New translations strings.json (Albanian)

* New translations strings.json (Portuguese)

* New translations strings.json (Polish)

* New translations strings.json (Norwegian)

* New translations strings.json (Dutch)

* New translations strings.json (Korean)

* New translations strings.json (Japanese)

* New translations strings.json (Hebrew)

* New translations strings.json (Italian)

* New translations strings.json (Finnish)

* New translations strings.json (Greek)

* New translations strings.json (Danish)

* New translations strings.json (Czech)

* New translations strings.json (Catalan)

* New translations strings.json (Afrikaans)

* New translations strings.json (Spanish)

* New translations strings.json (French)

* New translations strings.json (Romanian)

* New translations strings.json (Arabic)

* New translations strings.json (Ukrainian)

* New translations strings.json (Turkish)

* New translations strings.json (Russian)

* New translations strings.json (Portuguese, Brazilian)
2022-06-13 09:57:21 +05:30
Rajarshee Chatterjee 62a847acb2 build: Skip CI in translations 2022-06-13 09:38:57 +05:30
Rajarshee Chatterjee 13f394c76d build: Only run CI on main branch 2022-06-13 09:11:30 +05:30
Marcin Majewski 71f6d863c0 fix(reader-settings): Fix reader settings crash (#292) 2022-06-13 08:21:25 +05:30
Rajarshee Chatterjee d5a8ca2b47 feat(source): Add Liebe Schnee Hiver Novel (Closes LNReader/lnreader-sources#232) 2022-06-12 21:15:25 +05:30
Rajarshee Chatterjee c969916cd5 feat(source): Initial Source Filter Support (Closes #285) 2022-06-12 20:27:57 +05:30
Rajarshee Chatterjee e68c72ce02 fix(source):Fix ReadLightNovel's Novel Info 2022-06-12 13:53:15 +05:30
Rajarshee Chatterjee 5f83104171 feat: Add Ability to View Latest Novels 2022-06-12 13:50:16 +05:30
Rajarshee Chatterjee 7837d7cdaf feat: Add Option to Remove Extra Paragraph Spacing (Closes #248) 2022-06-12 13:01:10 +05:30
Rajarshee Chatterjee b243d764ce feat: Add New Themes (Closes #254) 2022-06-12 12:05:39 +05:30
Rajarshee Chatterjee 56c7716595 fix: Cross Icon Overlapping Search Text (Closes #273) 2022-06-12 11:23:34 +05:30
Rajarshee Chatterjee d44abc9039 feat: Add Last Read Sorting (Closes #255) 2022-06-12 09:20:26 +05:30
Rajarshee Chatterjee 6f30bfeeab fix: Add progressViewOffset (Closes #270) 2022-06-12 08:58:50 +05:30
Rajarshee Chatterjee 3d90e04415 fix: Global Search (Closes #256) 2022-06-12 08:51:49 +05:30
Rajarshee Chatterjee 23e27e8d40 fix(source): HakoLightNovel (Closes LNReader/lnreader-sources#239) 2022-06-12 08:48:14 +05:30
Marcin Majewski d0f7e25956 feat(volume-scroll): Added option to scroll WebView using volume buttons (#290)
* feat(volume-scroll): Added webview volume scroll

Scrolling with volume buttons in WebView

* feat(volume-settings): Add volume button settings
2022-06-12 07:47:39 +05:30
Rajarshee Chatterjee 9b297620fe refactor: Browse Source Screen 2022-06-06 07:58:07 +05:30
Marcin Majewski df851bf9d1 Webview improvements (#283)
* feat(webview): Added few webview improvements

Added buttons from TextReader on the bottom of the chapter (Closes #250)
Added webview seekbar (Closes #218)
Switched the handling of the navigation bar visibility to WebView. This might solve the issue from #229 because WebView is a bit more strict in what it interprets as "click/tap"
Migrating WebViewReader.js to TypeScript

* fix(webview): Removed unnecesarry console.logs

* feat(wv-autoscroll): WebView AutoScroll

* feat(wv-swipes): WebView new swipe detection

Added webview internal swipe detection instead of the react-native one
Hopefully making the detection a bit better (effects unknown yet)
This option can be turned on/off in the settings

* fix(wv-scrollbar): Added option to switch vertical

You can now switch vertical/horizontal scrollbar setting in reader screen options
2022-06-05 07:24:36 +05:30
Rajarshee Chatterjee 5fc411e14c Merge branch 'main' of https://github.com/LNReader/lnreader into main 2022-06-04 09:04:48 +05:30
Rajarshee Chatterjee 0f221b2823 fix: Build 2022-06-04 09:04:40 +05:30
Marcin Majewski 9e50159589 fix(clipboard): Fixed clipboard dependency (#282)
Closes #257
2022-06-04 08:59:31 +05:30
Rajarshee Chatterjee 58752a5589 fix: ESLint 2022-06-04 08:57:06 +05:30
Rajarshee Chatterjee c1a76458a8 fix: Novel Updates (Closes LNReader/lnreader-sources#227) 2022-06-01 05:35:34 +05:30
Rajarshee Chatterjee 5b3a6ceff4 fix: ReadLightNovel Crash 2022-06-01 05:19:32 +05:30
Rajarshee Chatterjee c4a14784f0 fix(source): Remove RewayatAr 2022-06-01 05:08:14 +05:30
Rajarshee Chatterjee d64e2e7fdb chore: Update Translations 2022-05-31 21:11:24 +05:30
Marcin Majewski 5d8463509c fix(tts): Android11 Manifest TTS Support (#277)
Add to AndroidManifest.xml a clause about querying TTS to remove `Unresolved Promise: No TTS Engine Installed` error in Android 11 Phones.
2022-05-28 09:13:46 +05:30
Marcin Majewski 5b4e32e184 fix(syosetu): Change cover to unavailable (#275)
fix(syosetu): Set cover to not available
2022-05-28 08:05:58 +05:30
Marcin Majewski a7fb3962f1 fix(typo): spelling mistake causing a crash (#276) 2022-05-28 08:05:11 +05:30
Marcin Majewski d333381959 feat(webview-scroll): webview scrolls to saved (#274)
Now webview has webViewScroll which after any change scrolls webView to given percentage

Co-authored-by: unknown <SkillGG>
2022-05-27 20:43:33 +05:30
Rajarshee Chatterjee d599e6669f chore: Update translations (#251)
* New translations strings.json (Turkish)

* New translations strings.json (Korean)

* New translations strings.json (Vietnamese)

* New translations strings.json (Chinese Traditional)

* New translations strings.json (Ukrainian)

* New translations strings.json (Swedish)

* New translations strings.json (Serbian (Cyrillic))

* New translations strings.json (Albanian)

* New translations strings.json (Russian)

* New translations strings.json (Portuguese)

* New translations strings.json (Polish)

* New translations strings.json (Norwegian)

* New translations strings.json (Dutch)

* New translations strings.json (Japanese)

* New translations strings.json (Romanian)

* New translations strings.json (Italian)

* New translations strings.json (Hungarian)

* New translations strings.json (Hebrew)

* New translations strings.json (Finnish)

* New translations strings.json (Greek)

* New translations strings.json (German)

* New translations strings.json (Danish)

* New translations strings.json (Czech)

* New translations strings.json (Catalan)

* New translations strings.json (Arabic)

* New translations strings.json (Afrikaans)

* New translations strings.json (Spanish)

* New translations strings.json (French)

* New translations strings.json (Portuguese, Brazilian)

* New translations strings.json (Turkish)

* New translations strings.json (Korean)

* New translations strings.json (Vietnamese)

* New translations strings.json (Chinese Traditional)

* New translations strings.json (Ukrainian)

* New translations strings.json (Swedish)

* New translations strings.json (Serbian (Cyrillic))

* New translations strings.json (Albanian)

* New translations strings.json (Russian)

* New translations strings.json (Portuguese)

* New translations strings.json (Polish)

* New translations strings.json (Norwegian)

* New translations strings.json (Dutch)

* New translations strings.json (Japanese)

* New translations strings.json (Romanian)

* New translations strings.json (Italian)

* New translations strings.json (Hungarian)

* New translations strings.json (Hebrew)

* New translations strings.json (Finnish)

* New translations strings.json (Greek)

* New translations strings.json (German)

* New translations strings.json (Danish)

* New translations strings.json (Czech)

* New translations strings.json (Catalan)

* New translations strings.json (Arabic)

* New translations strings.json (Afrikaans)

* New translations strings.json (Spanish)

* New translations strings.json (French)

* New translations strings.json (Portuguese, Brazilian)

* New translations strings.json (Turkish)

* New translations strings.json (German)

* New translations strings.json (German)

* New translations strings.json (Italian)

* New translations strings.json (Russian)

* New translations strings.json (Russian)
2022-05-27 05:21:49 +05:30
Rider21 5436bf8aaf fix: Source Fixes (#272)
* updated link

* small correction

* Bypass

* Update renovels.js
2022-05-27 05:14:17 +05:30
Rajarshee Chatterjee 9f6f9e71ca fix: Remove Circular Dependencies 2022-05-09 07:06:48 +05:30
Rajarshee Chatterjee 6caba807df fix:(source) 1stKissNovel: No Chapters 2022-05-08 12:29:37 +05:30
Rajarshee Chatterjee 9dabf1a60e Minor fixes 2022-05-08 12:26:29 +05:30
Rajarshee Chatterjee 7716ab75de refactor: Reader Settings Screen 2022-05-08 11:47:22 +05:30
Rajarshee Chatterjee 94b2dd51cb fix(source): ReaperScans (Closes LNReader/lnreader-sources#224) 2022-05-08 07:51:59 +05:30
Rajarshee Chatterjee 010d49daea fix: Update Icon Links 2022-05-08 07:27:31 +05:30
Rajarshee Chatterjee 6eb733eb66 feat: Ability to Save Custom Themes (Closes #145) 2022-05-07 18:33:40 +05:30
Rajarshee Chatterjee 33a33842c6 feat: Add WebView Button in Reader 2022-05-07 17:21:58 +05:30
Rajarshee Chatterjee 1bfd24a1b4 fix: History Screen Fixes 2022-05-07 16:53:22 +05:30
Rajarshee Chatterjee 7e1bf4dd3f feat: Add Path Aliases 2022-05-07 16:50:53 +05:30
Rajarshee Chatterjee 955e552597 fix: Make MyAnimeListCard Scrollable 2022-05-07 16:50:23 +05:30
Rajarshee Chatterjee 8b586754d2 fix: MyAnimeList Search 2022-05-07 15:16:40 +05:30
Rajarshee Chatterjee b921f9b764 refactor: Remove Ununsed Files and Dependencies 2022-05-07 14:44:42 +05:30
Rajarshee Chatterjee 4d30546b9a build: Remove Upload Artifact 2022-05-07 14:44:32 +05:30
Rajarshee Chatterjee 58eac42b14 refactor: Refactor Reader Bottomsheet (#249)
* refactor: Refactor Reader Bottomsheet

* refactor: Remove Unused Files
2022-05-07 13:36:36 +05:30
Rajarshee Chatterjee 872ec15ca5 build: Fix Artifact Name 2022-05-06 21:23:28 +05:30
Rajarshee Chatterjee e013b3cc55 build: Fix Artifact Name 2022-05-06 20:58:20 +05:30
Rajarshee Chatterjee 8e58ce2eb1 feat(source): Add SonicMTL and MTLNovel.Club
Closes LNReader/lnreader-sources#221, Closes LNReader/lnreader-sources#223
2022-05-06 20:31:44 +05:30
Rajarshee Chatterjee a9e2d872f2 build: Add Upload Artifact 2022-05-06 20:23:01 +05:30
Rajarshee Chatterjee ccdb150d6a fix: Add Strings to Translations 2022-05-06 20:12:23 +05:30
Rajarshee Chatterjee 4e1d8533e3 Release v1.1.10 2022-05-04 07:21:14 +05:30
Rajarshee Chatterjee da18cd9ade chore: Update translations (#244)
Lint / ESLint (push) Has been cancelled
* New translations strings.json (Turkish)

* New translations strings.json (Portuguese, Brazilian)
2022-05-04 07:18:32 +05:30
Rajarshee Chatterjee 802aa6d71a fix(source): MyLoveNovel (Closes LNReader/lnreader-sources#217) 2022-05-04 07:03:51 +05:30
Rajarshee Chatterjee 3fbca6c14d fix: Links not working in Render HTML (Closes #245) 2022-05-04 06:56:30 +05:30
Rajarshee Chatterjee 5a07255b95 fix: Tracker Button showing when Tracker is not available 2022-05-04 06:56:06 +05:30
Rajarshee Chatterjee 9ed1d5c8b6 Release v1.1.9 2022-05-03 18:30:03 +05:30
Rajarshee Chatterjee f07a4845b8 fix: Browse Screen Search Message 2022-05-03 17:51:37 +05:30
Rajarshee Chatterjee 922ffbe7b0 fix: Tracker Button not Showing 2022-05-03 17:50:14 +05:30
230 changed files with 11210 additions and 6001 deletions
+2 -2
View File
@@ -5,13 +5,13 @@ on:
- main
jobs:
build-android:
name: Build app
name: Build App
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: |
npm install
npm install --legacy-peer-deps
- name: Cache Gradle Wrapper
uses: actions/cache@v2
+10 -3
View File
@@ -1,5 +1,12 @@
name: Lint
on: [push, pull_request]
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
name: ESLint
@@ -7,6 +14,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install modules
run: npm i
run: npm i --legacy-peer-deps
- name: Run ESLint
run: npm run lint
+1 -2
View File
@@ -84,7 +84,6 @@ project.ext.react = [
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle"
apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle"
/**
* Set this to true to create two separate APKs instead of one:
@@ -136,7 +135,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.8"
versionName "1.1.10"
}
splits {
abi {
+7 -20
View File
@@ -4,6 +4,7 @@
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@@ -21,6 +22,12 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
<application
android:name=".MainApplication"
android:largeHeap="true"
@@ -30,26 +37,6 @@
android:allowBackup="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<meta-data
android:name="expo.modules.updates.EXPO_UPDATE_URL"
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data
android:name="expo.modules.updates.EXPO_SDK_VERSION"
android:value="41.0.0"/>
<meta-data
android:name="expo.modules.updates.ENABLED"
android:value="false"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
android:value="ALWAYS"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
android:value="0"/>
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
@@ -1,9 +1,11 @@
package com.rajarsheechatterjee.LNReader;
import android.content.res.Configuration;
import android.content.Intent;
import android.os.Bundle;
import android.graphics.Color;
import android.view.KeyEvent;
import android.view.WindowManager;
import android.os.Build;
import android.view.View;
@@ -12,6 +14,7 @@ import android.view.Window;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import expo.modules.splashscreen.singletons.SplashScreen;
@@ -31,24 +34,53 @@ public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// SplashScreen.show(...) has to be called after super.onCreate(...)
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
// SplashScreen.show(...) has to be called after super.onCreate(...)
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
Window w = getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
Window w = getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
super.onCreate(null);
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
super.onCreate(null);
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (VolumeButtonListener.isActive()) {
int action = event.getAction();
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (action == KeyEvent.ACTION_DOWN) {
// volup
VolumeButtonListener.up();
if (!VolumeButtonListener.prevent)
return super.dispatchKeyEvent(event);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
//voldown
VolumeButtonListener.down();
if (!VolumeButtonListener.prevent)
return super.dispatchKeyEvent(event);
}
return true;
default:
return super.dispatchKeyEvent(event);
}
}
return super.dispatchKeyEvent(event);
}
/**
* Returns the name of the main component registered from JavaScript.
@@ -68,4 +100,5 @@ public class MainActivity extends ReactActivity {
}
};
}
}
@@ -2,34 +2,24 @@ package com.rajarsheechatterjee.LNReader;
import android.app.Application;
import android.content.Context;
import android.net.Uri;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.rajarsheechatterjee.LNReader.generated.BasePackageList;
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
import org.unimodules.adapters.react.ReactAdapterPackage;
import org.unimodules.adapters.react.ModuleRegistryAdapter;
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
import org.unimodules.core.interfaces.Package;
import org.unimodules.core.interfaces.SingletonModule;
import expo.modules.constants.ConstantsPackage;
import expo.modules.permissions.PermissionsPackage;
import expo.modules.filesystem.FileSystemPackage;
import expo.modules.updates.UpdatesController;
import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
public class MainApplication extends Application implements ReactApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
@@ -46,7 +36,8 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
packages.add(new NavigationBarColorPackage());
packages.add(new com.rajarsheechatterjee.LNReader.NavigationBarColorPackage());
packages.add(new VolumeButtonListenerPackage());
return packages;
}
@@ -59,24 +50,6 @@ public class MainApplication extends Application implements ReactApplication {
protected JSIModulePackage getJSIModulePackage() {
return new ReanimatedJSIModulePackage();
}
@Override
protected @Nullable String getJSBundleFile() {
if (BuildConfig.DEBUG) {
return super.getJSBundleFile();
} else {
return UpdatesController.getInstance().getLaunchAssetFile();
}
}
@Override
protected @Nullable String getBundleAssetName() {
if (BuildConfig.DEBUG) {
return super.getBundleAssetName();
} else {
return UpdatesController.getInstance().getBundleAssetName();
}
}
};
@Override
@@ -89,10 +62,6 @@ public class MainApplication extends Application implements ReactApplication {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
if (!BuildConfig.DEBUG) {
UpdatesController.initialize(this);
}
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
@@ -22,7 +22,6 @@ public class BasePackageList {
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.splashscreen.SplashScreenPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.updates.UpdatesPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);
}
@@ -0,0 +1,93 @@
package com.rajarsheechatterjee.VolumeButtonListener;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class VolumeButtonListener extends ReactContextBaseJavaModule {
static ReactApplicationContext appContext;
public VolumeButtonListener(ReactApplicationContext context) {
super(context);
appContext = context;
}
private static boolean active = false;
public static boolean isActive() {
return active;
}
public static boolean prevent = false;
public static void sendEvent(boolean up) {
if (active) {
WritableMap args = Arguments.createMap();
if (up)
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeUp", args);
else
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeDown", args);
}
}
public static void down() {
sendEvent(false);
}
public static void up() {
sendEvent(true);
}
@ReactMethod
public void connect() {
active = true;
}
@ReactMethod
void disconnect() {
active = false;
}
@ReactMethod
public void pause() {
active = false;
}
@ReactMethod
public void unpause() {
active = true;
}
@ReactMethod
public void preventDefault() {
prevent = true;
}
@ReactMethod
public void noPreventDefault() {
prevent = false;
}
@NonNull
@Override
public String getName() {
return "VolumeButtonListener";
}
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}
}
@@ -0,0 +1,30 @@
package com.rajarsheechatterjee.VolumeButtonListener;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class VolumeButtonListenerPackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<NativeModule>();
modules.add(new VolumeButtonListener(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
+16
View File
@@ -2,5 +2,21 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
[
'module-resolver',
{
root: ['./src'],
alias: {
'@components': './src/components',
'@hooks': './src/hooks',
'@screens': './src/screens',
'@strings': './strings',
'@redux': './src/redux',
},
extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
},
],
],
};
};
+1
View File
@@ -1,4 +1,5 @@
pull_request_title: 'chore: Update translations'
commit_message: '[ci skip]'
files:
- source: /strings/languages/en/strings.json
translation: /strings/languages/%two_letters_code%/strings.json
+442 -342
View File
File diff suppressed because it is too large Load Diff
+8 -9
View File
@@ -1,6 +1,6 @@
{
"name": "lnreader",
"version": "1.1.8",
"version": "1.1.10",
"private": true,
"scripts": {
"android": "react-native run-android",
@@ -16,19 +16,19 @@
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.15.11",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/masked-view": "^0.1.11",
"@react-native-community/netinfo": "^7.1.1",
"@react-native-community/slider": "^4.1.12",
"@react-native-picker/picker": "^2.4.1",
"@react-navigation/material-bottom-tabs": "^6.0.9",
"@react-navigation/native": "^6.0.6",
"@react-navigation/stack": "^6.0.11",
"@reduxjs/toolkit": "^1.7.2",
"@types/react-native-vector-icons": "^6.4.10",
"buffer": "^6.0.3",
"cheerio": "^1.0.0-rc.10",
"expo": "^41.0.1",
"expo-document-picker": "^9.2.4",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-haptics": "~10.0.0",
"expo-keep-awake": "~9.1.2",
"expo-linear-gradient": "~9.1.0",
@@ -37,11 +37,10 @@
"expo-notifications": "~0.11.6",
"expo-splash-screen": "^0.11.4",
"expo-sqlite": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"i18n-js": "^3.8.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment": "^2.29.4",
"prop-types": "^15.7.2",
"qs": "^6.10.1",
"react": "16.13.1",
@@ -58,18 +57,16 @@
"react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "^3.2.0",
"react-native-screens": "^3.9.0",
"react-native-skeleton-placeholder": "^5.0.0",
"react-native-swipe-gestures": "^1.0.5",
"react-native-tab-view": "^3.1.1",
"react-native-tts": "^4.1.0",
"react-native-unimodules": "^0.13.3",
"react-native-vector-icons": "^9.0.0",
"react-native-web": "^0.17.5",
"react-native-webview": "^11.14.3",
"react-redux": "^7.2.6",
"redux": "^4.1.2",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.4.0",
"rn-fetch-blob": "^0.12.0",
"rn-sliding-up-panel": "^2.4.4",
"sanitize-html": "^2.7.0"
},
@@ -80,10 +77,12 @@
"@types/jest": "^27.4.0",
"@types/lodash": "^4.14.179",
"@types/react-native": "^0.66.15",
"@types/react-native-vector-icons": "^6.4.10",
"@types/react-test-renderer": "^17.0.1",
"@types/sanitize-html": "^2.6.2",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"babel-plugin-module-resolver": "^4.1.0",
"eslint": "^7.32.0",
"eslint-plugin-react-native": "^3.11.0",
"husky": "^7.0.4",
+7 -2
View File
@@ -5,7 +5,10 @@ const MadaraSources = require('../src/sources/multisrc/madara/MadaraSources.json
const AllSources = require('../src/sources/sources.json');
const getScraperName = scraperName => {
let name = scraperName.replace(/\./g, 'Dot').replace('1st', 'First');
let name = scraperName
.replace(/\s+/g, '')
.replace(/\./g, 'Dot')
.replace('1st', 'First');
return name;
};
@@ -40,7 +43,9 @@ MadaraSources.forEach(madaraSource => {
allSourcesContent.push({
sourceId: madaraSource.sourceId,
sourceName: madaraSource.sourceName,
icon: `https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/${madaraSource.sourceName.toLowerCase()}.png?raw=true`,
icon: `https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/${madaraSource.sourceName
.replace(/\s+/g, '')
.toLowerCase()}.png?raw=true`,
url: madaraSource.baseUrl,
lang: madaraSource.options.lang || 'English',
});
+1 -1
View File
@@ -254,7 +254,7 @@ try {
return novels;`
: `
showToast('Search is not available in this source')
showToast('${source.sourceName}: Search is not available in this source')
return [];
`
+2 -2
View File
@@ -2,12 +2,12 @@ import React from 'react';
import { StatusBar } from 'react-native';
import { Appbar as PaperAppbar } from 'react-native-paper';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
interface AppbarProps {
title: string;
handleGoBack: () => void;
theme: ThemeTypeV1;
theme: ThemeType;
}
const Appbar: React.FC<AppbarProps> = ({ title, handleGoBack, theme }) => (
-17
View File
@@ -1,17 +0,0 @@
import React from 'react';
import { View } from 'react-native';
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: `${theme.textColorPrimary}66`,
height: 4,
width: 40,
borderRadius: 50,
top: 8,
alignSelf: 'center',
}}
/>
);
export default BottomSheetHandle;
+11 -11
View File
@@ -1,7 +1,7 @@
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
import React from 'react';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
export enum ButtonVariation {
DEFAULT = 'default',
@@ -14,31 +14,31 @@ interface Props {
title: string;
textColor?: string;
onPress?: () => void;
margin?: number;
theme: ThemeTypeV1;
theme: ThemeType;
style?: ViewStyle;
}
const Button: React.FC<Props> = ({
variation = ButtonVariation.DEFAULT,
title,
textColor,
margin = 0,
onPress,
theme,
style,
}) => {
return (
<View
style={[
styles.button,
{ margin },
variation === 'outlined'
variation === ButtonVariation.OUTLINED
? {
...styles.outlined,
borderColor: theme.textColorHint,
}
: variation !== 'clear' && {
: variation !== ButtonVariation.CLEAR && {
backgroundColor: theme.colorAccent,
},
style,
]}
>
<Pressable
@@ -50,10 +50,10 @@ const Button: React.FC<Props> = ({
style={{
color: textColor
? textColor
: variation === 'outlined'
: variation === ButtonVariation.OUTLINED
? theme.colorAccent
: variation === ButtonVariation.CLEAR
? theme.colorAccent
: variation === 'default'
? theme.colorButtonText
: theme.textColorPrimary,
}}
numberOfLines={1}
+8 -2
View File
@@ -4,7 +4,13 @@ import { Checkbox as PaperCheckbox } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
export const Checkbox = ({
label,
status,
onPress,
disabled = false,
theme,
}) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
@@ -24,7 +30,7 @@ export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
uncheckedColor={theme.textColorSecondary}
disabled={disabled}
/>
<Text style={{ color: theme.textColorPrimary, marginLeft: 12 }}>
<Text style={{ color: theme.textColorSecondary, marginLeft: 12 }}>
{label}
</Text>
</Pressable>
+2 -2
View File
@@ -1,11 +1,11 @@
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
interface ChipProps {
label: string;
theme: ThemeTypeV1;
theme: ThemeType;
}
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
@@ -2,33 +2,46 @@ import React, { useState } from 'react';
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
import { Modal, Portal, TextInput } from 'react-native-paper';
import { ThemeType } from '../../theme/types';
const ColorPickerModal = ({
interface ColorPickerModalProps {
visible: boolean;
title: string;
color: string;
onSubmit: (val: string) => void;
closeModal: () => void;
theme: ThemeType;
showAccentColors?: boolean;
}
const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
theme,
color,
title,
onSubmit,
hideModal,
modalVisible,
closeModal,
visible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
const [text, setText] = useState<string>(color);
const [error, setError] = useState<string | null>();
const onDismiss = () => {
hideModal();
setText();
setError();
closeModal();
if (error) {
setText(color);
}
setError(null);
};
const onChangeText = txt => setText(txt);
const onChangeText = (txt: string) => setText(txt);
const onSubmitEditing = () => {
const re = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
const re = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3})$/i;
if (text.match(re)) {
onSubmit(text);
hideModal();
closeModal();
} else {
setError('Enter a valid hex color code');
}
@@ -69,7 +82,7 @@ const ColorPickerModal = ({
return (
<Portal>
<Modal
visible={modalVisible}
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
@@ -79,7 +92,7 @@ const ColorPickerModal = ({
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
{title}
</Text>
{showAccentColors && (
{showAccentColors ? (
<FlatList
contentContainerStyle={{ marginBottom: 8 }}
data={accentColors}
@@ -104,16 +117,16 @@ const ColorPickerModal = ({
}}
onPress={() => {
onSubmit(item);
hideModal();
closeModal();
}}
/>
</View>
)}
/>
)}
) : null}
<TextInput
value={text}
defaultValue={color}
defaultValue={typeof color === 'string' ? color : ''}
placeholder="Hex Color Code (E.g. #3399FF)"
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
@@ -121,7 +134,7 @@ const ColorPickerModal = ({
theme={textInputTheme}
underlineColor={theme.textColorHint}
dense
error={error}
error={Boolean(error)}
/>
<Text style={styles.errorText}>{error}</Text>
</Modal>
@@ -0,0 +1,54 @@
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { ThemeType } from '../../theme/types';
interface ColorPreferenceItemProps {
label: string;
description?: string;
onPress: () => void;
theme: ThemeType;
}
const ColorPreferenceItem: React.FC<ColorPreferenceItemProps> = ({
label,
description,
theme,
onPress,
}) => (
<Pressable
style={styles.container}
android_ripple={{ color: theme.rippleColor }}
onPress={onPress}
>
<View>
<Text style={[styles.label, { color: theme.textColorPrimary }]}>
{label}
</Text>
<Text style={{ color: theme.textColorSecondary }}>
{description?.toUpperCase?.()}
</Text>
</View>
<View style={[{ backgroundColor: description }, styles.colorPreview]} />
</Pressable>
);
export default ColorPreferenceItem;
const styles = StyleSheet.create({
container: {
padding: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
label: {
fontSize: 16,
},
colorPreview: {
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
},
});
@@ -1,21 +1,37 @@
import React from 'react';
import { Pressable, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { ThemeType } from '../../theme/types';
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
interface ToggleButtonProps {
icon: string;
selected: boolean;
theme: ThemeType;
color?: string;
onPress: () => void;
}
export const ToggleButton: React.FC<ToggleButtonProps> = ({
icon,
selected,
theme,
color,
onPress,
}) => (
<View
style={{
borderRadius: 8,
borderRadius: 6,
overflow: 'hidden',
marginHorizontal: 4,
marginHorizontal: 6,
}}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 10,
padding: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: selected ? theme.rippleColor : 'transparent',
}}
onPress={onPress}
>
@@ -30,7 +46,14 @@ export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
</View>
);
export const ToggleColorButton = ({
interface ToggleColorButtonProps {
selected: boolean;
backgroundColor: string;
textColor: string;
onPress: () => void;
}
export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
selected,
backgroundColor,
textColor,
@@ -56,9 +79,11 @@ export const ToggleColorButton = ({
}}
onPress={onPress}
>
{selected && (
<MaterialCommunityIcons name="check" color={textColor} size={24} />
)}
<MaterialCommunityIcons
name={selected ? 'check' : 'format-color-text'}
color={textColor}
size={24}
/>
</Pressable>
</View>
);
@@ -0,0 +1,74 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { getString } from '@strings/translations';
import { Dialog, Portal } from 'react-native-paper';
import { ThemeType } from '../../theme/types';
import { Button } from '@components/index';
import { ButtonVariation } from '@components/Button/Button';
interface ConfirmationDialogProps {
title: string;
visible: boolean;
theme: ThemeType;
onSubmit: () => void;
onDismiss: () => void;
}
const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
title,
visible,
onDismiss,
theme,
onSubmit,
}) => {
const handleOnSubmit = () => {
onSubmit();
onDismiss();
};
return (
<Portal>
<Dialog
visible={visible}
onDismiss={onDismiss}
style={[styles.container, { backgroundColor: theme.colorPrimary }]}
>
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
{title}
</Dialog.Title>
<Dialog.Actions>
<Button
theme={theme}
onPress={onDismiss}
title={getString('common.cancel')}
variation={ButtonVariation.CLEAR}
/>
<Button
theme={theme}
onPress={handleOnSubmit}
title={getString('common.ok')}
variation={ButtonVariation.CLEAR}
style={styles.button}
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default ConfirmationDialog;
const styles = StyleSheet.create({
container: {
borderRadius: 8,
},
title: {
letterSpacing: 0,
fontSize: 16,
},
button: {
marginLeft: 4,
},
});
-8
View File
@@ -1,8 +0,0 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import { StyleSheet } from 'react-native';
export const Dialog = ({ visible, theme }) => {};
const styles = StyleSheet.create({});
+2 -2
View File
@@ -1,12 +1,12 @@
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
interface EmptyViewProps {
icon?: string;
description: string;
theme: ThemeTypeV1;
theme: ThemeType;
}
const EmptyView: React.FC<EmptyViewProps> = ({ icon, description, theme }) => (
+10 -11
View File
@@ -1,24 +1,21 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Container } from '..';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
interface ErrorScreenProps {
error?: string;
theme: ThemeTypeV1;
theme: ThemeType;
}
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, theme }) => {
return (
<Container>
<View style={styles.container}>
<Text style={[styles.icon, { color: theme.textColorHint }]}>ಥ_ಥ</Text>
<Text style={[styles.error, { color: theme.textColorHint }]}>
{error}
</Text>
</View>
</Container>
<View style={styles.container}>
<Text style={[styles.icon, { color: theme.textColorHint }]}>ಥ_ಥ</Text>
<Text style={[styles.error, { color: theme.textColorHint }]}>
{error}
</Text>
</View>
);
};
@@ -35,5 +32,7 @@ const styles = StyleSheet.create({
},
error: {
marginTop: 16,
paddingHorizontal: 16,
textAlign: 'center',
},
});
-28
View File
@@ -1,28 +0,0 @@
import React from 'react';
import { Text, View } from 'react-native';
import { IconButton } from 'react-native-paper';
// import {Row} from './Common';
import EmptyView from './EmptyView';
const ErrorView = ({ theme, onRetry, error, openWebView }) => (
<View style={{ flex: 1, justifyContent: 'center' }}>
<EmptyView
icon="(; ̄Д ̄)"
description={error}
style={{ color: theme.textColorSecondary }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.text}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
</EmptyView>
</View>
);
export default ErrorView;
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
type Props = {
name: string;
@@ -11,7 +11,7 @@ type Props = {
disabled?: boolean;
padding?: number;
onPress?: () => void;
theme: ThemeTypeV1;
theme: ThemeType;
style?: ViewStyle;
};
+5 -5
View File
@@ -2,14 +2,14 @@ import React, { ReactNode } from 'react';
import { StyleSheet, View } from 'react-native';
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
interface ListItemProps {
title: string;
description?: string | null;
icon?: string;
onPress?: () => void;
theme: ThemeTypeV1;
theme: ThemeType;
}
const Section = ({ children }: { children: ReactNode }) => (
@@ -21,7 +21,7 @@ const SubHeader = ({
theme,
}: {
children: ReactNode;
theme: ThemeTypeV1;
theme: ThemeType;
}) => (
<PaperList.Subheader style={{ color: theme.colorAccent }}>
{children}
@@ -56,7 +56,7 @@ const Item: React.FC<ListItemProps> = ({
/>
);
const Divider = ({ theme }: { theme: ThemeTypeV1 }) => (
const Divider = ({ theme }: { theme: ThemeType }) => (
<PaperDivider
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
/>
@@ -69,7 +69,7 @@ const InfoItem = ({
}: {
title: string;
icon: string;
theme: ThemeTypeV1;
theme: ThemeType;
}) => (
<PaperList.Item
title={title}
@@ -0,0 +1,22 @@
import React from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { ThemeType } from '../../theme/types';
interface Props {
theme: ThemeType;
}
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
<ActivityIndicator color={theme.colorAccent} style={styles.indicator} />
);
export default LoadingMoreIndicator;
const styles = StyleSheet.create({
indicator: {
padding: 32,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
@@ -1,17 +1,14 @@
import React from 'react';
import { StyleSheet, ActivityIndicator } from 'react-native';
import { Container } from '..';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
const LoadingScreen: React.FC<{ theme: ThemeTypeV1 }> = ({ theme }) => (
<Container>
<ActivityIndicator
size={50}
color={theme.colorAccent}
style={styles.indicator}
/>
</Container>
const LoadingScreen: React.FC<{ theme: ThemeType }> = ({ theme }) => (
<ActivityIndicator
size={50}
color={theme.colorAccent}
style={styles.indicator}
/>
);
export default LoadingScreen;
-1
View File
@@ -14,7 +14,6 @@ import ListView from './ListView';
import { useDeviceOrientation } from '../services/utils/helpers';
import { useSettings } from '../hooks/reduxHooks';
import { defaultCoverUri } from '../sources/helpers/constants';
import { coverPlaceholderColor } from '../theme/colors';
const NovelCover = ({
-180
View File
@@ -1,180 +0,0 @@
import React from 'react';
import {
Pressable,
PressableProps,
StyleSheet,
Text,
View,
} from 'react-native';
import FastImage from 'react-native-fast-image';
import { useAppearanceSettings, useLibrarySettings } from '../../redux/hooks';
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
import { SourceNovelItem } from '../../sources/types';
import { ThemeType } from '../../theme/types';
import {
UnreadBadge,
DownloadsBadge,
InLibraryBadge,
} from './NovelCoverBadges';
import { DisplayModes } from '../../redux/settings/settingsSlice';
import { LinearGradient } from 'expo-linear-gradient';
import { useDeviceOrientation, useNovelCoverHeight } from '../../hooks';
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
interface Props extends PressableProps {
item: LibraryNovelInfo | SourceNovelItem | NovelInfo;
isSelected: boolean;
theme: ThemeType;
showInLibraryBadge?: boolean;
}
const NovelItem: React.FC<Props> = props => {
const { item, showInLibraryBadge = false, theme, isSelected = false } = props;
const { displayMode } = useAppearanceSettings();
const { novelsPerRowLandscape, novelsPerRowPotrait } =
useAppearanceSettings();
const { showUnreadBadge, showDownloadsBadge } = useLibrarySettings();
const orientation = useDeviceOrientation();
const coverHeight = useNovelCoverHeight();
const coverSelectedStyle = {
backgroundColor: theme.primary,
opacity: 0.8,
};
return (
<View
style={[
{
flex:
1 /
(orientation === DeviceOrientation.POTRAIT
? novelsPerRowPotrait
: novelsPerRowLandscape),
},
styles.container,
isSelected && coverSelectedStyle,
]}
>
<Pressable
style={styles.pressable}
android_ripple={{ color: theme.rippleColor }}
{...props}
>
<View style={styles.badgeContainer}>
{'chaptersDownloaded' in item &&
item.chaptersDownloaded &&
showDownloadsBadge ? (
<DownloadsBadge
unreadCount={item.chaptersUnread}
downloadCount={item.chaptersDownloaded}
showUnreadBadge={showUnreadBadge}
theme={theme}
/>
) : null}
{'chaptersUnread' in item &&
item.chaptersUnread &&
showUnreadBadge ? (
<UnreadBadge
unreadCount={item.chaptersUnread}
downloadCount={item.chaptersDownloaded}
showDownloadsBadge={showDownloadsBadge}
theme={theme}
/>
) : null}
</View>
{showInLibraryBadge ? <InLibraryBadge theme={theme} /> : null}
<FastImage
source={{ uri: item.novelCover }}
style={[
{ height: coverHeight },
styles.image,
showInLibraryBadge && styles.inLibrary,
]}
>
{displayMode === DisplayModes.Compact ? (
<CompactTitle name={item.novelName} />
) : null}
</FastImage>
{displayMode === DisplayModes.Comfortable ? (
<Text
numberOfLines={2}
style={[{ color: theme.textColorPrimary }, styles.title]}
>
{item.novelName}
</Text>
) : null}
</Pressable>
</View>
);
};
export default NovelItem;
const CompactTitle = ({ name }: { name: string }) => (
<View style={styles.compactTitleContainer}>
<LinearGradient
colors={['transparent', 'rgba(0,0,0,0.7)']}
style={styles.linearGradient}
>
<Text numberOfLines={2} style={[styles.title, styles.compactTitle]}>
{name}
</Text>
</LinearGradient>
</View>
);
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
borderRadius: 6,
margin: 2,
},
pressable: {
flex: 1,
padding: 4,
},
badgeContainer: {
position: 'absolute',
zIndex: 1,
top: 12,
left: 12,
flexDirection: 'row',
},
image: {
borderRadius: 4,
},
inLibrary: {
opacity: 0.5,
},
title: {
padding: 4,
fontSize: 12,
},
compactTitleContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flex: 1,
borderRadius: 4,
},
linearGradient: {
borderRadius: 4,
padding: 4,
paddingTop: 16,
},
compactTitle: {
color: 'rgba(255,255,255,1)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: {
width: -1,
height: 1,
},
textShadowRadius: 10,
},
});
@@ -1,104 +0,0 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { ThemeType } from '../../theme/types';
interface UnreadBadgeProps {
unreadCount: number;
downloadCount: number;
showDownloadsBadge: boolean;
theme: ThemeType;
}
const UnreadBadge: React.FC<UnreadBadgeProps> = ({
unreadCount,
downloadCount,
showDownloadsBadge,
theme,
}) => (
<Text
style={[
styles.unreadBadge,
(!downloadCount || !showDownloadsBadge) && styles.borderLeftRadius,
{ backgroundColor: theme.primary, color: theme.onPrimary },
]}
>
{unreadCount}
</Text>
);
interface DownloadsBadgeProps {
unreadCount: number;
downloadCount: number;
showUnreadBadge: boolean;
theme: ThemeType;
}
const DownloadsBadge: React.FC<DownloadsBadgeProps> = ({
unreadCount,
downloadCount,
showUnreadBadge,
theme,
}) => (
<Text
style={[
styles.downloadBadge,
(!unreadCount || !showUnreadBadge) && styles.borderRightRadius,
{ backgroundColor: theme.tertiary, color: theme.onTertiary },
]}
>
{downloadCount}
</Text>
);
interface InLibraryBadgeProps {
theme: ThemeType;
}
const InLibraryBadge: React.FC<InLibraryBadgeProps> = ({ theme }) => (
<Text
style={[
styles.inLibraryBadge,
{ backgroundColor: theme.primary, color: theme.onPrimary },
]}
>
In library
</Text>
);
export { UnreadBadge, DownloadsBadge, InLibraryBadge };
const styles = StyleSheet.create({
borderLeftRadius: {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
borderRightRadius: {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
unreadBadge: {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingVertical: 2,
paddingHorizontal: 4,
fontSize: 12,
},
downloadBadge: {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingVertical: 2,
paddingHorizontal: 4,
fontSize: 12,
},
inLibraryBadge: {
position: 'absolute',
top: 12,
right: 12,
borderRadius: 4,
paddingVertical: 2,
paddingHorizontal: 4,
fontSize: 12,
zIndex: 2,
},
});
+1
View File
@@ -43,6 +43,7 @@ export const Searchbar = ({
<TextInput
ref={searchRef}
style={{
flex: 1,
fontSize: 16,
color: theme.textColorPrimary,
marginLeft: 4,
+3 -3
View File
@@ -2,8 +2,8 @@ import React, { useRef } from 'react';
import { Pressable, StyleSheet, View, TextInput } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { IconButtonV2 } from '..';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import IconButtonV2 from '../IconButtonV2/IconButtonV2';
import { ThemeType } from '../../theme/types';
interface RightIcon {
iconName: string;
@@ -21,7 +21,7 @@ interface SearcbarProps {
handleBackAction?: () => void;
clearSearchbar: () => void;
onLeftIconPress?: () => void;
theme: ThemeTypeV1;
theme: ThemeType;
}
const Searchbar: React.FC<SearcbarProps> = ({
+3 -3
View File
@@ -2,14 +2,14 @@ import React from 'react';
import { Pressable, StyleSheet, View, Text } from 'react-native';
import { Switch } from 'react-native-paper';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
interface SwitchItemProps {
value: boolean;
label: string;
description?: string;
onPress: () => void;
theme: ThemeTypeV1;
theme: ThemeType;
}
const SwitchItem: React.FC<SwitchItemProps> = ({
@@ -52,10 +52,10 @@ const styles = StyleSheet.create({
},
labelContainer: {
flex: 1,
justifyContent: 'center',
},
label: {
fontSize: 16,
textAlignVertical: 'center',
},
switch: {
marginLeft: 8,
-20
View File
@@ -1,20 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text as NativeText, TextPropTypes } from 'react-native';
const Text = ({ type, children, style, theme }) => {
const color =
type === 'primary'
? theme.textColorPrimary
: type === 'secondary'
? theme.textColorSecondary
: theme.textColorHint;
return <NativeText style={[style, { color }]}>{children}</NativeText>;
};
Text.propType = {
style: TextPropTypes.style,
type: PropTypes.oneOf(['primary', 'secondary', 'hint']),
theme: PropTypes.object.isRequired,
};
+2
View File
@@ -10,3 +10,5 @@ export { default as Button } from './Button/Button';
export { default as Appbar } from './Appbar/Appbar';
export { default as SwitchItem } from './SwitchItem/SwitchItem';
export { default as List } from './List/List';
export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem';
export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
+198 -72
View File
@@ -1,18 +1,23 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as SQLite from 'expo-sqlite';
import { showToast } from '../../hooks/showToast';
import { sourceManager } from '../../sources/sourceManager';
import { ChapterItem } from '../types';
import * as cheerio from 'cheerio';
import RNFetchBlob from 'rn-fetch-blob';
const db = SQLite.openDatabase('lnreader.db');
const insertChaptersQuery =
'INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)';
export const insertChapters = async (novelId, chapters) => {
db.transaction(tx => {
chapters.map(
chapter =>
export const insertChapters = async (
novelId: number,
chapters: ChapterItem[],
) => {
db.transaction(
tx => {
chapters.map(chapter =>
tx.executeSql(
insertChaptersQuery,
[
@@ -21,8 +26,8 @@ export const insertChapters = async (novelId, chapters) => {
chapter.releaseDate,
novelId,
],
(txObj, res) => {},
(txObj, error) => {},
(_txObj, _res) => {},
(_txObj, _error) => false,
// console.log(
// "Error ",
// error,
@@ -32,30 +37,33 @@ export const insertChapters = async (novelId, chapters) => {
// novelId
// )
),
(txObj, res) => {
// console.log('Success');
},
(txObj, error) => {
// console.log('Error ', error)
},
);
});
);
},
_err => {
// console.log('Error', err);
},
() => {
// console.log('Success ')
},
);
};
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
export const getChapters = (novelId, sort, filter) => {
return new Promise((resolve, reject) =>
export const getChapters = (novelId: number, sort: string, filter: string) => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getChaptersQuery(sort, filter),
[novelId],
(txObj, { rows: { _array } }) => {
(txObj, { rows }) => {
const _array = (rows as any)._array;
resolve(_array);
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
}),
@@ -64,8 +72,8 @@ export const getChapters = (novelId, sort, filter) => {
const getChapterQuery = 'SELECT * FROM downloads WHERE downloadChapterId = ?';
export const getChapterFromDB = async chapterId => {
return new Promise((resolve, reject) =>
export const getChapterFromDB = async (chapterId: number) => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getChapterQuery,
@@ -73,8 +81,9 @@ export const getChapterFromDB = async chapterId => {
(txObj, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
}),
@@ -96,13 +105,17 @@ export const getPrevChapter = (
novelId: number,
chapterId: number,
): Promise<ChapterItem> => {
return new Promise((resolve, reject) =>
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getPrevChapterQuery,
[novelId, chapterId],
(txObj, results) => resolve(results.rows.item(results.rows.length - 1)),
(txObj, error) => showToast(error.message),
(_txObj, results) =>
resolve(results.rows.item(results.rows.length - 1)),
(_txObj, error) => {
showToast(error.message);
return false;
},
);
}),
);
@@ -123,13 +136,16 @@ export const getNextChapter = (
novelId: number,
chapterId: number,
): Promise<ChapterItem> => {
return new Promise((resolve, reject) =>
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getNextChapterQuery,
[novelId, chapterId],
(txObj, results) => resolve(results.rows.item(0)),
(txObj, error) => showToast(error.message),
(_txObj, results) => resolve(results.rows.item(0)),
(_txObj, error) => {
showToast(error.message);
return false;
},
);
}),
);
@@ -138,14 +154,15 @@ export const getNextChapter = (
const markChapterReadQuery =
'UPDATE chapters SET `read` = 1 WHERE chapterId = ?';
export const markChapterRead = async chapterId => {
export const markChapterRead = async (chapterId: number) => {
db.transaction(tx => {
tx.executeSql(
markChapterReadQuery,
[chapterId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
@@ -154,14 +171,15 @@ export const markChapterRead = async chapterId => {
const markChapterUnreadQuery =
'UPDATE chapters SET `read` = 0 WHERE chapterId = ?';
export const markChapterUnread = async chapterId => {
export const markChapterUnread = async (chapterId: number) => {
db.transaction(tx => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
@@ -170,14 +188,15 @@ export const markChapterUnread = async chapterId => {
const markAllChaptersReadQuery =
'UPDATE chapters SET `read` = 1 WHERE novelId = ?';
export const markAllChaptersRead = async novelId => {
export const markAllChaptersRead = async (novelId: number) => {
db.transaction(tx => {
tx.executeSql(
markAllChaptersReadQuery,
[novelId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
@@ -186,14 +205,15 @@ export const markAllChaptersRead = async novelId => {
const markAllChaptersUnreadQuery =
'UPDATE chapters SET `read` = 0 WHERE novelId = ?';
export const markAllChaptersUnread = async novelId => {
export const markAllChaptersUnread = async (novelId: number) => {
db.transaction(tx => {
tx.executeSql(
markAllChaptersUnreadQuery,
[novelId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
@@ -202,8 +222,8 @@ export const markAllChaptersUnread = async novelId => {
const isChapterDownloadedQuery =
'SELECT * FROM downloads WHERE downloadChapterId=?';
export const isChapterDownloaded = async chapterId => {
return new Promise((resolve, reject) =>
export const isChapterDownloaded = async (chapterId: number) => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
isChapterDownloadedQuery,
@@ -215,8 +235,9 @@ export const isChapterDownloaded = async chapterId => {
resolve(false);
}
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
}),
@@ -226,55 +247,148 @@ export const isChapterDownloaded = async chapterId => {
const downloadChapterQuery =
'INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)';
const createImageFolder = async (
path: string,
data?: {
sourceId: number;
novelId: number;
chapterId: number;
},
): Promise<string> => {
const mkdirIfNot = async (p: string) => {
if (!(await RNFetchBlob.fs.exists(p))) {
await RNFetchBlob.fs.mkdir(p);
}
};
await mkdirIfNot(path);
if (data) {
const { sourceId, novelId, chapterId } = data;
await mkdirIfNot(`${path}/${sourceId}/`);
await mkdirIfNot(`${path}/${sourceId}/${novelId}/`);
await mkdirIfNot(`${path}/${sourceId}/${novelId}/${chapterId}/`);
return `${path}/${sourceId}/${novelId}/${chapterId}/`;
} else {
return path;
}
};
const downloadImages = async (
html: string,
sourceId: number,
novelId: number,
chapterId: number,
): Promise<string> => {
try {
const loadedCheerio = cheerio.load(html);
const imgs = loadedCheerio('img').toArray();
for (let i = 0; i < imgs.length; i++) {
const elem = loadedCheerio(imgs[i]);
const url = elem.attr('src');
if (url) {
const imageb64 = (await RNFetchBlob.fetch('GET', url)).base64();
const fileurl =
(await createImageFolder(
`${RNFetchBlob.fs.dirs.DownloadDir}/LNReader`,
{ sourceId, novelId, chapterId },
)) +
i +
'.b64.png';
elem.replaceWith(
`<img type="file" src="${url}" file-src="${fileurl}" file-id="${i}">`,
);
const exists = await RNFetchBlob.fs.exists(fileurl);
if (!exists) {
RNFetchBlob.fs.createFile(fileurl, imageb64, 'utf8');
} else {
RNFetchBlob.fs.writeFile(fileurl, imageb64, 'utf8');
}
}
}
return loadedCheerio.html();
} catch (e) {
return html;
}
};
export const downloadChapter = async (
sourceId,
novelUrl,
chapterUrl,
chapterId,
sourceId: number,
novelUrl: string,
novelId: number,
chapterUrl: string,
chapterId: number,
) => {
const source = sourceManager(sourceId);
const chapter = await source.parseChapter(novelUrl, chapterUrl);
const imagedChapterText =
chapter.chapterText &&
(await downloadImages(chapter.chapterText, sourceId, novelId, chapterId));
db.transaction(tx => {
tx.executeSql('UPDATE chapters SET downloaded = 1 WHERE chapterId = ?', [
chapterId,
]);
tx.executeSql(
downloadChapterQuery,
[chapterId, chapter.chapterName, chapter.chapterText],
(txObj, res) => {
[chapterId, chapter.chapterName, imagedChapterText],
(_txObj, _res) => {
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
});
};
export const deleteChapter = async chapterId => {
export const deleteChapter = async (
sourceId: number,
novelId: number,
chapterId: number,
) => {
const updateIsDownloadedQuery =
'UPDATE chapters SET downloaded = 0 WHERE chapterId=?';
const deleteChapterQuery = 'DELETE FROM downloads WHERE downloadChapterId=?';
const path = `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader/`;
const files = await RNFetchBlob.fs.ls(
await createImageFolder(path, { sourceId, novelId, chapterId }),
);
for (let i = 0; i < files.length; i++) {
const ex = /(.*?)_(.*?)#(.*?)/.exec(files[i]);
if (ex) {
if (
parseInt(ex[1], 10) === sourceId &&
parseInt(ex[2], 10) === chapterId
) {
if (await RNFetchBlob.fs.exists(`${path}${files[i]}`)) {
RNFetchBlob.fs.unlink(`${path}${files[i]}`);
}
}
}
}
db.transaction(tx => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
tx.executeSql(
deleteChapterQuery,
[chapterId],
(txObj, res) => {
(_txObj, _res) => {
// console.log('Chapter deleted');
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
});
@@ -288,15 +402,16 @@ const getLastReadChapterQuery = `
WHERE history.historyNovelId = ?
`;
export const getLastReadChapter = async novelId => {
return new Promise((resolve, reject) => {
export const getLastReadChapter = async (novelId: number) => {
return new Promise(resolve => {
db.transaction(tx => {
tx.executeSql(
getLastReadChapterQuery,
[novelId],
(txObj, { rows }) => resolve(rows.item(0)),
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
});
@@ -306,14 +421,15 @@ export const getLastReadChapter = async novelId => {
const bookmarkChapterQuery =
'UPDATE chapters SET bookmark = ? WHERE chapterId = ?';
export const bookmarkChapter = async (bookmark, chapterId) => {
export const bookmarkChapter = async (bookmark: boolean, chapterId: number) => {
db.transaction(tx => {
tx.executeSql(
bookmarkChapterQuery,
[!bookmark, chapterId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
});
@@ -322,14 +438,18 @@ export const bookmarkChapter = async (bookmark, chapterId) => {
const markPreviuschaptersReadQuery =
'UPDATE chapters SET `read` = 1 WHERE chapterId < ? AND novelId = ?';
export const markPreviuschaptersRead = async (chapterId, novelId) => {
export const markPreviuschaptersRead = async (
chapterId: number,
novelId: number,
) => {
db.transaction(tx => {
tx.executeSql(
markPreviuschaptersReadQuery,
[chapterId, novelId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
@@ -338,14 +458,18 @@ export const markPreviuschaptersRead = async (chapterId, novelId) => {
const markPreviousChaptersUnreadQuery =
'UPDATE chapters SET `read` = 0 WHERE chapterId < ? AND novelId = ?';
export const markPreviousChaptersUnread = async (chapterId, novelId) => {
export const markPreviousChaptersUnread = async (
chapterId: number,
novelId: number,
) => {
db.transaction(tx => {
tx.executeSql(
markPreviousChaptersUnreadQuery,
[chapterId, novelId],
(txObj, res) => {},
(txObj, error) => {
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
@@ -359,16 +483,18 @@ const getDownloadedChaptersQuery = `
WHERE chapters.downloaded = 1`;
export const getDownloadedChapters = () => {
return new Promise((resolve, reject) =>
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getDownloadedChaptersQuery,
null,
(txObj, { rows: { _array } }) => {
undefined,
(txObj, { rows }) => {
const _array = (rows as any)._array;
resolve(_array);
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
}),
+49
View File
@@ -83,3 +83,52 @@ export const getChapterFromDb = async (
}),
);
};
const deleteReadChaptersFromDbQuery = `
DELETE FROM
downloads
WHERE
downloads.downloadChapterId IN (
SELECT
chapters.chapterId
FROM
downloads
INNER JOIN chapters ON chapters.chapterId = downloads.downloadChapterId
WHERE
chapters.read = 1
);
`;
let updateChaptersDeletedQuery = `
UPDATE
chapters
SET
downloaded = 0
WHERE
chapters.chapterId IN (
SELECT
downloads.downloadChapterId
FROM
downloads
INNER JOIN chapters ON chapters.chapterId = downloads.downloadChapterId
WHERE
chapters.read = 1
);
`;
export const deleteReadChaptersFromDb = (chapterId: number) => {
db.transaction(tx => {
tx.executeSql(
updateChaptersDeletedQuery,
[],
(txObj, res) => {},
(txObj, error) => showToast(error.message),
);
tx.executeSql(
deleteReadChaptersFromDbQuery,
[],
(txObj, res) => showToast('Deleted read chapters.'),
(txObj, error) => showToast(error.message),
);
});
};
+19 -3
View File
@@ -2,7 +2,7 @@ import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
const getLibraryQuery = (sort, filter) => `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
@@ -18,6 +18,14 @@ const getLibraryQuery = (sort, filter) => `
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
LEFT JOIN (
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
FROM history
GROUP BY history.historyNovelId
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
ORDER BY history.historyTimeRead DESC
) AS H
ON novels.novelId = H.novelId
WHERE novels.followed = 1 ${filter ? 'AND ' + filter : ''}
${sort ? 'ORDER BY ' + sort : ''}
`;
@@ -29,7 +37,7 @@ export const getLibrary = (sort, filter) => {
getLibraryQuery(sort, filter),
null,
(txObj, { rows: { _array } }) => {
// console.log(_array);
// console.log(JSON.stringify(_array, null, 2));
resolve(_array);
},
(txObj, error) => {
@@ -42,7 +50,7 @@ export const getLibrary = (sort, filter) => {
const searchLibraryQuery = (searchText, sort, filter) =>
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
@@ -58,6 +66,14 @@ const searchLibraryQuery = (searchText, sort, filter) =>
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
LEFT JOIN (
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
FROM history
GROUP BY history.historyNovelId
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
ORDER BY history.historyTimeRead DESC
) AS H
ON novels.novelId = H.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' ${
filter ? 'AND ' + filter : ''
}
-9
View File
@@ -1,9 +0,0 @@
export const createDownloadTableQuery = `
CREATE TABLE IF NOT EXISTS downloads(
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
downloadChapterId INTEGER NOT NULL,
chapterName TEXT,
chapterText TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
+9
View File
@@ -0,0 +1,9 @@
export const createDownloadTableQuery = `
CREATE TABLE IF NOT EXISTS downloads(
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
downloadChapterId INTEGER NOT NULL,
chapterName TEXT,
chapterText TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId) ON DELETE CASCADE
)
`;
+2 -2
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { appversion } from '../utils/utils';
import { appVersion } from '../utils/versionUtils';
export const useGithubUpdateChecker = () => {
const latestReleaseUrl =
@@ -23,7 +23,7 @@ export const useGithubUpdateChecker = () => {
};
const isNewVersion = versionTag => {
let currentVersion = `${appversion}`;
let currentVersion = `${appVersion}`;
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, '');
+1
View File
@@ -7,3 +7,4 @@ export { default as useNextAndPrevChapter } from './useNextAndPrevChapter';
export { default as useDeviceOrientation } from './useDeviceOrientation';
export { default as useNovelCoverHeight } from './useNovelCoverHeight';
export { default as useNovelTrackerInfo } from './useNovelTrackerInfo';
export { default as useBoolean } from './useBoolean';
+1 -8
View File
@@ -1,5 +1,5 @@
import { useSelector } from 'react-redux';
import { darkTheme } from '../theme/v1/theme/theme';
import { darkTheme } from '../theme/theme';
const useTheme = () => {
const theme = useSelector(state => state.settingsReducer.theme);
@@ -7,12 +7,6 @@ const useTheme = () => {
return theme || darkTheme;
};
const useReaderSettings = () => {
const readerSettings = useSelector(state => state.settingsReducer.reader);
return readerSettings;
};
const useSettings = () => {
const settings = useSelector(state => state.settingsReducer);
@@ -118,7 +112,6 @@ const useLibraryFilters = () => {
export {
useTheme,
useReaderSettings,
useLibrary,
useSettings,
usePreferences,
+21
View File
@@ -0,0 +1,21 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
interface ReturnType {
value: boolean;
setValue: Dispatch<SetStateAction<boolean>>;
setTrue: () => void;
setFalse: () => void;
toggle: () => void;
}
const useBoolean = (defaultValue?: boolean): ReturnType => {
const [value, setValue] = useState(!!defaultValue);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
const toggle = useCallback(() => setValue(x => !x), []);
return { value, setValue, setTrue, setFalse, toggle };
};
export default useBoolean;
-65
View File
@@ -1,65 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { useAppDispatch, useSavedChapterData } from '../redux/hooks';
import { saveChapterProgress } from '../redux/localStorage/localStorageSlice';
export const useChapterProgress = (chapterId: number, readerRef: any) => {
const dispatch = useAppDispatch();
const { progressPercentage = 0, progressLocation = 0 } =
useSavedChapterData(chapterId);
const [percentage, setPercentage] = useState(progressPercentage);
const [location, setLocation] = useState(progressLocation);
const percentageRef = useRef(0);
percentageRef.current = percentage;
const locationRef = useRef(0);
locationRef.current = location;
const setProgress = ({ nativeEvent }: { nativeEvent: any }) => {
const currentOffsetY = nativeEvent.contentOffset.y;
const currentPosition =
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
const currentProgressPercentage = Math.round(
(currentPosition / nativeEvent.contentSize.height) * 100,
);
setPercentage(currentProgressPercentage);
setLocation(currentOffsetY);
};
useEffect(() => {
return () => {
dispatch(
saveChapterProgress({
chapterId,
progressLocation: locationRef.current,
progressPercentage: percentageRef.current,
}),
);
};
}, []);
const [firstLayout, setFirstLayout] = useState(true);
const scrollToSavedProgress = () => {
if (firstLayout) {
percentage < 100 &&
readerRef.current.scrollTo({
x: 0,
y: location,
animated: false,
});
setFirstLayout(false);
}
};
return {
progressLocation,
progressPercentage: percentage,
setProgress,
scrollToSavedProgress,
};
};
+3 -3
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
import { StatusBar } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useReaderSettingsV1, useSettingsV1, useThemeV1 } from '../redux/hooks';
import { useReaderSettings, useSettingsV1, useTheme } from '../redux/hooks';
import {
changeNavigationBarColor,
hideNavigationBar,
@@ -12,12 +12,12 @@ import { readerBackground } from '../screens/reader/utils/readerStyles';
const useFullscreenMode = () => {
const { addListener } = useNavigation();
const readerSettings = useReaderSettingsV1();
const readerSettings = useReaderSettings();
const { fullScreenMode } = useSettingsV1();
const backgroundColor = readerBackground(readerSettings.theme);
const theme = useThemeV1();
const theme = useTheme();
const setImmersiveMode = useCallback(() => {
if (fullScreenMode) {
+5 -1
View File
@@ -27,7 +27,10 @@ const useHistory = () => {
}
};
const clearAllHistory = () => deleteAllHistory();
const clearAllHistory = () => {
deleteAllHistory();
getHistory();
};
const removeChapterFromHistory = async (historyId: number) => {
deleteChapterHistory(historyId);
@@ -39,6 +42,7 @@ const useHistory = () => {
getHistory();
}, []),
);
return {
isLoading,
history,
-61
View File
@@ -1,61 +0,0 @@
import { useCallback, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import { LibraryNovelInfo } from '../database/types';
import { getLibraryNovelsFromDb } from '../database/queries/LibraryQueries';
import { useLibrarySettings } from '../redux/hooks';
const useLibrary = () => {
const [isLoading, setIsLoading] = useState(true);
const [novels, setNovels] = useState<LibraryNovelInfo[]>([]);
const [error, setError] = useState<string>();
const { filters, sort } = useLibrarySettings();
const getLibraryNovels = useCallback(async () => {
try {
const res = await getLibraryNovelsFromDb(filters, sort);
setNovels(res);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
}
} finally {
setIsLoading(false);
}
}, [filters, sort]);
const getLibrarySearchResults = useCallback(
async (searchText: string) => {
try {
const res = await getLibraryNovelsFromDb(filters, sort, searchText);
setNovels(res);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
}
} finally {
setIsLoading(false);
}
},
[filters, sort],
);
const clearSearchResults = () => getLibraryNovels();
useFocusEffect(
useCallback(() => {
getLibraryNovels();
}, [getLibraryNovels]),
);
return {
isLoading,
novels,
error,
getLibrarySearchResults,
clearSearchResults,
getLibraryNovels,
};
};
export default useLibrary;
+1 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useWindowDimensions } from 'react-native';
import { useDeviceOrientation } from '../hooks';
import useDeviceOrientation from './useDeviceOrientation';
import { DeviceOrientation } from '../hooks/useDeviceOrientation';
import { useAppearanceSettings } from '../redux/hooks';
View File
+3 -3
View File
@@ -1,13 +1,13 @@
import { useAppSelector } from '../redux/hooks';
const useNovelTrackerInfo = (novelId: number) => {
const { trackedNovels } = useAppSelector(state => state.trackerReducer);
const tracker = useAppSelector(state => state.trackerReducer) as any;
const isTracked = trackedNovels.find(
const isTracked = tracker.trackedNovels.find(
(novel: any) => novel.novelId === novelId,
);
return { isTracked };
return { isTracked, isTrackerAvailable: tracker.tracker };
};
export default useNovelTrackerInfo;
+2 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
const useSearch = () => {
const [searchText, setSearchText] = useState<string>('');
const useSearch = (defaultSearchText?: string) => {
const [searchText, setSearchText] = useState<string>(defaultSearchText || '');
const clearSearchbar = useCallback(() => setSearchText(''), []);
return { searchText, setSearchText, clearSearchbar };
+12 -5
View File
@@ -19,8 +19,8 @@ import MoreStack from './MoreStack';
*/
import Novel from '../screens/novel/NovelScreen';
import Reader from '../screens/reader/ReaderScreen';
import SourceScreen from '../screens/source/SourceScreen';
import GlobalSearch from '../screens/browse/globalsearch/GlobalSearch';
import BrowseSourceScreen from '../screens/BrowseSourceScreen/BrowseSourceScreen';
import GlobalSearchScreen from '../screens/GlobalSearchScreen/GlobalSearchScreen';
import Migration from '../screens/browse/migration/Migration';
import SourceNovels from '../screens/browse/SourceNovels';
import MigrateNovel from '../screens/browse/migration/MigrationNovels';
@@ -35,10 +35,14 @@ const MainNavigator = () => {
const theme = useTheme();
useEffect(() => {
setTimeout(async () => {
const timer = setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
return () => {
clearTimeout(timer);
};
}, [theme]);
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
@@ -53,10 +57,13 @@ const MainNavigator = () => {
<Stack.Screen name="Novel" component={Novel} />
<Stack.Screen name="Chapter" component={Reader} />
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="SourceScreen" component={SourceScreen} />
<Stack.Screen name="SourceScreen" component={BrowseSourceScreen} />
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
<Stack.Screen name="BrowseSettings" component={BrowseSettings} />
<Stack.Screen name="GlobalSearch" component={GlobalSearch} />
<Stack.Screen
name="GlobalSearchScreen"
component={GlobalSearchScreen}
/>
<Stack.Screen name="Migration" component={Migration} />
<Stack.Screen name="SourceNovels" component={SourceNovels} />
<Stack.Screen name="MigrateNovel" component={MigrateNovel} />
+7 -7
View File
@@ -4,15 +4,15 @@ import { createStackNavigator } from '@react-navigation/stack';
// Screens
import About from '../screens/more/About';
import Settings from '../screens/setting/SettingsScreen';
import TrackerSettings from '../screens/setting/SettingsTrackerScreen';
import ReaderSettings from '../screens/setting/SettingsReaderScreen';
import BackupSettings from '../screens/setting/SettingsBackupScreen';
import AdvancedSettings from '../screens/setting/SettingsAdvancedScreen';
import GeneralSettings from '../screens/setting/SettingsGeneralScreen';
import Settings from '../screens/settings/SettingsScreen';
import TrackerSettings from '../screens/settings/SettingsTrackerScreen';
import ReaderSettings from '../screens/settings/SettingsReaderScreen/SettingsReaderScreen';
import BackupSettings from '../screens/settings/SettingsBackupScreen';
import AdvancedSettings from '../screens/settings/SettingsAdvancedScreen';
import GeneralSettings from '../screens/settings/SettingsGeneralScreen';
import DownloadQueue from '../screens/more/DownloadQueueScreen';
import Downloads from '../screens/more/DownloadsScreen';
import AppearanceSettings from '../screens/setting/SettingsAppearanceScreen';
import AppearanceSettings from '../screens/settings/SettingsAppearanceScreen';
const Stack = createStackNavigator();
+1
View File
@@ -51,6 +51,7 @@ export const resumeDownloads = chapters => async dispatch => {
await downloadChapter(
chapters[i].sourceId,
chapters[i].novelUrl,
chapters[i].novelId,
chapters[i].chapterUrl,
chapters[i].chapterId,
);
+2 -10
View File
@@ -1,19 +1,14 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { ThemeType } from '../../theme/types';
import type { RootState, AppDispatch } from '../store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useTheme = () =>
useAppSelector(
(state: RootState) => state.settingsReducerV2.appearance.theme,
);
export const useThemeV1 = () =>
useAppSelector(
(state: RootState) => state.settingsReducer.theme,
) as ThemeTypeV1;
) as ThemeType;
export const useSettingsV1 = () =>
useAppSelector((state: RootState) => state.settingsReducer);
@@ -28,9 +23,6 @@ export const useAppearanceSettings = () =>
useAppSelector((state: RootState) => state.settingsReducerV2.appearance);
export const useReaderSettings = () =>
useAppSelector((state: RootState) => state.settingsReducerV2.reader);
export const useReaderSettingsV1 = () =>
useAppSelector((state: RootState) => state.settingsReducer.reader);
export const useLibrarySettings = () =>
+41 -15
View File
@@ -269,8 +269,18 @@ export const markChapterUnreadAction =
});
};
/**
*
* @param {number} sourceId
* @param {string} novelUrl
* @param {number} novelId
* @param {string} chapterUrl
* @param {string} chapterName
* @param {number} chapterId
* @returns
*/
export const downloadChapterAction =
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) =>
(sourceId, novelUrl, novelId, chapterUrl, chapterName, chapterId) =>
async dispatch => {
// dispatch({
// type: CHAPTER_DOWNLOADING,
@@ -281,7 +291,7 @@ export const downloadChapterAction =
payload: [{ chapterId, chapterName }],
});
await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId);
await downloadChapter(sourceId, novelUrl, novelId, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
@@ -291,6 +301,13 @@ export const downloadChapterAction =
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
/**
*
* @param {number} sourceId
* @param {string} novelUrl
* @param {import("./../../database/types").ChapterItem[]} chaps
* @returns
*/
export const downloadAllChaptersAction =
(sourceId, novelUrl, chaps) => async (dispatch, getState) => {
try {
@@ -356,6 +373,7 @@ export const downloadAllChaptersAction =
await downloadChapter(
sourceId,
novelUrl,
chapters[i].novelId,
chapters[i].chapterUrl,
chapters[i].chapterId,
);
@@ -400,8 +418,8 @@ export const downloadAllChaptersAction =
};
export const deleteChapterAction =
(chapterId, chapterName) => async dispatch => {
await deleteChapter(chapterId);
(sourceId, novelId, chapterId, chapterName) => async dispatch => {
await deleteChapter(sourceId, novelId, chapterId);
dispatch({
type: CHAPTER_DELETED,
@@ -411,18 +429,26 @@ export const deleteChapterAction =
showToast(`Deleted ${chapterName}`);
};
export const deleteAllChaptersAction = chapters => async dispatch => {
await chapters.map(chapter => {
deleteChapter(chapter.chapterId);
/**
*
* @param {number} sourceId
* @param {import("../../database/types").ChapterItem[]} chapters
* @returns
*/
export const deleteAllChaptersAction =
(sourceId, chapters) => async dispatch => {
await Promise.all(
chapters.map(async chapter => {
await deleteChapter(sourceId, chapter.novelId, chapter.chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapter.chapterId,
});
});
showToast('Chapters deleted');
};
dispatch({
type: CHAPTER_DELETED,
payload: chapter.chapterId,
});
}),
);
showToast('Chapters deleted');
};
export const updateNovelAction =
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch, getState) => {
+15 -6
View File
@@ -23,7 +23,9 @@ import {
yotsubaTheme,
takoLightTheme,
tealTheme,
} from '../../theme/v1/theme/theme';
sapphireDuskTheme,
chocolateStrawberriesTheme,
} from '../../theme/theme.ts';
const themes = {
1: lightTheme,
@@ -40,6 +42,8 @@ const themes = {
12: yotsubaTheme,
13: takoLightTheme,
14: tealTheme,
15: sapphireDuskTheme,
16: chocolateStrawberriesTheme,
};
/**
@@ -51,12 +55,13 @@ const themes = {
* 3 -> No title
*/
const initialState = {
export const initialState = {
/**
* General settings
*/
incognitoMode: false,
disableHapticFeedback: false,
/**
* Appearence settings
@@ -107,18 +112,22 @@ const initialState = {
*/
fullScreenMode: true,
swipeGestures: true,
swipeGestures: false,
showScrollPercentage: true,
useWebViewForChapter: false,
wvUseNewSwipes: false,
wvShowSwipeMargins: true,
wvUseVolumeButtons: false,
showBatteryAndTime: false,
autoScroll: false,
autoScrollInterval: 10,
textSelectable: false,
autoScrollOffset: null,
verticalSeekbar: true,
removeExtraParagraphSpacing: false,
reader: {
theme: 1,
textColor: 'rgba(255,255,255,0.7)',
theme: '#292832',
textColor: '#CCCCCC',
textSize: 16,
textAlign: 'left',
padding: 5,
+40 -96
View File
@@ -1,69 +1,20 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ThemeType } from '../../theme/types';
import { defaultDarkTheme } from '../../theme/dark';
import { BrowseSettingsMap } from './types';
export enum DisplayModes {
Compact = 'compact',
Comfortable = 'comfortable',
NoTitle = 'no title',
List = 'list',
export interface ReaderTheme {
backgroundColor: string;
textColor: string;
}
export enum TextAlignments {
Left = 'left',
Center = 'center',
Justify = 'justfy',
Right = 'right',
}
// interface ReaderTheme {
// backgroundColor: string;
// textColor: string;
// }
interface SettingsState {
appearance: {
theme: ThemeType;
displayMode: DisplayModes;
novelsPerRowPotrait: number;
novelsPerRowLandscape: number;
showLabelsInNav: boolean;
showHistoryTab: boolean;
showUpdatesTab: boolean;
};
appearance: {};
reader: {
fontFamily: string | null;
fontSize: number;
lineHeight: number;
textAlign: TextAlignments;
backgroundColor: string;
textColor: string;
paddingHorizontal: number;
customCSS: string | null;
fullScreenMode: boolean;
useWebViewReader: boolean;
showProgressPercentage: boolean;
useSwipeGestures: boolean;
};
library: {
filters: string[];
sort: string;
showDownloadsBadge: boolean;
showUnreadBadge: boolean;
updateLibraryOnLaunch: boolean;
};
updates: {
lastUpdateTime: Date | null;
onlyUpdateOngoingNovels: boolean;
updateNovelMetadata: boolean;
showLastUpdateTime: boolean;
};
app: {
incognitoMode: boolean;
downloadOnlyMode: boolean;
customThemes: ReaderTheme[];
};
library: {};
updates: {};
app: {};
browse: {
onlyShowPinnedSources: boolean;
showMyAnimeList: boolean;
@@ -72,46 +23,13 @@ interface SettingsState {
}
const initialState: SettingsState = {
appearance: {
theme: defaultDarkTheme,
displayMode: DisplayModes.Compact,
novelsPerRowPotrait: 3,
novelsPerRowLandscape: 6,
showLabelsInNav: true,
showHistoryTab: true,
showUpdatesTab: true,
},
appearance: {},
reader: {
fontFamily: '',
fontSize: 16,
lineHeight: 1.5,
textAlign: TextAlignments.Left,
backgroundColor: '#000000',
textColor: 'rgba(255,255,255,0.7)',
paddingHorizontal: 5,
customCSS: null,
fullScreenMode: true,
useWebViewReader: false,
showProgressPercentage: false,
useSwipeGestures: false,
},
library: {
filters: [],
sort: 'novelId DESC',
showDownloadsBadge: true,
showUnreadBadge: true,
updateLibraryOnLaunch: false,
},
updates: {
lastUpdateTime: null,
onlyUpdateOngoingNovels: false,
updateNovelMetadata: false,
showLastUpdateTime: false,
},
app: {
incognitoMode: false,
downloadOnlyMode: false,
customThemes: [],
},
library: {},
updates: {},
app: {},
browse: {
onlyShowPinnedSources: false,
showMyAnimeList: true,
@@ -133,9 +51,35 @@ export const settingsSlice = createSlice({
state.browse[action.payload.key] = action.payload.value;
},
saveCustomReaderTheme: (
state,
action: PayloadAction<{ theme: ReaderTheme }>,
) => {
state.reader.customThemes = [
...(state.reader.customThemes || []),
action.payload.theme,
];
},
deleteCustomReaderTheme: (
state,
action: PayloadAction<{ theme: ReaderTheme }>,
) => {
const customTheme = action.payload.theme;
state.reader.customThemes = state.reader.customThemes.filter(
theme =>
!(
theme.backgroundColor === customTheme.backgroundColor &&
theme.textColor === customTheme.textColor
),
);
},
},
});
export const { setBrowseSettings } = settingsSlice.actions;
export const {
setBrowseSettings,
saveCustomReaderTheme,
deleteCustomReaderTheme,
} = settingsSlice.actions;
export default settingsSlice.reducer;
-32
View File
@@ -1,35 +1,3 @@
// export interface SettingsMap {
// 'app.downloadOnlyMode': 'string';
// 'app.incognitoMode': 'string';
// 'appearance.displayMode': 'string';
// 'appearance.novelsPerRowLandscape': 'string';
// 'appearance.novelsPerRowPotrait': 'string';
// 'appearance.showHistoryTab': 'string';
// 'appearance.showLabelsInNav': 'string';
// 'appearance.showUpdatesTab': 'string';
// 'appearance.theme': 'string';
// 'library.showDownloadsBadge': 'string';
// 'library.showUnreadBadge': 'string';
// 'library.sort': 'string';
// 'library.updateLibraryOnLaunch': 'string';
// 'reader.backgroundColor': 'string';
// 'reader.customCSS': 'string';
// 'reader.fontFamily': 'string';
// 'reader.fontSize': 'string';
// 'reader.fullScreenMode': 'string';
// 'reader.lineHeight': 'string';
// 'reader.paddingHorizontal': 'string';
// 'reader.showProgressPercentage': 'string';
// 'reader.textAlign': 'string';
// 'reader.textColor': 'string';
// 'reader.useSwipeGestures': 'string';
// 'reader.useWebViewReader': 'string';
// 'updates.lastUpdateTime': 'string';
// 'updates.onlyUpdateOngoingNovels': 'string';
// 'updates.showLastUpdateTime': 'string';
// 'updates.updateNovelMetadata': 'string';
// }
export interface BrowseSettingsMap {
onlyShowPinnedSources: 'string';
searchAllSources: 'string';
-57
View File
@@ -1,57 +0,0 @@
import {
ENABLE_DISCOVER,
FILTER_LANGUAGE,
GET_SOURCES,
PIN_SOURCES,
SEARCH_SOURCES,
} from './source.types';
import { showToast } from '../../hooks/showToast';
import sources from '../../sources/sources.json';
export const getSourcesAction = () => async dispatch => {
try {
/**
* Sort sources aplhabetically
*/
let sortedSources = sources.sort((a, b) =>
a.sourceName.localeCompare(b.sourceName),
);
dispatch({
type: GET_SOURCES,
payload: sortedSources,
});
} catch (error) {
showToast(error.message);
}
};
export const searchSourcesAction = searchText => async dispatch => {
const res = searchText.toLowerCase();
dispatch({
type: SEARCH_SOURCES,
payload: res,
});
};
export const pinSourceAction = sourceId => async dispatch => {
dispatch({
type: PIN_SOURCES,
payload: sourceId,
});
};
export const filterLanguage = language => async dispatch => {
dispatch({
type: FILTER_LANGUAGE,
payload: language,
});
};
export const enableDiscover = key => async dispatch => {
dispatch({
type: ENABLE_DISCOVER,
payload: key,
});
};
@@ -0,0 +1,182 @@
import React, { useCallback, useRef } from 'react';
import { useNavigation } from '@react-navigation/native';
import * as WebBrowser from 'expo-web-browser';
import { FAB, Portal } from 'react-native-paper';
import Bottomsheet from 'rn-sliding-up-panel';
import {
Container,
LoadingScreenV2,
LoadingMoreIndicator,
ErrorScreenV2,
SearchbarV2,
} from '@components/index';
import NovelList from '@components/NovelList';
import NovelCover from '@components/NovelCover';
import FilterBottomSheet from './components/BottomSheet';
import { useSearch } from '../../hooks';
import { useTheme } from '../../redux/hooks';
import { useBrowseSource, useSearchSource } from './useBrowseSource';
import { useLibrary } from '@hooks/reduxHooks';
import { LibraryNovelInfo } from '../../database/types';
import { SourceNovelItem } from '../../sources/types';
import { getString } from '@strings/translations';
import { StyleSheet } from 'react-native';
interface BrowseSourceScreenProps {
route: {
params: {
sourceId: number;
sourceName: string;
url: string;
showLatestNovels?: boolean;
};
};
}
const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
const theme = useTheme();
const { navigate, goBack } = useNavigation();
const {
sourceId,
sourceName,
url: sourceUrl,
showLatestNovels,
} = route.params;
const {
isLoading,
novels,
hasNextPage,
fetchNextPage,
error,
filterValues,
setFilters,
clearFilters,
} = useBrowseSource(sourceId, showLatestNovels);
const {
isSearching,
searchResults,
searchSource,
clearSearchResults,
searchError,
} = useSearchSource(sourceId);
const novelList = searchResults.length > 0 ? searchResults : novels;
const errorMessage = error || searchError;
const { searchText, setSearchText, clearSearchbar } = useSearch();
const onChangeText = (text: string) => setSearchText(text);
const onSubmitEditing = () => searchSource(searchText);
const handleClearSearchbar = () => {
clearSearchbar();
clearSearchResults();
};
const handleOpenWebView = async () => {
WebBrowser.openBrowserAsync(sourceUrl);
};
const library = useLibrary();
const novelInLibrary = (novelUrl: string) =>
library?.some(
(novel: LibraryNovelInfo) =>
novel.novelUrl === novelUrl && novel.sourceId === sourceId,
);
const navigateToNovel = useCallback(
(item: SourceNovelItem) =>
navigate(
'Novel' as never,
{
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId,
} as never,
),
[sourceId],
);
const filterSheetRef = useRef<Bottomsheet | null>(null);
return (
<Container>
<SearchbarV2
searchText={searchText}
leftIcon="magnify"
placeholder={`${getString('common.search')} ${sourceName}`}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={handleClearSearchbar}
handleBackAction={goBack}
rightIcons={[{ iconName: 'earth', onPress: handleOpenWebView }]}
theme={theme}
/>
{isLoading || isSearching ? (
<LoadingScreenV2 theme={theme} />
) : errorMessage || novelList.length === 0 ? (
<ErrorScreenV2
error={errorMessage || getString('sourceScreen.noResultsFound')}
theme={theme}
/>
) : (
<NovelList
data={novelList}
renderItem={({ item }) => (
<NovelCover
item={item}
theme={theme}
libraryStatus={novelInLibrary(item.novelUrl)}
onPress={() => navigateToNovel(item)}
/>
)}
onEndReached={() => {
if (hasNextPage && !searchText) {
fetchNextPage();
}
}}
ListFooterComponent={
hasNextPage && !searchText && <LoadingMoreIndicator theme={theme} />
}
/>
)}
{!showLatestNovels && filterValues?.length ? (
<>
<FAB
icon={'filter-variant'}
style={[styles.filterFab, { backgroundColor: theme.colorAccent }]}
label={'Filter'}
uppercase={false}
color={theme.colorButtonText}
onPress={() => filterSheetRef?.current?.show()}
/>
<Portal>
<FilterBottomSheet
filterSheetRef={filterSheetRef}
filtersValues={filterValues}
setFilters={setFilters}
clearFilters={clearFilters}
/>
</Portal>
</>
) : null}
</Container>
);
};
export default BrowseSourceScreen;
const styles = StyleSheet.create({
filterFab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 16,
},
});
@@ -0,0 +1,226 @@
import React, { LegacyRef, useState } from 'react';
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
import Bottomsheet from 'rn-sliding-up-panel';
import { useTheme } from '@redux/hooks';
import {
FilterInputs,
SelectedFilter,
SourceFilter,
} from '../../../sources/types/filterTypes';
import { FlatList } from 'react-native-gesture-handler';
import { Button, IconButtonV2 } from '@components/index';
import { ButtonVariation } from '@components/Button/Button';
import { coverPlaceholderColor } from '../../../theme/colors';
import { Checkbox } from '@components/Checkbox/Checkbox';
import { defaultTo } from 'lodash';
import { Picker } from '@react-native-picker/picker';
import useBoolean from '@hooks/useBoolean';
const insertOrRemoveIntoArray = (array: string[], val: string): string[] =>
array.indexOf(val) > -1 ? array.filter(ele => ele !== val) : [...array, val];
interface FilterItemProps {
filter: SourceFilter;
selectedFilters: SelectedFilter | undefined;
setSelectedFilters: React.Dispatch<
React.SetStateAction<SelectedFilter | undefined>
>;
}
const FilterItem: React.FC<FilterItemProps> = ({
filter,
selectedFilters,
setSelectedFilters,
}) => {
const theme = useTheme();
const { value: isVisible, toggle: toggleCard } = useBoolean();
if (filter.inputType === FilterInputs.Picker) {
return (
<View style={styles.pickerContainer}>
<Text style={[styles.pickerLabel, { color: theme.textColorSecondary }]}>
{filter.label}
</Text>
<Picker
style={styles.picker}
mode="dropdown"
selectedValue={selectedFilters?.[filter.key] as string}
dropdownIconRippleColor={theme.colorAccent}
onValueChange={(itemValue: string) =>
setSelectedFilters(prevFilters => ({
...prevFilters,
[filter.key]: itemValue,
}))
}
placeholder={filter.label}
>
{filter.values.map(val => {
return <Picker.Item label={val.label} value={val.value} />;
})}
</Picker>
</View>
);
}
if (filter.inputType === FilterInputs.Checkbox) {
const isChecked = (val: string) =>
(selectedFilters?.[filter.key] as string[])?.includes(val);
return (
<View>
<Pressable
style={styles.checkboxHeader}
onPress={toggleCard}
android_ripple={{ color: theme.rippleColor }}
>
<Text
style={[styles.filterLabel, { color: theme.textColorSecondary }]}
>
{filter.label}
</Text>
<IconButtonV2
name={isVisible ? 'chevron-up' : 'chevron-down'}
onPress={toggleCard}
theme={theme}
/>
</Pressable>
{isVisible
? filter.values.map(val => {
return (
<Checkbox
label={val.label}
theme={theme}
status={isChecked(val.value)}
onPress={() =>
setSelectedFilters(prevFilters => ({
...prevFilters,
[filter.key]: insertOrRemoveIntoArray(
defaultTo(prevFilters?.[filter.key] as string[], []),
val.value,
),
}))
}
/>
);
})
: null}
</View>
);
}
return <></>;
};
interface BottomSheetProps {
filtersValues: SourceFilter[] | undefined;
filterSheetRef: LegacyRef<Bottomsheet> | null;
setFilters: React.Dispatch<React.SetStateAction<SelectedFilter | undefined>>;
clearFilters: () => void;
}
const BottomSheet: React.FC<BottomSheetProps> = ({
filterSheetRef,
filtersValues,
clearFilters,
setFilters,
}) => {
const theme = useTheme();
const [animatedValue] = useState(new Animated.Value(0));
const [selectedFilters, setSelectedFilters] = useState<
SelectedFilter | undefined
>();
return (
<Bottomsheet
animatedValue={animatedValue}
ref={filterSheetRef}
draggableRange={{ top: 600, bottom: 0 }}
snappingPoints={[0, 400, 600]}
showBackdrop={true}
backdropOpacity={0.3}
height={600}
>
<View style={[styles.container, { backgroundColor: theme.colorPrimary }]}>
<View style={styles.buttonContainer}>
<Button
title={'Reset'}
variation={ButtonVariation.CLEAR}
onPress={() => {
setSelectedFilters(undefined);
clearFilters();
}}
theme={theme}
/>
<Button
title={'Filter'}
textColor={theme.colorButtonText}
onPress={() => {
setFilters(selectedFilters);
filterSheetRef?.current?.hide();
}}
theme={theme}
/>
</View>
<FlatList
contentContainerStyle={styles.filterContainer}
data={filtersValues}
keyExtractor={item => item.key}
renderItem={({ item }: { item: SourceFilter }) => (
<FilterItem
filter={item}
selectedFilters={selectedFilters}
setSelectedFilters={setSelectedFilters}
/>
)}
/>
</View>
</Bottomsheet>
);
};
export default BottomSheet;
const styles = StyleSheet.create({
container: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderBottomColor: coverPlaceholderColor,
borderBottomWidth: 1,
},
filterContainer: {
paddingVertical: 16,
},
filterLabel: {
paddingHorizontal: 16,
marginVertical: 16,
},
pickerLabel: {
paddingHorizontal: 16,
},
pickerContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
picker: {
width: 200,
},
checkboxHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: 16,
},
});
@@ -0,0 +1,116 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { SelectedFilter, SourceFilter } from 'src/sources/types/filterTypes';
import { sourceManager } from '../../sources/sourceManager';
import { SourceNovelItem } from '../../sources/types';
export const useBrowseSource = (
sourceId: number,
showLatestNovels?: boolean,
) => {
const [isLoading, setIsLoading] = useState(true);
const [novels, setNovels] = useState<SourceNovelItem[]>([]);
const [error, setError] = useState<string>();
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const [filterValues, setFilterValues] = useState<SourceFilter[]>();
const [selectedFilters, setSelectedFilters] = useState<
SelectedFilter | undefined
>();
const isScreenMounted = useRef(true);
const hasNextPage = currentPage <= totalPages;
const fetchNovels = useCallback(
async (page: number, filters?: SelectedFilter) => {
if (isScreenMounted.current === true) {
try {
const source = sourceManager(sourceId);
const res = await source.popularNovels(page, {
showLatestNovels,
filters,
});
setNovels(prevState =>
page === 1 ? res.novels : [...prevState, ...res.novels],
);
setTotalPages(res.totalPages);
setFilterValues(source.filters);
} catch (err: unknown) {
setError(`${err}`);
} finally {
setIsLoading(false);
}
}
},
[sourceId],
);
const fetchNextPage = () => {
hasNextPage && setCurrentPage(prevState => prevState + 1);
};
useEffect(() => {
fetchNovels(currentPage, selectedFilters);
}, [fetchNovels, currentPage]);
/**
* On screen unmount
*/
useEffect(() => {
return () => {
isScreenMounted.current = false;
};
}, []);
const clearFilters = useCallback(() => setSelectedFilters(undefined), []);
const setFilters = (filters: SelectedFilter) => {
setIsLoading(true);
setCurrentPage(1);
fetchNovels(1, filters);
setSelectedFilters(filters);
};
return {
isLoading,
novels,
hasNextPage,
fetchNextPage,
error,
filterValues,
setFilters,
clearFilters,
};
};
export const useSearchSource = (sourceId: number) => {
const [isSearching, setIsSearching] = useState(false);
const [searchResults, setSearchResults] = useState<SourceNovelItem[]>([]);
const [searchError, setSearchError] = useState<string>();
const searchSource = useCallback(
async (searchTerm: string) => {
try {
setIsSearching(true);
const res = await sourceManager(sourceId).searchNovels(searchTerm);
setSearchResults(res);
} catch (err) {
setSearchError(`${err}`);
} finally {
setIsSearching(false);
}
},
[sourceId],
);
const clearSearchResults = useCallback(() => setSearchResults([]), []);
return {
isSearching,
searchResults,
searchSource,
clearSearchResults,
searchError,
};
};
@@ -0,0 +1,66 @@
import React from 'react';
import { ProgressBar } from 'react-native-paper';
import { EmptyView, SearchbarV2 } from '@components/index';
import GlobalSearchResultsList from './components/GlobalSearchResultsList';
import useSearch from '@hooks/useSearch';
import { useTheme } from '@redux/hooks';
import { getString } from '@strings/translations';
import { useGlobalSearch } from './hooks/useGlobalSearch';
interface Props {
route?: {
params?: {
searchText?: string;
};
};
}
const GlobalSearchScreen = (props: Props) => {
const theme = useTheme();
const { searchText, setSearchText, clearSearchbar } = useSearch(
props?.route?.params?.searchText,
);
const onChangeText = (text: string) => setSearchText(text);
const onSubmitEditing = () => globalSearch(searchText);
const { searchResults, globalSearch, searchAllSources, progress } =
useGlobalSearch({
defaultSearchText: searchText,
});
return (
<>
<SearchbarV2
searchText={searchText}
placeholder={getString('browseScreen.globalSearch')}
leftIcon="magnify"
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
theme={theme}
/>
{progress ? (
<ProgressBar color={theme.colorAccent} progress={progress} />
) : null}
<GlobalSearchResultsList
searchResults={searchResults}
ListEmptyComponent={
<EmptyView
icon="__φ(..)"
description={`${getString('globalSearch.searchIn')} ${getString(
searchAllSources
? 'globalSearch.allSources'
: 'globalSearch.pinnedSources',
)}`}
theme={theme}
/>
}
/>
</>
);
};
export default GlobalSearchScreen;
@@ -0,0 +1,78 @@
import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useMemo } from 'react';
import FastImage from 'react-native-fast-image';
import { coverPlaceholderColor } from '../../../theme/colors';
import { SourceNovelItem } from '../../../sources/types';
import { ThemeType } from '../../../theme/types';
interface Props {
novel: SourceNovelItem;
navigateToNovel: (novel: SourceNovelItem) => void;
theme: ThemeType;
}
const GlobalSearchNovelItem: React.FC<Props> = ({
novel,
navigateToNovel,
theme,
}) => {
const novelItemDimensions = useMemo(
() => ({
width: Dimensions.get('window').width / 3 - 16,
height: ((Dimensions.get('window').width / 3 - 16) * 4) / 3,
}),
[],
);
return (
<View style={styles.novelItem}>
<Pressable
style={styles.pressable}
android_ripple={{ color: theme.rippleColor }}
onPress={() => navigateToNovel(novel)}
>
<FastImage
source={{ uri: novel.novelCover }}
style={[styles.novelCover, { ...novelItemDimensions }]}
/>
<Text
style={[
{ color: theme.textColorPrimary, width: novelItemDimensions.width },
styles.novelName,
]}
numberOfLines={2}
>
{novel.novelName}
</Text>
</Pressable>
</View>
);
};
export default GlobalSearchNovelItem;
const styles = StyleSheet.create({
novelItem: {
flex: 1,
borderRadius: 6,
overflow: 'hidden',
},
pressable: {
padding: 4,
flex: 1,
},
novelName: {
paddingHorizontal: 4,
paddingVertical: 8,
flexWrap: 'wrap',
fontSize: 12,
fontWeight: 'bold',
},
novelCover: {
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
});
@@ -0,0 +1,165 @@
import {
ActivityIndicator,
FlatList,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import React, { useCallback, useMemo } from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { getString } from '@strings/translations';
import { useTheme } from '@redux/hooks';
import { GlobalSearchResult } from '../hooks/useGlobalSearch';
import GlobalSearchNovelItem from './GlobalSearchNovelItem';
interface GlobalSearchResultsListProps {
searchResults: GlobalSearchResult[];
ListEmptyComponent?: JSX.Element;
}
const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
searchResults,
ListEmptyComponent,
}) => {
const theme = useTheme();
const navigation = useNavigation<StackNavigationProp<any>>();
const keyExtractor = useCallback(item => item.source.sourceId.toString(), []);
const errorColor = useMemo(
() => (theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5'),
[],
);
const navigateToNovel = useCallback(
item => navigation.push('Novel', item),
[],
);
return (
<FlatList<GlobalSearchResult>
keyExtractor={keyExtractor}
data={searchResults}
contentContainerStyle={styles.resultList}
renderItem={({ item }) => (
<>
<View>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.sourceHeader}
onPress={() =>
navigation.navigate('SourceScreen', {
sourceId: item.source.sourceId,
sourceName: item.source.sourceName,
url: item.source.url,
})
}
>
<View>
<Text
style={[styles.sourceName, { color: theme.textColorPrimary }]}
>
{item.source.sourceName}
</Text>
<Text
style={[styles.language, { color: theme.textColorSecondary }]}
>
{item.source.lang}
</Text>
</View>
<MaterialCommunityIcons
name="arrow-right"
size={24}
color={theme.textColorPrimary}
/>
</Pressable>
{item.isLoading ? (
<ActivityIndicator
color={theme.colorAccent}
style={styles.loading}
/>
) : item.error ? (
<Text style={[styles.error, { color: errorColor }]}>
{item.error}
</Text>
) : (
<FlatList
horizontal
contentContainerStyle={styles.novelsContainer}
keyExtractor={novelItem =>
novelItem.novelUrl + novelItem.sourceId
}
data={item.novels}
ListEmptyComponent={
<Text
style={[
styles.listEmpty,
{ color: theme.textColorSecondary },
]}
>
{getString('sourceScreen.noResultsFound')}
</Text>
}
renderItem={({ item: novelItem }) => (
<GlobalSearchNovelItem
novel={novelItem}
navigateToNovel={navigateToNovel}
theme={theme}
/>
)}
/>
)}
</View>
</>
)}
ListEmptyComponent={ListEmptyComponent}
/>
);
};
export default GlobalSearchResultsList;
const styles = StyleSheet.create({
resultList: {
flexGrow: 1,
paddingTop: 8,
paddingBottom: 60,
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 16,
},
sourceHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: 8,
},
sourceName: {
marginTop: 8,
marginBottom: 4,
paddingHorizontal: 16,
},
language: {
fontSize: 12,
marginBottom: 8,
paddingHorizontal: 16,
},
error: {
padding: 16,
marginBottom: 16,
},
novelsContainer: {
padding: 8,
},
listEmpty: {
paddingHorizontal: 8,
marginBottom: 16,
},
});
@@ -0,0 +1,108 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useBrowseSettings, useSourcesReducer } from '@redux/hooks';
import { Source, SourceNovelItem } from '../../../sources/types';
import { sourceManager } from '../../../sources/sourceManager';
interface Props {
defaultSearchText?: string;
}
export interface GlobalSearchResult {
isLoading: boolean;
source: Source;
novels: SourceNovelItem[];
error?: string | null;
}
export const useGlobalSearch = ({ defaultSearchText }: Props) => {
const isMounted = useRef(true);
const { searchAllSources = false } = useBrowseSettings();
const { allSources, pinnedSourceIds = [] } = useSourcesReducer();
const isPinned = useCallback(
(sourceId: number) => pinnedSourceIds.indexOf(sourceId) > -1,
[pinnedSourceIds],
);
const pinnedSources = useMemo(
() => allSources.filter(source => isPinned(source.sourceId)),
[allSources, isPinned],
);
const [searchResults, setSearchResults] = useState<GlobalSearchResult[]>([]);
const [progress, setProgress] = useState(0);
const globalSearch = (searchText: string) => {
const sources = searchAllSources ? allSources : pinnedSources;
const defaultResult: GlobalSearchResult[] = sources.map(source => ({
isLoading: true,
source,
novels: [],
error: null,
}));
setSearchResults(defaultResult);
sources.forEach(async source => {
if (isMounted.current) {
try {
const res = await sourceManager(source.sourceId).searchNovels(
searchText,
);
setSearchResults(prevState =>
prevState.map(prevResult =>
prevResult.source.sourceId === source.sourceId
? { ...prevResult, novels: res, isLoading: false }
: { ...prevResult },
),
);
setSearchResults(prevState =>
prevState.sort(
(
{ novels: a, source: { sourceName: aName } },
{ novels: b, source: { sourceName: bName } },
) => {
if (!a.length) {
return 1;
}
if (!b.length) {
return -1;
}
return aName.localeCompare(bName);
},
),
);
} catch (error) {
setSearchResults(prevState =>
prevState.map(prevResult =>
prevResult.source.sourceId === source.sourceId
? {
...prevResult,
novels: [],
isLoading: false,
error: error.message,
}
: { ...prevResult },
),
);
} finally {
setProgress(prevState => prevState + 1 / sources.length);
}
}
});
};
useEffect(() => {
if (defaultSearchText) {
globalSearch(defaultSearchText);
}
}, []);
return { searchResults, globalSearch, searchAllSources, progress };
};
+35 -29
View File
@@ -12,7 +12,7 @@ import {
useAppDispatch,
useBrowseSettings,
useSourcesReducer,
useThemeV1,
useTheme,
} from '../../redux/hooks';
import { useNavigation } from '@react-navigation/native';
import { useSearch } from '../../hooks';
@@ -23,7 +23,7 @@ import MalCard from './discover/MalCard/MalCard';
const BrowseScreen = () => {
const { navigate } = useNavigation();
const theme = useThemeV1();
const theme = useTheme();
const dispatch = useAppDispatch();
const { searchText, setSearchText, clearSearchbar } = useSearch();
@@ -59,23 +59,27 @@ const BrowseScreen = () => {
? allSources.filter(source => source.sourceId === lastUsed)
: [];
const navigateToSource = useCallback((source: Source) => {
navigate(
'SourceScreen' as never,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
url: source.url,
} as never,
);
dispatch(setLastUsedSource({ sourceId: source.sourceId }));
}, []);
const navigateToSource = useCallback(
(source: Source, showLatestNovels?: boolean) => {
navigate(
'SourceScreen' as never,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
url: source.url,
showLatestNovels,
} as never,
);
dispatch(setLastUsedSource({ sourceId: source.sourceId }));
},
[],
);
const searchbarActions = useMemo(
() => [
{
iconName: 'book-search',
onPress: () => navigate('GlobalSearch' as never),
onPress: () => navigate('GlobalSearchScreen' as never),
},
{
iconName: 'swap-vertical-variant',
@@ -100,27 +104,14 @@ const BrowseScreen = () => {
theme={theme}
rightIcons={searchbarActions}
/>
{allSources.length === 0 ? (
{languageFilters.length === 0 ? (
<EmptyView
icon="(・Д・。"
description={getString('browseScreen.listEmpty')}
theme={theme}
/>
) : (
) : allSources.length === 0 ? null : (
<>
{showMyAnimeList && (
<>
<Text
style={[
styles.sectionHeader,
{ color: theme.textColorSecondary },
]}
>
{getString('browseScreen.discover')}
</Text>
{showMyAnimeList && <MalCard theme={theme} />}
</>
)}
<SectionList
sections={[
{
@@ -133,6 +124,21 @@ const BrowseScreen = () => {
data: onlyShowPinnedSources ? [] : allSources,
},
]}
ListHeaderComponent={
showMyAnimeList ? (
<>
<Text
style={[
styles.sectionHeader,
{ color: theme.textColorSecondary },
]}
>
{getString('browseScreen.discover')}
</Text>
{showMyAnimeList && <MalCard theme={theme} />}
</>
) : null
}
keyExtractor={(_, index) => index.toString()}
renderSectionHeader={({ section: { header, data } }) =>
data.length > 0 ? (
+2 -2
View File
@@ -6,7 +6,7 @@ import {
useAppDispatch,
useBrowseSettings,
useSourcesReducer,
useThemeV1,
useTheme,
} from '../../redux/hooks';
import { useNavigation } from '@react-navigation/native';
import { getString } from '../../../strings/translations';
@@ -15,7 +15,7 @@ import { toggleLanguageFilter } from '../../redux/source/sourcesSlice';
import { setBrowseSettings } from '../../redux/settings/settingsSlice';
const BrowseSettings = () => {
const theme = useThemeV1();
const theme = useTheme();
const { goBack } = useNavigation();
const dispatch = useAppDispatch();
@@ -6,14 +6,14 @@ import { ButtonVariation } from '../../../../components/Button/Button';
import { Source } from '../../../../sources/types';
import { coverPlaceholderColor } from '../../../../theme/colors';
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
import { ThemeType } from '../../../../theme/types';
interface Props {
source: Source;
isPinned: boolean;
theme: ThemeTypeV1;
theme: ThemeType;
onTogglePinSource: (sourceId: number) => void;
navigateToSource: (source: Source) => void;
navigateToSource: (source: Source, showLatestNovels?: boolean) => void;
}
const SourceCard: React.FC<Props> = ({
@@ -41,12 +41,11 @@ const SourceCard: React.FC<Props> = ({
</View>
<View style={styles.flexRow}>
<Button
title={getString('browse')}
title={getString('browseScreen.latest')}
variation={ButtonVariation.CLEAR}
textColor={theme.colorAccent}
onPress={() => navigateToSource(source)}
onPress={() => navigateToSource(source, true)}
theme={theme}
margin={0}
/>
<IconButtonV2
name={isPinned ? 'pin' : 'pin-outline'}
@@ -5,10 +5,10 @@ import { getString } from '../../../../../strings/translations';
import { Button } from '../../../../components';
import { ButtonVariation } from '../../../../components/Button/Button';
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
import { ThemeType } from '../../../../theme/types';
interface Props {
theme: ThemeTypeV1;
theme: ThemeType;
}
const MalCard: React.FC<Props> = ({ theme }) => {
@@ -36,7 +36,6 @@ const MalCard: React.FC<Props> = ({ theme }) => {
textColor={theme.colorAccent}
onPress={() => navigate('BrowseMal' as never)}
theme={theme}
margin={0}
/>
</View>
</Pressable>
@@ -2,12 +2,12 @@ import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
import { ThemeType } from '../../../../theme/types';
interface Props {
novel: { novelName: string; novelCover: string; score: string; info: string };
onPress: () => void;
theme: ThemeTypeV1;
theme: ThemeType;
}
const MalNovelCard: React.FC<Props> = ({ novel, onPress, theme }) => {
@@ -34,18 +34,22 @@ const MalNovelCard: React.FC<Props> = ({ novel, onPress, theme }) => {
{novel.score}
</Text>
</Text>
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
Type:{' '}
<Text style={{ color: theme.textColorSecondary }}>
{novel.info[1]}
{novel?.info?.[1] ? (
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
Type:{' '}
<Text style={{ color: theme.textColorSecondary }}>
{novel.info[1]}
</Text>
</Text>
</Text>
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
Published:{' '}
<Text style={{ color: theme.textColorSecondary }}>
{novel.info[2]}
) : null}
{novel?.info?.[2] ? (
<Text style={[styles.small, { color: theme.textColorPrimary }]}>
Published:{' '}
<Text style={{ color: theme.textColorSecondary }}>
{novel.info[2]}
</Text>
</Text>
</Text>
) : null}
</View>
</Pressable>
</View>
@@ -73,7 +77,7 @@ const styles = StyleSheet.create({
},
infoContainer: {
flex: 1,
padding: 8,
padding: 16,
},
title: {
marginBottom: 4,
+1 -1
View File
@@ -69,7 +69,7 @@ const BrowseMalScreen = ({ navigation, route }) => {
novel={item}
theme={theme}
onPress={() =>
navigation.navigate('GlobalSearch', {
navigation.navigate('GlobalSearchScreen', {
novelName: item.novelName,
})
}
@@ -1,149 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { FlatList } from 'react-native';
import { ProgressBar } from 'react-native-paper';
import { Searchbar } from '../../../components/Searchbar/Searchbar';
import EmptyView from '../../../components/EmptyView';
import { ScreenContainer } from '../../../components/Common';
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
import { useLibrary, useTheme } from '../../../hooks/reduxHooks';
import { sourceManager } from '../../../sources/sourceManager';
import { useBrowseSettings, useSourcesReducer } from '../../../redux/hooks';
const GlobalSearch = ({ route, navigation }) => {
const theme = useTheme();
let novelName = '';
if (route.params) {
novelName = route.params.novelName;
}
const { allSources, pinnedSourceIds = [] } = useSourcesReducer();
const isPinned = sourceId => pinnedSourceIds.indexOf(sourceId) > -1;
const pinnedSources = allSources.filter(source => isPinned(source.sourceId));
const { searchAllSources = false } = useBrowseSettings();
const [searchText, setSearchText] = useState(novelName);
const [searchResults, setSearchResults] = useState([]);
const [progress, setProgress] = useState(0);
const library = useLibrary();
const isMounted = useRef(true);
useEffect(() => {
if (novelName) {
onSubmitEditing();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const clearSearchbar = () => setSearchText('');
const onChangeText = text => setSearchText(text);
const onSubmitEditing = async () => {
setProgress(0);
let globalSearchSources = searchAllSources ? allSources : pinnedSources;
setSearchResults(
globalSearchSources.map(item => ({
sourceId: item.sourceId,
sourceName: item.sourceName,
lang: item.lang,
loading: true,
novels: [],
error: null,
})),
);
globalSearchSources.map(async item => {
if (isMounted.current === true) {
try {
const source = sourceManager(item.sourceId);
const data = await source.searchNovels(encodeURI(searchText));
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? { ...sourceItem, novels: data, loading: false }
: { ...sourceItem },
),
);
} catch (e) {
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {
...sourceItem,
loading: false,
error: e.message,
}
: sourceItem,
),
);
}
setProgress(before => before + 1 / globalSearchSources.length);
}
});
};
const renderItem = ({ item }) => (
<GlobalSearchSourceItem
source={item}
library={library}
theme={theme}
navigation={navigation}
/>
);
return (
<ScreenContainer theme={theme}>
<Searchbar
theme={theme}
placeholder="Global Search"
backAction="arrow-left"
onBackAction={navigation.goBack}
searchText={searchText}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{ flexGrow: 1, padding: 4 }}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinnedSources}
ListEmptyComponent={
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? 'all sources'
: pinnedSources.length === 0
? 'pinned sources\n(No sources pinned)'
: 'pinned sources'
}`}
/>
}
/>
</ScreenContainer>
);
};
export default GlobalSearch;
@@ -1,51 +0,0 @@
import React from 'react';
import { StyleSheet, FlatList, Text } from 'react-native';
import GlobalSearchNovelCover from './GlobalSearchNovelCover';
const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
const inLibrary = (sourceId, novelUrl) =>
library.some(obj => obj.novelUrl === novelUrl && obj.sourceId === sourceId);
const navigateToNovel = item => navigation.push('Novel', item);
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
novel={item}
theme={theme}
onPress={() => navigateToNovel(item)}
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
/>
);
const ListEmptyComponent = (
<Text style={[{ color: theme.textColorSecondary }, styles.emptyList]}>
No results found
</Text>
);
return (
<FlatList
contentContainerStyle={styles.listContainer}
horizontal={true}
data={data}
keyExtractor={item => item.sourceId + item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
);
};
export default GlobalSearchNovelList;
const styles = StyleSheet.create({
listContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
emptyList: {
padding: 8,
paddingVertical: 4,
},
});
@@ -1,51 +0,0 @@
import React from 'react';
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native';
import GlobalSearchNovelList from './GlobalSearchNovelList';
const GlobalSearchSourceItem = ({ source, library, theme, navigation }) => {
const { sourceName, lang, loading, novels, error } = source;
const colorError = {
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
};
return (
<>
<View style={styles.sourceContainer}>
<Text style={{ color: theme.textColorPrimary }}>{sourceName}</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
{lang}
</Text>
</View>
{error ? (
<Text style={[styles.error, colorError]}>{error}</Text>
) : loading ? (
<ActivityIndicator
color={theme.colorAccent}
style={{ marginVertical: 16 }}
/>
) : (
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
)}
</>
);
};
export default GlobalSearchSourceItem;
const styles = StyleSheet.create({
sourceContainer: {
padding: 8,
paddingVertical: 16,
},
error: {
paddingHorizontal: 8,
paddingVertical: 16,
},
});
+26 -6
View File
@@ -2,26 +2,28 @@ import React, { useState } from 'react';
import { StyleSheet, SectionList, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import moment from 'moment';
import { Portal } from 'react-native-paper';
import {
EmptyView,
ErrorScreenV2,
LoadingScreenV2,
SearchbarV2,
} from '../../components';
} from '../../components/index';
import HistoryCard from './components/HistoryCard/HistoryCard';
import { useSearch, useHistory } from '../../hooks';
import { useThemeV1 } from '../../redux/hooks';
import { useSearch, useHistory, useBoolean } from '@hooks/index';
import { useTheme } from '../../redux/hooks';
import { dateFormats } from '../../utils/constants/dateFormats';
import { convertDateToISOString } from '../../database/utils/convertDateToISOString';
import { History } from '../../database/types';
import { getString } from '../../../strings/translations';
import { getString } from '@strings/translations';
import ClearHistoryDialog from './components/ClearHistoryDialog';
const HistoryScreen = () => {
const theme = useThemeV1();
const theme = useTheme();
const { navigate } = useNavigation();
const {
@@ -74,8 +76,10 @@ const HistoryScreen = () => {
sourceId: number,
novelId: number,
novelName: string,
novelUrl: string,
chapterId: number,
chapterUrl: string,
chapterName: string,
bookmark: number,
) =>
navigate(
@@ -84,9 +88,11 @@ const HistoryScreen = () => {
sourceId,
novelId,
novelName,
novelUrl,
chapterId,
chapterUrl,
bookmark,
chapterName,
} as never,
);
@@ -108,6 +114,12 @@ const HistoryScreen = () => {
} as never,
);
const {
value: clearHistoryDialogVisible,
setTrue: openClearHistoryDialog,
setFalse: closeClearHistoryDialog,
} = useBoolean();
return (
<>
<SearchbarV2
@@ -119,7 +131,7 @@ const HistoryScreen = () => {
rightIcons={[
{
iconName: 'delete-sweep-outline',
onPress: clearAllHistory,
onPress: openClearHistoryDialog,
},
]}
theme={theme}
@@ -158,6 +170,14 @@ const HistoryScreen = () => {
/>
}
/>
<Portal>
<ClearHistoryDialog
visible={clearHistoryDialogVisible}
onSubmit={clearAllHistory}
onDismiss={closeClearHistoryDialog}
theme={theme}
/>
</Portal>
</>
)}
</>
@@ -0,0 +1,72 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { getString } from '@strings/translations';
import { Dialog, Portal } from 'react-native-paper';
import { ThemeType } from '../../../theme/types';
import { Button } from '@components/index';
import { ButtonVariation } from '@components/Button/Button';
interface ClearHistoryDialogProps {
visible: boolean;
theme: ThemeType;
onSubmit: () => void;
onDismiss: () => void;
}
const ClearHistoryDialog: React.FC<ClearHistoryDialogProps> = ({
visible,
onDismiss,
theme,
onSubmit,
}) => {
const handleOnSubmit = () => {
onSubmit();
onDismiss();
};
return (
<Portal>
<Dialog
visible={visible}
onDismiss={onDismiss}
style={[styles.container, { backgroundColor: theme.colorPrimary }]}
>
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
{getString('historyScreen.clearHistorWarning')}
</Dialog.Title>
<Dialog.Actions>
<Button
theme={theme}
onPress={onDismiss}
title={getString('common.cancel')}
variation={ButtonVariation.CLEAR}
/>
<Button
theme={theme}
onPress={handleOnSubmit}
title={getString('common.ok')}
variation={ButtonVariation.CLEAR}
style={styles.button}
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default ClearHistoryDialog;
const styles = StyleSheet.create({
container: {
borderRadius: 8,
},
title: {
letterSpacing: 0,
fontSize: 16,
},
button: {
marginLeft: 4,
},
});
@@ -9,7 +9,7 @@ import { IconButtonV2 } from '../../../../components';
import { parseChapterNumber } from '../../../../utils/parseChapterNumber';
import { History } from '../../../../database/types';
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
import { ThemeType } from '../../../../theme/types';
import { coverPlaceholderColor } from '../../../../theme/colors';
interface HistoryCardProps {
@@ -18,8 +18,10 @@ interface HistoryCardProps {
sourceId: number,
novelId: number,
novelName: string,
novelUrl: string,
chapterId: number,
chapterUrl: string,
chapterName: string,
isBookmarked: number,
) => void;
handleRemoveFromHistory: (historyId: number) => void;
@@ -30,7 +32,7 @@ interface HistoryCardProps {
novelName: string,
novelCover: string,
) => void;
theme: ThemeTypeV1;
theme: ThemeType;
}
const HistoryCard: React.FC<HistoryCardProps> = ({
@@ -57,7 +59,7 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
const chapterNoAndTime = useMemo(
() =>
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
.format('h:mm a')
.format('LT')
.toUpperCase()}`,
[chapterName, historyTimeRead],
);
@@ -103,8 +105,10 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
sourceId,
novelId,
novelName,
novelUrl,
chapterId,
chapterUrl,
chapterName,
bookmark,
)
}
@@ -1,149 +0,0 @@
import React, { useCallback } from 'react';
import { Pressable, StyleSheet, View, Text } from 'react-native';
import moment from 'moment';
import FastImage from 'react-native-fast-image';
import { parseChapterNumber } from '../../../services/utils/helpers';
import { setNovel } from '../../../redux/novel/novel.actions';
import { defaultCoverUri } from '../../../sources/helpers/constants';
import { IconButton } from '../../../components/IconButton/IconButton';
const HistoryItem = ({
history,
theme,
dispatch,
navigation,
deleteHistory,
}) => {
const {
historyId,
historyTimeRead,
historyChapterId,
sourceId,
novelId,
novelUrl,
novelName,
novelCover,
chapterName,
chapterUrl,
bookmark,
} = history;
const getChapterNumber = useCallback(
() =>
`Chapter ${parseChapterNumber(chapterName)} • ${moment(historyTimeRead)
.format('h:mm a')
.toUpperCase()}`,
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const navigateToNovel = () => {
navigation.navigate('Novel', history);
dispatch(setNovel(history));
};
const navigateToChapter = () =>
navigation.navigate('Chapter', {
chapterId: historyChapterId,
chapterUrl,
sourceId,
novelUrl,
novelId,
chapterName,
novelName,
bookmark,
});
const uri =
novelCover && !novelCover.startsWith('/') ? novelCover : defaultCoverUri;
return (
<Pressable
onPress={navigateToNovel}
android_ripple={{ color: theme.rippleColor }}
>
<View style={styles.content}>
<View style={styles.container}>
<View>
<FastImage source={{ uri }} style={styles.novelCover} />
</View>
<View style={styles.textContainer}>
<Text
style={[{ color: theme.textColorPrimary }, styles.chapterTitle]}
numberOfLines={2}
>
{novelName}
</Text>
<Text
style={[
{ color: theme.textColorSecondary },
styles.chapterNumber,
]}
numberOfLines={1}
>
{getChapterNumber()}
</Text>
</View>
</View>
<View style={styles.buttons}>
<IconButton
icon="delete-outline"
size={24}
color={theme.textColorPrimary}
onPress={() => deleteHistory(historyId)}
containerStyle={{ marginLeft: 12 }}
theme={theme}
/>
<IconButton
icon="play"
size={24}
color={theme.textColorPrimary}
onPress={navigateToChapter}
containerStyle={{ marginLeft: 12 }}
theme={theme}
/>
</View>
</View>
</Pressable>
);
};
export default HistoryItem;
const styles = StyleSheet.create({
content: {
paddingVertical: 8,
paddingHorizontal: 16,
flexDirection: 'row',
justifyContent: 'space-between',
},
container: {
flex: 1,
flexDirection: 'row',
overflow: 'hidden',
},
novelCover: {
height: 80,
width: 56,
borderRadius: 4,
},
textContainer: {
flexShrink: 1,
marginLeft: 16,
justifyContent: 'center',
},
chapterTitle: {
fontSize: 15,
flexWrap: 'wrap',
},
buttons: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
chapterNumber: {
marginTop: 4,
},
});
@@ -1,48 +0,0 @@
import React from 'react';
import { Button, Dialog, Portal } from 'react-native-paper';
const RemoveHistoryDialog = ({ dialogVisible, hideDialog, theme, onPress }) => {
return (
<Portal>
<Dialog
visible={dialogVisible}
onDismiss={hideDialog}
style={{
borderRadius: 8,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.textColorPrimary,
}}
>
Are you sure? All history will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideDialog}
labelStyle={{ letterSpacing: 0 }}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={onPress}
labelStyle={{ letterSpacing: 0 }}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default RemoveHistoryDialog;
+10 -5
View File
@@ -44,6 +44,7 @@ const LibraryScreen = ({ navigation }) => {
incognitoMode = false,
updateLibraryOnLaunch = false,
downloadedOnlyMode = false,
disableHapticFeedback = false,
} = useSettings();
const bottomSheetRef = useRef(null);
@@ -94,7 +95,9 @@ const LibraryScreen = ({ navigation }) => {
const selectNovel = novelId => {
if (selectedNovels.indexOf(novelId) === -1) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
if (!disableHapticFeedback) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
setSelectedNovels(prevState => [...prevState, novelId]);
} else {
setSelectedNovels(prevState => prevState.filter(id => id !== novelId));
@@ -185,8 +188,8 @@ const LibraryScreen = ({ navigation }) => {
<View style={styles.globalSearch}>
<Pressable
onPress={() =>
navigation.navigate('GlobalSearch', {
novelName: searchText,
navigation.navigate('GlobalSearchScreen', {
searchText,
})
}
android_ripple={{ color: theme.rippleColor }}
@@ -259,11 +262,13 @@ const styles = StyleSheet.create({
},
globalSearch: {
marginHorizontal: 16,
borderRadius: 4,
marginTop: 8,
marginBottom: 16,
borderRadius: 50,
overflow: 'hidden',
},
globalSearchBtn: {
alignItems: 'center',
paddingVertical: 8,
paddingVertical: 12,
},
});
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
StyleSheet,
View,
@@ -68,33 +68,41 @@ const LibrarySheet = ({
</View>
);
const sortOrders = [
{
label: 'Date Added',
ASC: 'novels.novelId ASC',
DESC: 'novels.novelId DESC',
},
{
label: 'Alphabetically',
ASC: 'novels.novelName ASC',
DESC: 'novels.novelName DESC',
},
{
label: 'Unread',
ASC: 'novels.unread ASC',
DESC: 'novels.unread DESC',
},
{
label: 'Downloaded',
ASC: 'chaptersDownloaded ASC',
DESC: 'chaptersDownloaded DESC',
},
{
label: 'Total Chapters',
ASC: 'chaptersUnread ASC',
DESC: 'chaptersUnread DESC',
},
];
const sortOrders = useMemo(
() => [
{
label: 'Last Read',
ASC: 'lastReadAt ASC',
DESC: 'lastReadAt DESC',
},
{
label: 'Alphabetically',
ASC: 'novels.novelName ASC',
DESC: 'novels.novelName DESC',
},
{
label: 'Unread',
ASC: 'novels.unread ASC',
DESC: 'novels.unread DESC',
},
{
label: 'Downloaded',
ASC: 'chaptersDownloaded ASC',
DESC: 'chaptersDownloaded DESC',
},
{
label: 'Total Chapters',
ASC: 'chaptersUnread ASC',
DESC: 'chaptersUnread DESC',
},
{
label: 'Date Added',
ASC: 'novels.novelId ASC',
DESC: 'novels.novelId DESC',
},
],
[],
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
+3 -3
View File
@@ -7,7 +7,7 @@ import { useSelector } from 'react-redux';
import { List } from '../../components/List';
import { ScreenContainer } from '../../components/Common';
import { MoreHeader } from './components/MoreHeader';
import { appversion, releasedt } from '../../utils/utils';
import { appVersion, releaseDate } from '../../utils/versionUtils';
const AboutScreen = ({ navigation }) => {
const theme = useSelector(state => state.settingsReducer.theme);
@@ -24,14 +24,14 @@ const AboutScreen = ({ navigation }) => {
<List.Section>
<List.Item
title="Version"
description={`Stable ${appversion} (${releasedt})`}
description={`Stable ${appVersion} (${releaseDate})`}
theme={theme}
/>
<List.Item
title="What's new"
onPress={() =>
Linking.openURL(
`https://github.com/LNReader/lnreader/releases/tag/v${appversion}`,
`https://github.com/LNReader/lnreader/releases/tag/v${appVersion}`,
)
}
theme={theme}
+3 -1
View File
@@ -84,6 +84,7 @@ const Downloads = ({ navigation }) => {
const downloadChapter = (
sourceId,
novelUrl,
novelId,
chapterUrl,
chapterName,
chapterId,
@@ -92,6 +93,7 @@ const Downloads = ({ navigation }) => {
downloadChapterAction(
sourceId,
novelUrl,
novelId,
chapterUrl,
chapterName,
chapterId,
@@ -99,7 +101,7 @@ const Downloads = ({ navigation }) => {
);
const deleteChapter = (chapterId, chapterName) => {
dispatch(deleteChapterAction(chapterId, chapterName));
dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
setChapters(chaps => chaps.filter(item => item.chapterId !== chapterId));
};
+16 -5
View File
@@ -57,6 +57,7 @@ import { pickCustomNovelCover } from '../../database/queries/NovelQueries';
import FadeView from '../../components/Common/CrossFadeView';
import { useModal } from '../../hooks/useModal';
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const Novel = ({ route, navigation }) => {
const item = route.params;
@@ -64,6 +65,9 @@ const Novel = ({ route, navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { top: topInset } = useSafeAreaInsets();
const progressViewOffset = topInset + 32;
const { novel, chapters, loading, updating } = useNovel();
const { downloadQueue } = useSelector(state => state.downloadsReducer);
@@ -80,6 +84,7 @@ const Novel = ({ route, navigation }) => {
const {
useFabForContinueReading = false,
defaultChapterSort = 'ORDER BY chapterId ASC',
disableHapticFeedback = false,
} = useSettings();
const {
@@ -107,6 +112,7 @@ const Novel = ({ route, navigation }) => {
const refreshControl = () => (
<RefreshControl
progressViewOffset={progressViewOffset}
onRefresh={onRefresh}
refreshing={updating}
colors={[theme.textColorPrimary]}
@@ -123,6 +129,7 @@ const Novel = ({ route, navigation }) => {
downloadChapterAction(
sourceId,
novelUrl,
novelId,
chapterUrl,
chapterName,
chapterId,
@@ -130,7 +137,7 @@ const Novel = ({ route, navigation }) => {
);
const deleteChapter = (chapterId, chapterName) =>
dispatch(deleteChapterAction(chapterId, chapterName));
dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
const isSelected = chapterId => {
return selected.some(obj => obj.chapterId === chapterId);
@@ -152,7 +159,9 @@ const Novel = ({ route, navigation }) => {
const onSelectLongPress = chapter => {
if (selected.length === 0) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
if (!disableHapticFeedback) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
setSelected(sel => [...sel, chapter]);
} else {
/**
@@ -472,7 +481,9 @@ const Novel = ({ route, navigation }) => {
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() => dispatch(deleteAllChaptersAction(chapters))}
onPress={() =>
dispatch(deleteAllChaptersAction(sourceId, chapters))
}
/>
</Menu>
@@ -594,7 +605,7 @@ const Novel = ({ route, navigation }) => {
selected.some(obj => obj.downloaded === 1) && {
icon: 'trash-can-outline',
onPress: () => {
dispatch(deleteAllChaptersAction(selected));
dispatch(deleteAllChaptersAction(sourceId, selected));
setSelected([]);
},
},
@@ -641,7 +652,7 @@ const Novel = ({ route, navigation }) => {
action={{
label: 'Delete',
onPress: () => {
dispatch(deleteAllChaptersAction(chapters));
dispatch(deleteAllChaptersAction(sourceId, chapters));
},
}}
theme={{ colors: { accent: theme.colorAccent } }}
@@ -108,7 +108,10 @@ const EditInfoModal = ({ theme, hideModal, modalVisible, novel, dispatch }) => {
dense
/>
<TextInput
placeholder={`Description: ${info.novelSummary.substring(0, 16)}...`}
placeholder={`Description: ${info?.novelSummary?.substring(
0,
16,
)}...`}
style={{ fontSize: 14 }}
numberOfLines={1}
mode="outlined"
@@ -1,6 +1,8 @@
import React, { memo, useMemo } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import { IconButton } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
@@ -68,7 +70,7 @@ const NovelInfoHeader = ({
<NovelTitle
theme={theme}
onPress={() =>
navigation.navigate('GlobalSearch', {
navigation.navigate('GlobalSearchScreen', {
novelName: item.novelName,
})
}
@@ -31,7 +31,8 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
? 'Start reading'
: getString('novelScreen.continueReading')
} ${lastRead.chapterName}`}
margin={16}
style={{ margin: 16 }}
textColor={theme.colorButtonText}
theme={theme}
onPress={navigateToLastReadChapter}
/>
@@ -42,8 +43,9 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
? 'All chapters read'
: getString('novelScreen.continueReading')
} ${lastRead.chapterName}`}
margin={16}
style={{ margin: 16 }}
color={theme.colorDisabled}
textColor={theme.colorButtonText}
theme={theme}
/>
))
@@ -6,12 +6,12 @@ import * as WebBrowser from 'expo-web-browser';
import { NovelInfo } from '../../../../database/types';
import { useNavigation } from '@react-navigation/native';
import { useNovelTrackerInfo } from '../../../../hooks';
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
import { ThemeType } from '../../../../theme/types';
import { getString } from '../../../../../strings/translations';
interface NovelScreenButtonGroupProps {
novel: NovelInfo;
theme: ThemeTypeV1;
theme: ThemeType;
handleTrackerSheet: () => void;
handleFollowNovel: () => void;
}
@@ -26,7 +26,11 @@ const NovelScreenButtonGroup: React.FC<NovelScreenButtonGroupProps> = ({
const { navigate } = useNavigation();
const followButtonColor = followed ? theme.colorAccent : theme.textColorHint;
const { isTracked } = useNovelTrackerInfo(novel.novelId);
const { isTracked, isTrackerAvailable } = useNovelTrackerInfo(novel.novelId);
const trackerButtonColor = isTracked
? theme.colorAccent
: theme.textColorHint;
const handleOpenWebView = async () => {
WebBrowser.openBrowserAsync(sourceUrl);
@@ -61,7 +65,7 @@ const NovelScreenButtonGroup: React.FC<NovelScreenButtonGroupProps> = ({
</Text>
</Pressable>
</View>
{isTracked ? (
{isTrackerAvailable ? (
<View style={styles.buttonContainer}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
@@ -70,10 +74,10 @@ const NovelScreenButtonGroup: React.FC<NovelScreenButtonGroupProps> = ({
>
<MaterialCommunityIcons
name={isTracked ? 'check' : 'sync'}
color={theme.textColorHint}
color={trackerButtonColor}
size={24}
/>
<Text style={[styles.buttonLabel, { color: theme.textColorHint }]}>
<Text style={[styles.buttonLabel, { color: trackerButtonColor }]}>
{isTracked ? 'Tracked' : 'Tracking'}
</Text>
</Pressable>
@@ -2,12 +2,12 @@ import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { ThemeTypeV1 } from '../../../../theme/v1/theme/types';
import { ThemeType } from '../../../../theme/types';
interface NovelSummaryProps {
summary: string;
isExpanded: boolean;
theme: ThemeTypeV1;
theme: ThemeType;
}
const NovelSummary: React.FC<NovelSummaryProps> = ({
+148 -139
View File
@@ -6,8 +6,12 @@ import {
ScrollView,
TouchableWithoutFeedback,
Dimensions,
NativeModules,
NativeEventEmitter,
} from 'react-native';
import VolumeButtonListener from './../../utils/volumeButtonListener';
import { useDispatch } from 'react-redux';
import { IconButton, Portal } from 'react-native-paper';
import { useKeepAwake } from 'expo-keep-awake';
@@ -20,19 +24,16 @@ import { fetchChapter } from '../../services/Source/source';
import { showToast } from '../../hooks/showToast';
import {
usePosition,
useReaderSettings,
useSettings,
useTheme,
useTrackingStatus,
} from '../../hooks/reduxHooks';
import { updateChaptersRead } from '../../redux/tracker/tracker.actions';
import { readerBackground, readerTextColor } from './utils/readerStyles';
import { markChapterReadAction } from '../../redux/novel/novel.actions';
import { saveScrollPosition } from '../../redux/preferences/preference.actions';
import { parseChapterNumber } from '../../utils/parseChapterNumber';
import ReaderAppbar from './components/ReaderAppbar';
import ReaderBottomSheet from './components/ReaderBottomSheet/ReaderBottomSheet';
import ReaderFooter from './components/ReaderFooter';
import ReaderSeekBar from './components/ReaderSeekBar';
@@ -43,14 +44,16 @@ import { LoadingScreen } from '../../components/LoadingScreen/LoadingScreen';
import { insertHistory } from '../../database/queries/HistoryQueries';
import { SET_LAST_READ } from '../../redux/preferences/preference.types';
import { setAppSettings } from '../../redux/settings/settings.actions';
import { useBatteryLevel } from 'react-native-device-info';
import moment from 'moment';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import TextReader from './components/TextReader';
import WebViewReader from './components/WebViewReader';
import { useTextToSpeech } from '../../hooks/useTextToSpeech';
import { useFullscreenMode } from '../../hooks';
import { getChapterFromDb } from '../../database/queries/DownloadQueries';
import ReaderBottomSheetV2 from './components/ReaderBottomSheet/ReaderBottomSheet';
import { useReaderSettings } from '../../redux/hooks';
import { defaultTo } from 'lodash';
import BottomInfoBar from './components/BottomInfoBar/BottomInfoBar';
import { sanitizeChapterText } from './utils/sanitizeChapterText';
const Chapter = ({ route, navigation }) => {
useKeepAwake();
@@ -72,25 +75,23 @@ const Chapter = ({ route, navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const readerSettings = useReaderSettings();
const insets = useSafeAreaInsets();
const {
showScrollPercentage = true,
fullScreenMode = true,
swipeGestures = true,
swipeGestures = false,
incognitoMode = false,
textSelectable = false,
useWebViewForChapter = false,
showBatteryAndTime = false,
wvUseNewSwipes = false,
wvShowSwipeMargins = true,
wvUseVolumeButtons = false,
autoScroll = false,
autoScrollInterval = 10,
autoScrollOffset = null,
verticalSeekbar = true,
removeExtraParagraphSpacing = false,
} = useSettings();
const { setImmersiveMode, showStatusAndNavBar } = useFullscreenMode();
const batteryLevel = useBatteryLevel();
const [hidden, setHidden] = useState(true);
const { tracker, trackedNovels } = useTrackingStatus();
@@ -107,6 +108,40 @@ const Chapter = ({ route, navigation }) => {
const [contentSize, setContentSize] = useState(0);
const [scrollPage, setScrollPage] = useState(null);
useEffect(() => {
VolumeButtonListener.disconnect();
if (useWebViewForChapter && wvUseVolumeButtons) {
VolumeButtonListener.connect();
VolumeButtonListener.preventDefault();
const emmiter = new NativeEventEmitter(
NativeModules.VolumeButtonListener,
);
emmiter.removeAllListeners('VolumeUp');
emmiter.removeAllListeners('VolumeDown');
const upSub = emmiter.addListener('VolumeUp', e => {
setScrollPage('up');
});
const downSub = emmiter.addListener('VolumeDown', e => {
setScrollPage('down');
});
return () => {
VolumeButtonListener.disconnect();
upSub?.remove();
downSub?.remove();
};
}
}, []);
useEffect(() => {
if (wvUseVolumeButtons) {
VolumeButtonListener.connect();
} else {
VolumeButtonListener.disconnect();
}
}, [wvUseVolumeButtons]);
const getChapter = async id => {
try {
if (id) {
@@ -163,30 +198,33 @@ const Chapter = ({ route, navigation }) => {
setPrevAndNextChap();
}, [chapter]);
const [currentTime, setCurrentTime] = useState();
useEffect(() => {
const timeInterval = setInterval(() => {
setCurrentTime(new Date().toISOString());
}, 60000);
return () => clearInterval(timeInterval);
}, []);
const [currentOffset, setCurrentOffset] = useState(position?.position || 0);
let scrollTimeout;
useEffect(() => {
if (!useWebViewForChapter && scrollPercentage !== 100 && autoScroll) {
if (scrollPercentage !== 100 && autoScroll) {
scrollTimeout = setTimeout(() => {
scrollViewRef.current.scrollTo({
x: 0,
y: currentOffset + Dimensions.get('window').height,
animated: true,
});
if (useWebViewForChapter) {
setWebViewScroll({
percentage:
currentOffset +
defaultTo(autoScrollOffset, Dimensions.get('window').height),
type: 'exact',
});
} else {
scrollViewRef.current.scrollTo({
x: 0,
y:
currentOffset +
defaultTo(autoScrollOffset, Dimensions.get('window').height),
animated: true,
});
}
setCurrentOffset(
prevState => prevState + Dimensions.get('window').height,
prevState =>
prevState +
defaultTo(autoScrollOffset, Dimensions.get('window').height),
);
}, autoScrollInterval * 1000);
}
@@ -235,22 +273,40 @@ const Chapter = ({ route, navigation }) => {
}
}, []);
const scrollToSavedProgress = useCallback(event => {
if (position && firstLayout) {
position.percentage < 100 &&
scrollViewRef.current.scrollTo({
x: 0,
y: position.position,
animated: false,
});
setFirstLayout(false);
}
}, []);
const [webViewScroll, setWebViewScroll] = useState(scrollPercentage);
const scrollToSavedProgress = useCallback(
event => {
if (position && firstLayout) {
if (position.percentage < 100) {
scrollViewRef.current.scrollTo({
x: 0,
y: position.position,
animated: false,
});
if (useWebViewForChapter) {
setWebViewScroll({
percentage: position.percentage,
type: 'smooth',
});
}
}
setFirstLayout(false);
}
},
[nextChapter],
);
const hideHeader = () => {
if (!hidden) {
if (useWebViewForChapter && wvUseVolumeButtons) {
VolumeButtonListener.connect();
}
setImmersiveMode();
} else {
if (useWebViewForChapter && wvUseVolumeButtons) {
VolumeButtonListener.disconnect();
}
showStatusAndNavBar();
}
setHidden(!hidden);
@@ -289,13 +345,6 @@ const Chapter = ({ route, navigation }) => {
})
: showToast("There's no next chapter");
const enableSwipeGestures = () => {
dispatch(setAppSettings('swipeGestures', !swipeGestures));
showToast(
swipeGestures ? 'Swipe gestures disabled' : 'Swipe gestured enabled',
);
};
const enableAutoScroll = () =>
dispatch(setAppSettings('autoScroll', !autoScroll));
@@ -311,7 +360,11 @@ const Chapter = ({ route, navigation }) => {
}
};
const backgroundColor = readerBackground(readerSettings.theme);
const backgroundColor = readerSettings.theme;
const chapterText = sanitizeChapterText(chapter.chapterText, {
removeExtraParagraphSpacing,
});
return (
<>
@@ -332,8 +385,16 @@ const Chapter = ({ route, navigation }) => {
pauseTts={pauseTts}
/>
<GestureRecognizer
onSwipeRight={swipeGestures && navigateToPrevChapter}
onSwipeLeft={swipeGestures && navigateToNextChapter}
onSwipeRight={
swipeGestures &&
(!useWebViewForChapter || !wvUseNewSwipes) &&
navigateToPrevChapter
}
onSwipeLeft={
swipeGestures &&
(!useWebViewForChapter || !wvUseNewSwipes) &&
navigateToNextChapter
}
config={config}
style={{ flex: 1 }}
>
@@ -352,35 +413,20 @@ const Chapter = ({ route, navigation }) => {
<EmptyView
icon="Σ(ಠ_ಠ)"
description={error}
style={{
color:
readerSettings.textColor ||
readerTextColor(readerSettings.theme),
}}
style={{ color: readerSettings.textColor }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 16 }}
color={
readerSettings.textColor ||
readerTextColor(readerSettings.theme)
}
color={readerSettings.textColor}
onPress={() => {
getChapter(chapterId);
setLoading(true);
setError();
}}
/>
<Text
style={{
color:
readerSettings.textColor ||
readerTextColor(readerSettings.theme),
}}
>
Retry
</Text>
<Text style={{ color: readerSettings.textColor }}>Retry</Text>
</EmptyView>
</View>
) : loading ? (
@@ -388,28 +434,40 @@ const Chapter = ({ route, navigation }) => {
) : (
<TouchableWithoutFeedback
style={{ flex: 1 }}
onPress={hideHeader}
onLayout={scrollToSavedProgress}
>
{useWebViewForChapter ? (
<View style={{ flex: 1 }}>
<WebViewReader
layoutHeight={Dimensions.get('window').height}
webViewScroll={webViewScroll}
setScrollPercentage={setScrollPercentage}
scrollPercentage={scrollPercentage}
reader={readerSettings}
html={chapter.chapterText}
html={chapterText}
chapterName={chapter.chapterName || chapterName}
nextChapter={nextChapter}
navigateToNextChapter={() => navigateToNextChapter()}
navigateToPrevChapter={() => navigateToPrevChapter()}
onScroll={onScroll}
onPress={hideHeader}
onWebViewNavigationStateChange={
onWebViewNavigationStateChange
}
scrollPage={scrollPage}
setScrollPage={setScrollPage}
swipeGestures={swipeGestures && wvUseNewSwipes}
wvShowSwipeMargins={wvShowSwipeMargins}
theme={theme}
/>
</View>
) : (
<View>
<TextReader
text={chapter.chapterText}
onPress={hideHeader}
text={chapterText}
reader={readerSettings}
chapterName={chapterName || chapter.chapterName}
textSelectable={textSelectable}
chapterName={chapter.chapterName || chapterName}
theme={theme}
nextChapter={nextChapter}
navigateToNextChapter={navigateToNextChapter}
@@ -420,42 +478,26 @@ const Chapter = ({ route, navigation }) => {
)}
</ScrollView>
</GestureRecognizer>
<BottomInfoBar scrollPercentage={scrollPercentage} />
<Portal>
<ReaderBottomSheet
theme={theme}
reader={readerSettings}
dispatch={dispatch}
navigation={navigation}
bottomSheetRef={readerSheetRef}
verticalSeekbar={verticalSeekbar}
selectText={textSelectable}
autoScroll={autoScroll}
useWebViewForChapter={useWebViewForChapter}
showScrollPercentage={showScrollPercentage}
showBatteryAndTime={showBatteryAndTime}
fullScreenMode={fullScreenMode}
swipeGestures={swipeGestures}
enableSwipeGestures={enableSwipeGestures}
/>
<ReaderBottomSheetV2 bottomSheetRef={readerSheetRef} />
</Portal>
{!useWebViewForChapter && (
<ReaderSeekBar
theme={theme}
hide={hidden}
setLoading={setLoading}
contentSize={contentSize}
scrollViewRef={scrollViewRef}
scrollPercentage={scrollPercentage}
setScrollPercentage={setScrollPercentage}
verticalSeekbar={verticalSeekbar}
/>
)}
<ReaderSeekBar
theme={theme}
hide={hidden}
setLoading={setLoading}
contentSize={contentSize}
scrollViewRef={scrollViewRef}
scrollPercentage={scrollPercentage}
setScrollPercentage={setScrollPercentage}
setWebViewScroll={setWebViewScroll}
useWebViewForChapter={useWebViewForChapter}
verticalSeekbar={verticalSeekbar}
/>
<ReaderFooter
theme={theme}
novelUrl={novelUrl}
swipeGestures={swipeGestures}
enableSwipeGestures={enableSwipeGestures}
chapterUrl={chapterUrl}
enableWebView={enableWebView}
dispatch={dispatch}
nextChapter={nextChapter}
@@ -469,39 +511,6 @@ const Chapter = ({ route, navigation }) => {
scrollViewRef={scrollViewRef}
enableAutoScroll={enableAutoScroll}
/>
{(showScrollPercentage || showBatteryAndTime) && (
<View
style={[
styles.scrollPercentageContainer,
{ backgroundColor },
!fullScreenMode && {
paddingBottom: insets.bottom,
},
]}
>
{showBatteryAndTime && (
<Text style={{ color: readerSettings.textColor }}>
{Math.ceil(batteryLevel * 100) + '%'}
</Text>
)}
{showScrollPercentage && (
<Text
style={{
flex: 1,
color: readerSettings.textColor,
textAlign: 'center',
}}
>
{scrollPercentage + '%'}
</Text>
)}
{showBatteryAndTime && (
<Text style={{ color: readerSettings.textColor }}>
{moment(currentTime).format('h:mm')}
</Text>
)}
</View>
)}
</>
</>
);
@@ -0,0 +1,87 @@
import { StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useState } from 'react';
import { useBatteryLevel } from 'react-native-device-info';
import moment from 'moment';
import { useReaderSettings, useSettingsV1 } from '@redux/hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface BottomInfoBarProps {
scrollPercentage: number;
}
const BottomInfoBar: React.FC<BottomInfoBarProps> = ({ scrollPercentage }) => {
const { bottom: bottomInset } = useSafeAreaInsets();
const {
showScrollPercentage = true,
showBatteryAndTime = false,
fullScreenMode = true,
} = useSettingsV1();
const { textColor, theme: backgroundColor } = useReaderSettings();
const batteryLevel = useBatteryLevel();
const [currentTime, setCurrentTime] = useState<string>();
useEffect(() => {
const timeInterval = setInterval(() => {
setCurrentTime(new Date().toISOString());
}, 60000);
return () => clearInterval(timeInterval);
}, []);
if (showScrollPercentage || showBatteryAndTime) {
return (
<View
style={[
styles.container,
{ backgroundColor },
!fullScreenMode && {
paddingBottom: bottomInset,
},
]}
>
{showBatteryAndTime && batteryLevel ? (
<Text style={{ color: textColor }}>
{Math.ceil(batteryLevel * 100) + '%'}
</Text>
) : null}
{showScrollPercentage ? (
<Text style={[styles.scrollPercentage, { color: textColor }]}>
{scrollPercentage + '%'}
</Text>
) : null}
{showBatteryAndTime ? (
<Text style={{ color: textColor }}>
{moment(currentTime).format('LT')}
</Text>
) : null}
</View>
);
} else {
return null;
}
};
export default BottomInfoBar;
const styles = StyleSheet.create({
container: {
position: 'absolute',
paddingVertical: 10,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
flexDirection: 'row',
paddingHorizontal: 32,
alignItems: 'center',
justifyContent: 'space-between',
},
scrollPercentage: {
flex: 1,
textAlign: 'center',
},
});
@@ -42,7 +42,7 @@ const ReaderAppbar = ({
<Appbar.Content
title={novelName}
titleStyle={{ color: theme.textColorPrimary }}
subtitle={chapterName}
subtitle={chapterName?.trim()}
subtitleStyle={{ color: theme.textColorSecondary }}
/>
<Appbar.Action

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