Compare commits

..

133 Commits

Author SHA1 Message Date
Rajarshee Chatterjee d749225820 chore: Update translations (#343)
* New translations strings.json (Russian)
[ci skip]

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

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

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

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

* New translations strings.json (Turkish)
[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 (Italian)
[ci skip]

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

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

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

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

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

* New translations strings.json (German)
[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 (Arabic)
[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 (Portuguese, Brazilian)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

* New translations strings.json (Turkish)
[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 (Italian)
[ci skip]

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

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

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

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

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

* New translations strings.json (German)
[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 (Arabic)
[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 (Portuguese, Brazilian)
[ci skip]

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

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

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

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

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

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

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

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

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

* New translations strings.json (Turkish)
[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 (Italian)
[ci skip]

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

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

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

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

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

* New translations strings.json (German)
[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 (Arabic)
[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 (Portuguese, Brazilian)
[ci skip]

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

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

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

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

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

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

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

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

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

* New translations strings.json (Turkish)
[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 (Italian)
[ci skip]

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

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

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

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

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

* New translations strings.json (German)
[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 (Arabic)
[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 (Portuguese, Brazilian)
[ci skip]

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

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

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

* New translations strings.json (Russian)
[ci skip]
2022-09-02 09:30:32 +05:30
Rajarshee Chatterjee d6d487101b feat(source): Add Madara Sources
Closes LNReader/lnreader-sources#365
Closes LNReader/lnreader-sources#363
2022-08-28 11:49:05 +05:30
Rajarshee Chatterjee ba62c1ba0a Minor Fixes 2022-08-28 11:37:04 +05:30
Rajarshee Chatterjee 75f4f18448 Minor Fixes 2022-08-28 11:05:25 +05:30
Rajarshee Chatterjee f9ebec4a30 fix: Global Search Navigation (Closes #406) 2022-08-28 10:56:05 +05:30
Rajarshee Chatterjee de18e3d9d8 fix: Category Fixes 2022-08-28 10:56:05 +05:30
Rajarshee Chatterjee acaac74db4 fix: Add Chapter Name Back in Reader (Closes #410) 2022-08-28 10:56:05 +05:30
Rajarshee Chatterjee d8845661a7 fix: Long Category Names (Closes #407) 2022-08-28 10:56:05 +05:30
Rider21 b66a6130c5 feat(source): Add Ficbook (#409)
* update

* fix

* oops
2022-08-25 07:15:34 +05:30
Rhino 220f25bf72 feat: Update chapter tab sort (#402)
* Added following sources Zetro Translation, Nocturne, Asada Translation and Sugar Babies

* added time stamp to backup file

* tweaks updated chapter sort order in Updates Tab
2022-08-25 07:12:45 +05:30
Rajarshee Chatterjee 934edbb84c feat: Add Change Default Category (Closes #383) 2022-08-18 21:16:35 +05:30
Rajarshee Chatterjee 0d8a5d6172 fix: Navbar Color (Closes #386) 2022-08-18 21:13:36 +05:30
Rajarshee Chatterjee e706745f30 feat: Select Current Category in Library (Closes #396) 2022-08-18 09:43:05 +05:30
Rajarshee Chatterjee dce3652ae8 feat: Add Select All in Library (Closes #380) 2022-08-18 09:07:12 +05:30
Rajarshee Chatterjee 0ca3b8ce15 fix: Cleanup
Closes #378
Closes #393
2022-08-18 08:40:49 +05:30
Rajarshee Chatterjee c62db5cb23 chore: Update Source Icon Links 2022-08-18 07:44:55 +05:30
Rajarshee Chatterjee e33b433e84 fix: Change status bar style on background color change (Closes #150) 2022-08-17 08:18:12 +05:30
Rhino c95f376e20 feat(source): Add following sources Zetro, Nocturne, Asada and SugarBB (#389)
* Added following sources Zetro Translation, Nocturne, Asada Translation and Sugar Babies

* added time stamp to backup file
2022-08-16 23:20:00 +05:30
Rhino 9b45375638 feat: Add time stamp to backup file (#390) 2022-08-16 23:17:03 +05:30
Rajarshee Chatterjee 7fbdc6732c fix: Reset Category to default on removing from current category (Closes #379) 2022-08-16 13:27:54 +05:30
Rajarshee Chatterjee 3f9549d320 fix: Path Aliases 2022-08-16 13:03:29 +05:30
Rajarshee Chatterjee 079010b6ca chore: Fix Issue Template Label 2022-08-16 13:00:34 +05:30
Rajarshee Chatterjee 107e534565 Cleanup 2022-08-16 12:38:13 +05:30
Rajarshee Chatterjee f0665866ae feat: Custom Order in Categories (Closes #376) 2022-08-16 10:56:08 +05:30
Rajarshee Chatterjee 706b4581b1 fix: Hide "Removed from library" toast if novel is in library 2022-08-16 07:26:06 +05:30
Rajarshee Chatterjee a8d9c3abfa chore: Add Issue Templates and Moderator 2022-08-15 20:27:19 +05:30
Rajarshee Chatterjee 66798c46db Minor fixes
Closes #373
Closes #374
2022-08-15 16:51:21 +05:30
Rajarshee Chatterjee 87fa081cfd feat: Material You (#377)
* feat: Material You

* feat: Material You
2022-08-15 15:36:06 +05:30
Rajarshee Chatterjee ea26cd67e4 fix(source): Add Status in Ranobes (Closes LNReader/lnreader-sources#359) 2022-08-14 15:44:09 +05:30
Rajarshee Chatterjee 9400fcd252 feat(source): Add NeoSekai Translations and MTL-Novel
Closes LNReader/lnreader-sources#306
Closes LNReader/lnreader-sources#328
2022-08-14 15:43:22 +05:30
Rajarshee Chatterjee 30ca05df06 feat: Add novel in library on long press in search results (Closes #151) 2022-08-14 13:00:21 +05:30
Rajarshee Chatterjee eac3ee32c8 fix: Add Empty Summary placeholder and show Full Novel Title (Closes #355) 2022-08-14 11:43:49 +05:30
Rajarshee Chatterjee 0cf249eec5 fix: Use Separate States for Search Results in Browse Screen (Closes #359) 2022-08-14 11:15:56 +05:30
Rajarshee Chatterjee 27e5300f4f fix: Clear Searchbar when Screen is Unfocused (Closes #358) 2022-08-14 11:13:10 +05:30
Rajarshee Chatterjee 97e02177a4 fix: Global Search from Novel and MAL Screen (Closes #356) 2022-08-14 10:23:04 +05:30
Rajarshee Chatterjee 42b37f87fe fix(source): Remove NovelSpace and Move ArNovel to MadaraGenerator
(Closes LNReader/lnreader-sources#353)
2022-08-14 10:06:03 +05:30
Rajarshee Chatterjee 4ad1c0253b fix: Reset Novel Categories on deleting a Category (Closes #370) 2022-08-14 09:52:00 +05:30
Rajarshee Chatterjee a57ed7a7a2 feat: Add category-wise updates 2022-08-14 09:30:16 +05:30
Rajarshee Chatterjee 9fa00d9d9b fix(source): Update WuxiaWorldCo baseUrl 2022-08-14 09:28:33 +05:30
Rajarshee Chatterjee 18f63a217f fix: Category/Library bugs 2022-08-14 07:44:53 +05:30
Rajarshee Chatterjee 05401ada6b fix: Fix deleting downloaded chapters (Closes #341) 2022-08-14 07:02:25 +05:30
Rajarshee Chatterjee e256537079 feat: Add Categories (#366)
* feat: Add Categories (Closes #45)

* fix: ESLint Errors

* feat: Add Validation for Duplicate Category Name

* feat: Multiple Categories Support & Hide Default Category

* fix: Creating Tables on Clean Installation

* feat: Add Started Filter
2022-08-13 23:07:58 +05:30
Rider21 0eec38f26f feat: Source Updates (#350)
- Fix ranobes.net (Closes LNReader/lnreader-sources#70)
- Add ranobes.com source
2022-08-05 21:22:24 +05:30
Rider21 fa90d57eff refactor: Replace moment with Dayjs (#346)
* dayjs

* locales added

* fix
2022-08-02 13:17:45 +05:30
Rajarshee Chatterjee fd6a907c5f Release v1.1.12 2022-07-30 21:36:45 +05:30
Rajarshee Chatterjee c7561fc3dd fix: New Update Dialog Enhancements 2022-07-30 21:33:26 +05:30
Rajarshee Chatterjee e1401ba39f fix(source): Fix Riwyat Chapter Text 2022-07-30 21:32:20 +05:30
Rajarshee Chatterjee cbb3c7cfa2 Release v1.1.11 2022-07-30 09:53:23 +05:30
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
343 changed files with 18903 additions and 8859 deletions
+94
View File
@@ -0,0 +1,94 @@
# Cloned from tachiyomi
name: Issue report
description: Report an issue in LNReader
labels: [Bug]
body:
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce
description: Provide an example of the issue.
placeholder: |
Example:
1. First step
2. Second step
3. Issue here
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Explain what you should expect to happen.
placeholder: |
Example:
"This should happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: Explain what actually happens.
placeholder: |
Example:
"This happened instead..."
validations:
required: true
- type: input
id: lnreader-version
attributes:
label: LNReader version
description: You can find your LNReader version in **More → About**.
placeholder: |
Example: "1.1.12"
validations:
required: true
- type: input
id: android-version
attributes:
label: Android version
description: You can find this somewhere in your Android settings.
placeholder: |
Example: "Android 11"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: List your device and model.
placeholder: |
Example: "Google Pixel 5"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
required: true
- label: I have updated the app to version **[1.1.12](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
@@ -0,0 +1,40 @@
# Cloned from tachiyomi
name: Feature request
description: Suggest a feature to improve LNReader
labels: [Feature Request]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can LNReader be improved?
placeholder: |
Example:
"It should work like this..."
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
required: true
- label: I have updated the app to version **[1.1.12](https://github.com/LNReader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+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
+31
View File
@@ -0,0 +1,31 @@
# Cloned from tachiyomi
name: Issue moderator
on:
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
auto-close-rules: |
[
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (LNReader version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
}
]
+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
+2 -2
View File
@@ -13,13 +13,13 @@ import * as SplashScreen from 'expo-splash-screen';
import Main from './src/navigators/Main';
import { createDB } from './src/database/db';
import { createDatabase } from './src/database/db';
import { SafeAreaProvider } from 'react-native-safe-area-context';
const App = () => {
useEffect(() => {
SplashScreen.preventAutoHideAsync();
createDB();
createDatabase();
}, []);
return (
+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.12"
}
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();
}
}
+1
View File
@@ -6,6 +6,7 @@ buildscript {
minSdkVersion = 21
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "21.1.6352462"
}
repositories {
google()
+19
View File
@@ -2,5 +2,24 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
[
'module-resolver',
{
alias: {
'@components': './src/components',
'@database': './src/database',
'@hooks': './src/hooks',
'@screens': './src/screens',
'@strings': './strings',
'@redux': './src/redux',
'@services': './src/services',
'@sources': './src/sources',
'@utils': './src/utils',
'@theme': './src/theme',
},
},
],
],
};
};
+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
+769 -440
View File
File diff suppressed because it is too large Load Diff
+15 -11
View File
@@ -1,6 +1,6 @@
{
"name": "lnreader",
"version": "1.1.8",
"version": "1.1.12",
"private": true,
"scripts": {
"android": "react-native run-android",
@@ -9,26 +9,29 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"prettier": "prettier --write \"./src/**/*.{js,jsx,ts,tsx}\"",
"buildRelease": "cd android && gradlew clean && gradlew assembleRelease",
"sources:madara": "node scripts/generateMadaraSource.cjs && prettier --write {src/sources/multisrc/madara/MadaraGenerator.ts,src/sources/sources.json}",
"sources:madara": "node scripts/generateMadaraSource.cjs",
"source:generator": "node scripts/generateSource.cjs",
"strings": "node scripts/stringTypes.cjs",
"prepare": "husky install"
},
"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",
"@shopify/flash-list": "^1.2.1",
"buffer": "^6.0.3",
"cheerio": "^1.0.0-rc.10",
"color": "^4.2.3",
"dayjs": "^1.11.4",
"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 +40,9 @@
"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",
"prop-types": "^15.7.2",
"qs": "^6.10.1",
"react": "16.13.1",
@@ -52,38 +53,41 @@
"react-native-easing-gradient": "^1.0.0",
"react-native-fast-image": "^8.5.11",
"react-native-gesture-handler": "~1.10.2",
"react-native-markdown-display": "^7.0.0-alpha.2",
"react-native-mmkv": "^2.4.3",
"react-native-pager-view": "^5.4.9",
"react-native-paper": "^4.10.1",
"react-native-paper": "^5.0.0-rc.4",
"react-native-reanimated": "^2.2.4",
"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"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@react-native-community/eslint-config": "^3.0.1",
"@types/color": "^3.0.3",
"@types/i18n-js": "^3.8.2",
"@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",
+21 -2
View File
@@ -1,11 +1,17 @@
const path = require('path');
const fs = require('fs');
const prettier = require('prettier');
const prettierConfig = require('../.prettierrc');
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, 'Dash')
.replace(/\./g, 'Dot')
.replace('1st', 'First');
return name;
};
@@ -23,6 +29,11 @@ MadaraSources.forEach(source => {
}", ${source.options ? JSON.stringify(source.options) : ''});\n\n`;
});
content = prettier.format(content, {
parser: 'typescript',
...prettierConfig,
});
fs.writeFile(
path.resolve(process.cwd(), 'src/sources/multisrc/madara/MadaraGenerator.ts'),
content,
@@ -40,13 +51,21 @@ 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',
});
allSourcesContent = JSON.stringify(allSourcesContent);
allSourcesContent = prettier.format(allSourcesContent, {
parser: 'json5',
quoteProps: 'preserve',
trailingComma: 'none',
});
fs.writeFile(
path.resolve(process.cwd(), 'src/sources/sources.json'),
allSourcesContent,
+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 [];
`
-47
View File
@@ -1,47 +0,0 @@
import React from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import { IconButton } from 'react-native-paper';
export const Actionbar = ({ active, actions, theme, style }) => {
if (active) {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
style,
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
),
)}
</View>
);
} else {
return null;
}
};
const styles = StyleSheet.create({
actionbarContainer: {
position: 'absolute',
width: Dimensions.get('window').width - 32,
bottom: 0,
margin: 16,
borderRadius: 6,
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingVertical: 4,
elevation: 2,
},
});
+88
View File
@@ -0,0 +1,88 @@
import { useTheme } from '@hooks/useTheme';
import React from 'react';
import {
Dimensions,
Pressable,
StyleProp,
StyleSheet,
View,
ViewProps,
} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import color from 'color';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { overlay } from 'react-native-paper';
type Action = {
icon: string;
onPress: () => void;
};
interface ActionbarProps {
active: boolean;
actions: Action[];
viewStyle?: StyleProp<ViewProps>;
}
export const Actionbar: React.FC<ActionbarProps> = ({
active,
actions,
viewStyle,
}) => {
const theme = useTheme();
const { bottom } = useSafeAreaInsets();
if (active) {
return (
<View
style={[
styles.actionbarContainer,
{
backgroundColor: theme.isDark
? overlay(2, theme.surface)
: theme.secondaryContainer,
minHeight: 80 + bottom,
paddingBottom: bottom,
},
viewStyle,
]}
>
{actions.map(({ icon, onPress }, id) => (
<Pressable
key={id}
android_ripple={{
radius: 50,
color: color(theme.primary).alpha(0.12).string(),
borderless: true,
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={theme.onSurface}
size={24}
/>
</Pressable>
))}
</View>
);
} else {
return null;
}
};
const styles = StyleSheet.create({
actionbarContainer: {
position: 'absolute',
width: Dimensions.get('window').width,
bottom: 0,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
elevation: 1,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
},
});
-30
View File
@@ -1,30 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Appbar as MaterialAppbar } from 'react-native-paper';
import { useTheme } from '../hooks/reduxHooks';
import { ViewPropTypes } from 'react-native';
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useTheme();
return (
<MaterialAppbar.Header
style={[{ backgroundColor: theme.colorPrimary }, style]}
>
{onBackAction && <MaterialAppbar.BackAction onPress={onBackAction} />}
<MaterialAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</MaterialAppbar.Header>
);
};
Appbar.propTypes = {
title: PropTypes.string,
onBackAction: PropTypes.func,
style: ViewPropTypes.style,
};
+19 -5
View File
@@ -2,24 +2,38 @@ 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 { MD3ThemeType } from '../../theme/types';
interface AppbarProps {
title: string;
handleGoBack: () => void;
theme: ThemeTypeV1;
theme: MD3ThemeType;
mode?: 'small' | 'medium' | 'large' | 'center-aligned';
}
const Appbar: React.FC<AppbarProps> = ({ title, handleGoBack, theme }) => (
const Appbar: React.FC<AppbarProps> = ({
title,
handleGoBack,
theme,
mode = 'large',
children,
}) => (
<PaperAppbar.Header
style={{ backgroundColor: theme.colorPrimary }}
style={{ backgroundColor: theme.surface }}
statusBarHeight={StatusBar.currentHeight}
mode={mode}
>
{handleGoBack && <PaperAppbar.BackAction onPress={handleGoBack} />}
{handleGoBack && (
<PaperAppbar.BackAction
onPress={handleGoBack}
iconColor={theme.textColorPrimary}
/>
)}
<PaperAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</PaperAppbar.Header>
);
-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;
+41 -23
View File
@@ -1,7 +1,16 @@
import { Pressable, StyleSheet, Text, View } from 'react-native';
import React from 'react';
import {
Pressable,
StyleProp,
StyleSheet,
Text,
TextStyle,
View,
ViewStyle,
} from 'react-native';
import React, { useMemo } from 'react';
import color from 'color';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { MD3ThemeType } from '../../theme/types';
export enum ButtonVariation {
DEFAULT = 'default',
@@ -14,50 +23,59 @@ interface Props {
title: string;
textColor?: string;
onPress?: () => void;
margin?: number;
theme: ThemeTypeV1;
theme: MD3ThemeType;
style?: ViewStyle;
labelStyle?: StyleProp<TextStyle>;
disabled?: boolean;
}
const Button: React.FC<Props> = ({
variation = ButtonVariation.DEFAULT,
title,
textColor,
margin = 0,
onPress,
theme,
style,
disabled,
labelStyle,
}) => {
const textStyles: StyleProp<TextStyle> = useMemo(
() => ({
color: disabled
? theme.textColorHint
: textColor
? textColor
: variation === ButtonVariation.OUTLINED
? theme.primary
: variation === ButtonVariation.CLEAR
? theme.primary
: theme.onSecondary,
}),
[],
);
return (
<View
style={[
styles.button,
{ margin },
variation === 'outlined'
variation === ButtonVariation.OUTLINED
? {
...styles.outlined,
borderColor: theme.textColorHint,
}
: variation !== 'clear' && {
backgroundColor: theme.colorAccent,
: variation !== ButtonVariation.CLEAR && {
backgroundColor: theme.secondary,
},
style,
]}
>
<Pressable
style={styles.pressable}
onPress={onPress}
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
disabled={disabled}
>
<Text
style={{
color: textColor
? textColor
: variation === 'outlined'
? theme.colorAccent
: variation === 'default'
? theme.colorButtonText
: theme.textColorPrimary,
}}
numberOfLines={1}
>
<Text style={[textStyles, labelStyle]} numberOfLines={1}>
{title}
</Text>
</Pressable>
@@ -1,13 +1,40 @@
import React from 'react';
import { Pressable, StyleSheet, Text } from 'react-native';
import {
Pressable,
StyleProp,
StyleSheet,
Text,
TextStyle,
ViewStyle,
} from 'react-native';
import { Checkbox as PaperCheckbox } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import color from 'color';
export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
import { MD3ThemeType } from '../../theme/types';
interface CheckboxProps {
label: string;
status: boolean | 'indeterminate';
onPress?: () => void;
disabled?: boolean;
theme: MD3ThemeType;
viewStyle?: StyleProp<ViewStyle>;
labelStyle?: StyleProp<TextStyle>;
}
export const Checkbox: React.FC<CheckboxProps> = ({
label,
status,
theme,
disabled,
onPress,
viewStyle,
labelStyle,
}) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={[styles.pressable, viewStyle]}
onPress={onPress}
disabled={disabled}
>
@@ -20,11 +47,14 @@ export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
: 'unchecked'
}
onPress={onPress}
color={theme.colorAccent}
color={theme.primary}
theme={{
colors: { disabled: theme.textColorSecondary },
}}
uncheckedColor={theme.textColorSecondary}
disabled={disabled}
/>
<Text style={{ color: theme.textColorPrimary, marginLeft: 12 }}>
<Text style={[styles.defaultLabel, { color: theme.onSurface }, labelStyle]}>
{label}
</Text>
</Pressable>
@@ -32,19 +62,19 @@ export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
export const SortItem = ({ label, status, onPress, theme }) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={[styles.pressable, { paddingVertical: 16, paddingLeft: 64 }]}
onPress={onPress}
>
{status && (
<MaterialCommunityIcons
name={status === 'asc' ? 'arrow-up' : 'arrow-down'}
color={theme.colorAccent}
color={theme.primary}
size={21}
style={styles.icon}
/>
)}
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
<Text style={{ color: theme.onSurface }}>{label}</Text>
</Pressable>
);
@@ -60,4 +90,7 @@ const styles = StyleSheet.create({
left: 24,
alignSelf: 'center',
},
defaultLabel: {
marginLeft: 12,
},
});
+24 -6
View File
@@ -1,20 +1,38 @@
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import color from 'color';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { MD3ThemeType } from '../../theme/types';
import { overlay } from 'react-native-paper';
interface ChipProps {
label: string;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
<View style={[styles.chipContainer, { backgroundColor: theme.colorPrimary }]}>
<View
style={[
styles.chipContainer,
{
backgroundColor: theme.isDark
? overlay(1, theme.surface)
: theme.secondaryContainer,
},
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={styles.pressable}
>
<Text style={[styles.label, { color: theme.textColorSecondary }]}>
<Text
style={[
styles.label,
{
color: theme.isDark ? theme.onSurface : theme.onSecondaryContainer,
},
]}
>
{label}
</Text>
</Pressable>
@@ -27,7 +45,7 @@ const styles = StyleSheet.create({
chipContainer: {
overflow: 'hidden',
borderRadius: 8,
height: 28,
height: 32,
marginRight: 8,
},
pressable: {
@@ -1,48 +1,52 @@
import React, { useState } from 'react';
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
import { Modal, Portal, TextInput } from 'react-native-paper';
import { Modal, overlay, Portal, TextInput } from 'react-native-paper';
import { MD3ThemeType } from '../../theme/types';
const ColorPickerModal = ({
interface ColorPickerModalProps {
visible: boolean;
title: string;
color: string;
onSubmit: (val: string) => void;
closeModal: () => void;
theme: MD3ThemeType;
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');
}
};
const textInputTheme = {
colors: {
primary: color,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: 'transparent',
},
};
const accentColors = [
'#EF5350',
'#EC407A',
@@ -69,17 +73,17 @@ const ColorPickerModal = ({
return (
<Portal>
<Modal
visible={modalVisible}
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: theme.colorPrimary },
{ backgroundColor: overlay(2, theme.surface) },
]}
>
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
{title}
</Text>
{showAccentColors && (
{showAccentColors ? (
<FlatList
contentContainerStyle={{ marginBottom: 8 }}
data={accentColors}
@@ -104,24 +108,24 @@ 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}
mode="outlined"
theme={textInputTheme}
theme={{ colors: { ...theme } }}
underlineColor={theme.textColorHint}
dense
error={error}
error={Boolean(error)}
/>
<Text style={styles.errorText}>{error}</Text>
</Modal>
@@ -134,11 +138,11 @@ export default ColorPickerModal;
const styles = StyleSheet.create({
modalContainer: {
margin: 30,
padding: 20,
borderRadius: 8,
padding: 24,
borderRadius: 28,
},
modalTitle: {
fontSize: 18,
fontSize: 24,
marginBottom: 16,
},
errorText: {
@@ -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,
},
});
-64
View File
@@ -1,64 +0,0 @@
import React from 'react';
import { Pressable, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
<View
style={{
borderRadius: 8,
overflow: 'hidden',
marginHorizontal: 4,
}}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 10,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
);
export const ToggleColorButton = ({
selected,
backgroundColor,
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: 'hidden',
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={onPress}
>
{selected && (
<MaterialCommunityIcons name="check" color={textColor} size={24} />
)}
</Pressable>
</View>
);
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import { Pressable, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { MD3ThemeType } from '../../theme/types';
import Color from 'color';
interface ToggleButtonProps {
icon: string;
selected: boolean;
theme: MD3ThemeType;
color?: string;
onPress: () => void;
}
export const ToggleButton: React.FC<ToggleButtonProps> = ({
icon,
selected,
theme,
color,
onPress,
}) => (
<View
style={{
borderRadius: 6,
overflow: 'hidden',
marginHorizontal: 6,
}}
>
<Pressable
android_ripple={{ color: Color(theme.primary).alpha(0.12).string() }}
style={{
padding: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: selected
? Color(theme.primary).alpha(0.12).string()
: 'transparent',
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={
selected ? theme.primary : color ? color : theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
);
interface ToggleColorButtonProps {
selected: boolean;
backgroundColor: string;
textColor: string;
onPress: () => void;
}
export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
selected,
backgroundColor,
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: 'hidden',
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={selected ? 'check' : 'format-color-text'}
color={textColor}
size={24}
/>
</Pressable>
</View>
);
@@ -0,0 +1,78 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { getString } from '@strings/translations';
import { Dialog, Portal } from 'react-native-paper';
import { MD3ThemeType } from '../../theme/types';
import { Button } from '@components/index';
import { ButtonVariation } from '@components/Button/Button';
import { getDialogBackground } from '@theme/colors';
interface ConfirmationDialogProps {
title: string;
visible: boolean;
theme: MD3ThemeType;
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: getDialogBackground(theme) },
]}
>
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
{title}
</Dialog.Title>
<View style={styles.buttonCtn}>
<Button
theme={theme}
onPress={handleOnSubmit}
title={getString('common.ok')}
variation={ButtonVariation.CLEAR}
/>
<Button
theme={theme}
onPress={onDismiss}
title={getString('common.cancel')}
variation={ButtonVariation.CLEAR}
/>
</View>
</Dialog>
</Portal>
);
};
export default ConfirmationDialog;
const styles = StyleSheet.create({
container: {
borderRadius: 28,
},
title: {
letterSpacing: 0,
fontSize: 16,
},
buttonCtn: {
flexDirection: 'row-reverse',
padding: 16,
},
});
-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,9 +1,9 @@
import { useTheme } from '@hooks/useTheme';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useSelector } from 'react-redux';
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
const theme = useSelector(state => state.settingsReducer.theme);
const theme = useTheme();
return (
<View style={styles.emptyViewContainer}>
+3 -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 { MD3ThemeType } from '../../theme/types';
interface EmptyViewProps {
icon?: string;
description: string;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}
const EmptyView: React.FC<EmptyViewProps> = ({ icon, description, theme }) => (
@@ -25,6 +25,7 @@ export default EmptyView;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
alignItems: 'center',
justifyContent: 'center',
},
+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 { MD3ThemeType } from '@theme/types';
interface ErrorScreenProps {
error?: string;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}
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;
+8 -3
View File
@@ -1,8 +1,9 @@
import React from 'react';
import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import Color from 'color';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { MD3ThemeType } from '../../theme/types';
type Props = {
name: string;
@@ -11,7 +12,7 @@ type Props = {
disabled?: boolean;
padding?: number;
onPress?: () => void;
theme: ThemeTypeV1;
theme: MD3ThemeType;
style?: ViewStyle;
};
@@ -30,7 +31,11 @@ const IconButton: React.FC<Props> = ({
style={[styles.pressable, { padding }]}
onPress={onPress}
disabled={disabled}
android_ripple={{ color: theme.rippleColor }}
android_ripple={
onPress
? { color: Color(theme.primary).alpha(0.12).string() }
: undefined
}
>
<MaterialCommunityIcons
name={name}
+16 -8
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import color from 'color';
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
@@ -9,9 +10,7 @@ const ListSection = ({ children }) => (
);
const ListSubHeader = ({ children, theme }) => (
<PaperList.Subheader
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
<PaperList.Subheader style={[styles.listSubHeader, { color: theme.primary }]}>
{children}
</PaperList.Subheader>
);
@@ -38,7 +37,7 @@ const ListItem = ({
<View style={{ justifyContent: 'center' }}>
{icon && (
<PaperList.Icon
color={theme.colorAccent}
color={theme.primary}
icon={icon}
style={{
marginVertical: 0,
@@ -58,12 +57,20 @@ const ListItem = ({
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
rippleColor={color(theme.primary).alpha(0.12).string()}
/>
);
const Divider = ({ theme }) => (
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
<PaperDivider
style={{
backgroundColor: color(theme.isDark ? '#FFFFFF' : '#000000')
.alpha(0.12)
.rgb()
.string(),
height: 1,
}}
/>
);
const InfoItem = ({ title, icon, theme }) => (
@@ -85,7 +92,7 @@ const ColorItem = ({ title, description, theme, onPress }) => (
justifyContent: 'space-between',
alignItems: 'center',
}}
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
onPress={onPress}
>
<View>
@@ -107,7 +114,7 @@ const ColorItem = ({ title, description, theme, onPress }) => (
);
const Icon = ({ icon, theme }) => (
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{ margin: 0 }} />
<PaperList.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
);
export const List = {
@@ -126,6 +133,7 @@ const styles = StyleSheet.create({
marginVertical: 0,
},
listSubHeader: {
fontWeight: 'bold',
paddingBottom: 5,
},
});
+11 -9
View File
@@ -1,15 +1,17 @@
import React, { ReactNode } from 'react';
import { StyleSheet, View } from 'react-native';
import color from 'color';
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { MD3ThemeType } from '../../theme/types';
import { dividerColor } from '../../theme/colors';
interface ListItemProps {
title: string;
description?: string | null;
icon?: string;
onPress?: () => void;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}
const Section = ({ children }: { children: ReactNode }) => (
@@ -21,9 +23,9 @@ const SubHeader = ({
theme,
}: {
children: ReactNode;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}) => (
<PaperList.Subheader style={{ color: theme.colorAccent }}>
<PaperList.Subheader style={{ color: theme.primary }}>
{children}
</PaperList.Subheader>
);
@@ -44,7 +46,7 @@ const Item: React.FC<ListItemProps> = ({
<View style={styles.iconContainer}>
{icon && (
<PaperList.Icon
color={theme.colorAccent}
color={theme.primary}
icon={icon}
style={styles.icon}
/>
@@ -52,13 +54,13 @@ const Item: React.FC<ListItemProps> = ({
</View>
)}
onPress={onPress}
rippleColor={theme.rippleColor}
rippleColor={color(theme.primary).alpha(0.12).string()}
/>
);
const Divider = ({ theme }: { theme: ThemeTypeV1 }) => (
const Divider = ({ theme }: { theme: MD3ThemeType }) => (
<PaperDivider
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
style={[styles.divider, { backgroundColor: dividerColor(theme.isDark) }]}
/>
);
@@ -69,7 +71,7 @@ const InfoItem = ({
}: {
title: string;
icon: string;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}) => (
<PaperList.Item
title={title}
+6 -2
View File
@@ -5,6 +5,8 @@ import FastImage from 'react-native-fast-image';
import { defaultCoverUri } from '../sources/helpers/constants';
import { coverPlaceholderColor } from '../theme/colors';
import color from 'color';
const ListView = ({
item,
downloadBadge,
@@ -17,10 +19,12 @@ const ListView = ({
}) => {
return (
<Pressable
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={[
styles.listView,
isSelected && { backgroundColor: theme.rippleColor },
isSelected && {
backgroundColor: color(theme.primary).alpha(0.12).string(),
},
]}
onPress={onPress}
onLongPress={onLongPress}
@@ -0,0 +1,22 @@
import React from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { MD3ThemeType } from '../../theme/types';
interface Props {
theme: MD3ThemeType;
}
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
<ActivityIndicator color={theme.primary} style={styles.indicator} />
);
export default LoadingMoreIndicator;
const styles = StyleSheet.create({
indicator: {
padding: 32,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
@@ -1,8 +0,0 @@
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
export const LoadingScreen = ({ theme }) => (
<View style={{ flex: 1, justifyContent: 'center' }}>
<ActivityIndicator size={50} color={theme.colorAccent} />
</View>
);
@@ -1,17 +1,10 @@
import React from 'react';
import { StyleSheet, ActivityIndicator } from 'react-native';
import { Container } from '..';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { MD3ThemeType } 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: MD3ThemeType }> = ({ theme }) => (
<ActivityIndicator size={50} color={theme.primary} style={styles.indicator} />
);
export default LoadingScreen;
-75
View File
@@ -1,75 +0,0 @@
import React, { useState } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Portal, Modal, Button } from 'react-native-paper';
import * as Linking from 'expo-linking';
import { useTheme } from '../hooks/reduxHooks';
const NewUpdateDialog = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
{`New update available ${newVersion.tag_name}`}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
{newVersion.body}
</Text>
<View
style={{
flexDirection: 'row',
marginTop: 30,
justifyContent: 'flex-end',
}}
>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => showNewUpdateDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
>
Install
</Button>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontSize: 20,
marginBottom: 10,
},
});
+98
View File
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { StyleSheet, View, Text, Dimensions } from 'react-native';
import { Portal, Modal } from 'react-native-paper';
import * as Linking from 'expo-linking';
import { ScrollView } from 'react-native-gesture-handler';
import Button, { ButtonVariation } from './Button/Button';
import { getString } from '@strings/translations';
import Markdown from 'react-native-markdown-display';
import { dividerColor, getDialogBackground } from '@theme/colors';
import { useTheme } from '@hooks/useTheme';
interface NewUpdateDialogProps {
newVersion: {
tag_name: string;
body: string;
downloadUrl: string;
};
}
const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
const modalHeight = Dimensions.get('window').height / 2;
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: getDialogBackground(theme) },
]}
>
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
</Text>
<ScrollView style={{ height: modalHeight }}>
<Markdown
style={{
body: {
color: theme.textColorSecondary,
lineHeight: 20,
paddingVertical: 8,
},
hr: {
backgroundColor: dividerColor(theme.isDark),
marginVertical: 16,
},
code_inline: {
backgroundColor: getDialogBackground(theme),
},
}}
>
{newVersion.body}
</Markdown>
</ScrollView>
<View style={styles.buttonCtn}>
<Button
title={getString('common.cancel')}
theme={theme}
variation={ButtonVariation.CLEAR}
onPress={() => showNewUpdateDialog(false)}
/>
<Button
title={getString('common.install')}
theme={theme}
variation={ButtonVariation.CLEAR}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
/>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontWeight: 'bold',
fontSize: 20,
marginBottom: 16,
},
body: {},
buttonCtn: {
flexDirection: 'row',
marginTop: 16,
justifyContent: 'flex-end',
},
});
+38 -27
View File
@@ -1,4 +1,4 @@
import React, { memo, useCallback } from 'react';
import React, { memo, useMemo } from 'react';
import {
StyleSheet,
View,
@@ -6,16 +6,16 @@ import {
useWindowDimensions,
Pressable,
} from 'react-native';
import color from 'color';
import { LinearGradient } from 'expo-linear-gradient';
import FastImage from 'react-native-fast-image';
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';
import { useLibrarySettings } from '@hooks/useSettings';
import { DisplayModes } from '@screens/library/constants/constants';
const NovelCover = ({
item,
@@ -26,43 +26,48 @@ const NovelCover = ({
onLongPress,
selectedNovels,
}) => {
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
const {
displayMode = DisplayModes.Comfortable,
showDownloadBadges = true,
showUnreadBadges = true,
novelsPerRow = 3,
} = useLibrarySettings();
const window = useWindowDimensions();
const orientation = useDeviceOrientation();
const getNovelsPerRow = () =>
orientation === 'landscape' ? 6 : novelsPerRow;
const numColumns = useMemo(
() => (orientation === 'landscape' ? 6 : novelsPerRow),
[orientation, novelsPerRow],
);
const getHeight = useCallback(
() => (window.width / getNovelsPerRow()) * (4 / 3),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
const coverHeight = useMemo(
() => (window.width / numColumns) * (4 / 3),
[numColumns],
);
const selectNovel = () => onLongPress && onLongPress(item.novelId);
const uri = item.novelCover;
return displayMode !== 2 ? (
return displayMode !== DisplayModes.List ? (
<View
style={[
{
flex: 1 / getNovelsPerRow(),
flex: 1 / numColumns,
borderRadius: 6,
overflow: 'hidden',
margin: 2,
},
isSelected && {
backgroundColor: theme.colorAccent,
backgroundColor: theme.primary,
opacity: 0.8,
},
]}
>
<Pressable
android_ripple={{ color: theme.colorAccent }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={styles.opac}
onPress={
selectedNovels && selectedNovels.length > 0 ? selectNovel : onPress
@@ -76,6 +81,7 @@ const NovelCover = ({
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
theme={theme}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
@@ -91,7 +97,7 @@ const NovelCover = ({
source={{ uri }}
style={[
{
height: getHeight(),
height: coverHeight,
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
@@ -99,9 +105,11 @@ const NovelCover = ({
]}
/>
<View style={styles.compactTitleContainer}>
{displayMode === 0 && <CompactTitle novelName={item.novelName} />}
{displayMode === DisplayModes.Compact && (
<CompactTitle novelName={item.novelName} />
)}
</View>
{displayMode === 1 && (
{displayMode === DisplayModes.Comfortable && (
<ComfortableTitle novelName={item.novelName} theme={theme} />
)}
</Pressable>
@@ -113,6 +121,7 @@ const NovelCover = ({
showDownloadBadges &&
item.chaptersDownloaded && (
<DownloadBadge
theme={theme}
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
@@ -187,8 +196,8 @@ const InLibraryBadge = ({ theme }) => (
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
backgroundColor: theme.primary,
color: theme.onPrimary,
borderRadius: 4,
},
]}
@@ -214,8 +223,8 @@ const UnreadBadge = ({
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
backgroundColor: theme.primary,
color: theme.onPrimary,
},
]}
>
@@ -227,6 +236,7 @@ const DownloadBadge = ({
chaptersDownloaded,
showUnreadBadges,
chaptersUnread,
theme,
}) => (
<Text
style={[
@@ -238,6 +248,10 @@ const DownloadBadge = ({
!showUnreadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.tertiary,
color: theme.onTertiary,
},
]}
>
{chaptersDownloaded}
@@ -277,16 +291,13 @@ const styles = StyleSheet.create({
},
downloadBadge: {
backgroundColor: '#2979FF',
color: '#FFFFFF',
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
paddingHorizontal: 5,
fontSize: 12,
},
unreadBadge: {
color: '#FFFFFF',
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
-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,
},
});
-61
View File
@@ -1,61 +0,0 @@
import React, { useCallback } from 'react';
import { StyleSheet, FlatList } from 'react-native';
import { useSettings } from '../hooks/reduxHooks';
import { useDeviceOrientation } from '../services/utils/helpers';
const NovelList = ({
data,
onScroll,
onEndReached,
renderItem,
refreshControl,
ListEmptyComponent,
ListFooterComponent,
}) => {
const { displayMode, novelsPerRow } = useSettings();
const orientation = useDeviceOrientation();
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
if (orientation === 'landscape') {
return 6;
} else {
return novelsPerRow;
}
};
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
return (
<FlatList
contentContainerStyle={[
styles.flatListCont,
displayMode !== 2 && { paddingHorizontal: 4 },
]}
numColumns={getNovelsPerRow()}
key={[orientation, getNovelsPerRow()]}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
refreshControl={refreshControl}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onScroll={onScroll}
onEndReached={onEndReached}
/>
);
};
export default NovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingBottom: 56,
},
});
+67
View File
@@ -0,0 +1,67 @@
import { useLibrarySettings } from '@hooks/useSettings';
import { DisplayModes } from '@screens/library/constants/constants';
import React, { useCallback, useMemo } from 'react';
import {
StyleSheet,
FlatList,
FlatListProps,
ListRenderItem,
} from 'react-native';
import { SourceNovelItem } from '../sources/types';
import { LibraryNovelInfo, NovelInfo } from '../database/types';
import { useDeviceOrientation } from '../services/utils/helpers';
export type NovelListRenderItem = ListRenderItem<
LibraryNovelInfo | NovelInfo | SourceNovelItem
>;
const NovelList: React.FC<
FlatListProps<LibraryNovelInfo | NovelInfo | SourceNovelItem>
> = props => {
const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } =
useLibrarySettings();
const orientation = useDeviceOrientation();
const isListView = displayMode === DisplayModes.List;
const numColumns = useMemo(() => {
if (isListView) {
return 1;
}
if (orientation === 'landscape') {
return 6;
} else {
return novelsPerRow;
}
}, [isListView, orientation, novelsPerRow]);
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
return (
<FlatList
contentContainerStyle={[
!isListView && styles.listView,
styles.flatListCont,
]}
numColumns={numColumns}
key={numColumns}
keyExtractor={keyExtractor}
{...props}
/>
);
};
export default NovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingBottom: 56,
},
listView: {
paddingHorizontal: 4,
},
});
-40
View File
@@ -1,40 +0,0 @@
import React from 'react';
import { FlatList, FlatListProps, StyleSheet } from 'react-native';
import { useAppearanceSettings } from '../../redux/hooks';
import { useDeviceOrientation } from '../../hooks';
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
import { SourceNovelItem } from '../../sources/types';
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
const NovelList: React.FC<
FlatListProps<SourceNovelItem | NovelInfo | LibraryNovelInfo>
> = props => {
const orientation = useDeviceOrientation();
const { novelsPerRowPotrait, novelsPerRowLandscape } =
useAppearanceSettings();
return (
<FlatList
contentContainerStyle={styles.container}
numColumns={
orientation === DeviceOrientation.LANDSCAPE
? novelsPerRowLandscape
: novelsPerRowPotrait
}
key={[orientation, novelsPerRowPotrait] as any}
{...props}
/>
);
};
export default NovelList;
const styles = StyleSheet.create({
container: {
flexGrow: 1,
padding: 4,
},
});
+1 -1
View File
@@ -12,7 +12,7 @@ export const RadioButton = ({ value, label, theme, labelStyle }) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
color={theme.colorAccent}
color={theme.primary}
uncheckedColor={theme.textColorSecondary}
/>
<Text
@@ -1,8 +1,26 @@
import React from 'react';
import { Pressable, StyleSheet, Text } from 'react-native';
import {
Pressable,
StyleProp,
StyleSheet,
Text,
TextStyle,
ViewStyle,
} from 'react-native';
import { RadioButton as PaperRadioButton } from 'react-native-paper';
import { MD3ThemeType } from '../../theme/types';
import color from 'color';
export const RadioButton = ({
interface Props {
label: string;
status: boolean;
onPress?: () => void;
style?: StyleProp<ViewStyle>;
labelStyle?: StyleProp<TextStyle>;
theme: MD3ThemeType;
}
export const RadioButton: React.FC<Props> = ({
label,
status,
onPress,
@@ -11,19 +29,18 @@ export const RadioButton = ({
theme,
}) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={[styles.pressable, style]}
onPress={onPress}
>
<PaperRadioButton
status={status ? 'checked' : 'unchecked'}
value={label}
onPress={onPress}
color={theme.colorAccent}
color={theme.primary}
uncheckedColor={theme.textColorSecondary}
/>
<Text
style={[labelStyle, { color: theme.textColorPrimary, marginLeft: 12 }]}
>
<Text style={[styles.label, labelStyle, { color: theme.onSurface }]}>
{label}
</Text>
</Pressable>
@@ -41,4 +58,7 @@ const styles = StyleSheet.create({
left: 24,
alignSelf: 'center',
},
label: {
marginLeft: 12,
},
});
-96
View File
@@ -1,96 +0,0 @@
import React, { useRef } from 'react';
import {
StyleSheet,
View,
TextInput,
StatusBar,
Pressable,
} from 'react-native';
import { Row } from '../Common';
import { IconButton } from '../IconButton/IconButton';
export const Searchbar = ({
theme,
placeholder,
searchText,
backAction,
onBackAction,
clearSearchbar,
onChangeText,
onSubmitEditing,
actions,
}) => {
const searchRef = useRef(null);
return (
<View style={styles.searchBar}>
<Pressable
onPress={() => searchRef.current.focus()}
style={[styles.pressable, { backgroundColor: theme.searchBarColor }]}
android_ripple={{ color: theme.rippleColor }}
>
<Row style={{ flex: 1 }}>
{backAction && (
<IconButton
icon={backAction}
color={theme.textColorPrimary}
onPress={onBackAction}
theme={theme}
containerStyle={{ marginLeft: 0 }}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorPrimary,
marginLeft: 4,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
/>
</Row>
{searchText !== '' && (
<IconButton
icon="close"
color={theme.textColorPrimary}
onPress={clearSearchbar}
theme={theme}
/>
)}
{actions &&
actions.map((action, index) => (
<IconButton
key={index}
icon={action.icon}
color={action.color ?? theme.textColorPrimary}
onPress={action.onPress}
theme={theme}
containerStyle={{ marginRight: 0, marginLeft: 16 }}
/>
))}
</Pressable>
</View>
);
};
const styles = StyleSheet.create({
searchBar: {
marginTop: StatusBar.currentHeight + 8,
marginBottom: 8,
marginHorizontal: 16,
height: 46,
borderRadius: 50,
overflow: 'hidden',
},
pressable: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
});
+15 -11
View File
@@ -1,9 +1,10 @@
import React, { useRef } from 'react';
import { Pressable, StyleSheet, View, TextInput } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import color from 'color';
import { IconButtonV2 } from '..';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import IconButtonV2 from '../IconButtonV2/IconButtonV2';
import { MD3ThemeType } from '../../theme/types';
interface RightIcon {
iconName: string;
@@ -21,7 +22,7 @@ interface SearcbarProps {
handleBackAction?: () => void;
clearSearchbar: () => void;
onLeftIconPress?: () => void;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}
const Searchbar: React.FC<SearcbarProps> = ({
@@ -46,17 +47,20 @@ const Searchbar: React.FC<SearcbarProps> = ({
<View
style={[
styles.searchbarContainer,
{ marginTop, backgroundColor: theme.searchBarColor },
{
marginTop,
backgroundColor: color(theme.primary).alpha(0.08).string(),
},
]}
>
<Pressable
onPress={focusSearchbar}
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(12).toString() }}
style={[styles.searchbar]}
>
<IconButtonV2
name={handleBackAction ? 'arrow-left' : leftIcon}
color={theme.textColorPrimary}
color={theme.onSurface}
onPress={() => {
if (handleBackAction) {
handleBackAction();
@@ -69,9 +73,9 @@ const Searchbar: React.FC<SearcbarProps> = ({
<TextInput
ref={searchbarRef}
style={[styles.textInput, { color: theme.textColorPrimary }]}
style={[styles.textInput, { color: theme.onSurface }]}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
placeholderTextColor={theme.onSurface}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
@@ -79,7 +83,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
{searchText !== '' ? (
<IconButtonV2
name="close"
color={theme.textColorPrimary}
color={theme.onSurface}
onPress={clearSearchbar}
theme={theme}
/>
@@ -88,7 +92,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
<IconButtonV2
key={index}
name={icon.iconName}
color={icon.color || theme.textColorPrimary}
color={icon.color || theme.onSurface}
onPress={icon.onPress}
theme={theme}
/>
@@ -103,7 +107,7 @@ export default Searchbar;
const styles = StyleSheet.create({
searchbarContainer: {
marginHorizontal: 16,
marginBottom: 16,
marginBottom: 12,
minHeight: 46,
borderRadius: 50,
overflow: 'hidden',
+4 -8
View File
@@ -1,21 +1,17 @@
import React from 'react';
import { Pressable, StyleSheet, View, Text } from 'react-native';
import color from 'color';
import { Switch, List } from 'react-native-paper';
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).rgb().string() }}
style={[styles.container, { paddingVertical: description ? 16 : 12 }]}
onPress={onPress}
>
<View style={{ flexDirection: 'row', flex: 1 }}>
{icon && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{ margin: 0 }}
/>
<List.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
)}
<View style={[icon && { marginLeft: 16 }]}>
<Text
@@ -36,7 +32,7 @@ const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
<Switch
value={value}
onValueChange={onPress}
color={theme.colorAccent}
color={theme.primary}
style={{ marginLeft: 8 }}
/>
</Pressable>
+6 -5
View File
@@ -1,15 +1,16 @@
import React from 'react';
import { Pressable, StyleSheet, View, Text } from 'react-native';
import color from 'color';
import { Switch } from 'react-native-paper';
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
import { MD3ThemeType } from '../../theme/types';
interface SwitchItemProps {
value: boolean;
label: string;
description?: string;
onPress: () => void;
theme: ThemeTypeV1;
theme: MD3ThemeType;
}
const SwitchItem: React.FC<SwitchItemProps> = ({
@@ -20,7 +21,7 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
value,
}) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
style={[styles.container]}
onPress={onPress}
>
@@ -35,7 +36,7 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
<Switch
value={value}
onValueChange={onPress}
color={theme.colorAccent}
color={theme.primary}
style={styles.switch}
/>
</Pressable>
@@ -52,10 +53,10 @@ const styles = StyleSheet.create({
},
labelContainer: {
flex: 1,
justifyContent: 'center',
},
label: {
fontSize: 16,
textAlignVertical: 'center',
},
switch: {
marginLeft: 8,
+11 -9
View File
@@ -1,5 +1,7 @@
import React from 'react';
import { View, Text, /* StyleSheet, */ Pressable } from 'react-native';
import { overlay } from 'react-native-paper';
import color from 'color';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
@@ -12,12 +14,12 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
>
<View
style={{
backgroundColor: theme.colorPrimaryDark,
backgroundColor: theme.background,
borderWidth: 3.6,
borderColor:
currentTheme.id === theme.id
? theme.colorAccent
: currentTheme.colorPrimaryDark,
? theme.primary
: currentTheme.background,
width: 95,
height: 140,
borderRadius: 16,
@@ -29,10 +31,10 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
{currentTheme.id === theme.id && (
<MaterialCommunityIcons
name="check"
color={theme.colorButtonText}
color={theme.onPrimary}
size={15}
style={{
backgroundColor: theme.colorAccent,
backgroundColor: theme.primary,
position: 'absolute',
top: 5,
right: 5,
@@ -45,7 +47,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
<View
style={{
height: 20,
backgroundColor: theme.colorPrimary,
backgroundColor: overlay(2, theme.surface),
elevation: 1,
justifyContent: 'center',
}}
@@ -82,7 +84,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
height: 10,
width: 16,
marginLeft: 4,
backgroundColor: theme.colorAccent,
backgroundColor: theme.primary,
borderRadius: 50,
}}
/>
@@ -114,7 +116,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
left: 0,
right: 0,
height: 24,
backgroundColor: theme.colorPrimary,
backgroundColor: color(theme.primary).alpha(0.08).string(),
justifyContent: 'center',
}}
>
@@ -141,7 +143,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.colorAccent,
backgroundColor: theme.primary,
}}
/>
<View
-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,
};
+4 -1
View File
@@ -4,9 +4,12 @@ export { default as LoadingScreenV2 } from './LoadingScreenV2/LoadingScreenV2';
export { default as Container } from './Container/Container';
export { default as ErrorScreenV2 } from './ErrorScreenV2/ErrorScreenV2';
export { default as EmptyView } from './EmptyView/EmptyView';
export { default as NovelList } from './NovelList/NovelList';
export { default as Chip } from './Chip/Chip';
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';
export { Checkbox } from './Checkbox/Checkbox';
export { RadioButton } from './RadioButton/RadioButton';
+61 -13
View File
@@ -3,6 +3,7 @@ import {
createNovelTableQuery,
createUrlIndexQuery,
createLibraryIndexQuery,
addCategoryColumnQuery,
} from './tables/NovelTable';
import {
createChapterTableQuery,
@@ -15,36 +16,83 @@ import {
} from './tables/HistoryTable';
import { createDownloadTableQuery } from './tables/DownloadTable';
import { createUpdatesTableQuery } from './tables/UpdateTable';
import {
addCategorySortColumnQuery,
createCategoriesTableQuery,
createCategorydIndexQuery,
createDefaultCategoryQuery,
} from './tables/CategoryTable';
import {
dbTxnErrorCallback,
txnErrorCallbackWithoutToast,
} from './utils/helpers';
import { noop } from 'lodash';
const dbName = 'lnreader.db';
const db = SQLite.openDatabase(dbName);
export const createDB = () => {
const createTables = () => {
db.transaction(tx => {
tx.executeSql(createNovelTableQuery);
tx.executeSql(createCategoriesTableQuery, [], () => {
tx.executeSql(
addCategorySortColumnQuery,
undefined,
noop,
txnErrorCallbackWithoutToast,
);
tx.executeSql(
createDefaultCategoryQuery,
undefined,
noop,
txnErrorCallbackWithoutToast,
);
});
tx.executeSql(createNovelTableQuery, [], () => {
tx.executeSql(
addCategoryColumnQuery,
undefined,
noop,
txnErrorCallbackWithoutToast,
);
});
tx.executeSql(createChapterTableQuery);
tx.executeSql(createHistoryTableQuery);
tx.executeSql(createDownloadTableQuery);
tx.executeSql(createUpdatesTableQuery);
});
};
/**
* Indexes
*/
const createIndexes = () => {
db.transaction(tx => {
tx.executeSql(createUrlIndexQuery);
tx.executeSql(createLibraryIndexQuery);
tx.executeSql(createNovelIdIndexQuery);
tx.executeSql(createUnreadChaptersIndexQuery);
tx.executeSql(createChapterIdIndexQuery);
tx.executeSql(createCategorydIndexQuery);
});
};
export const deleteDb = () => {
db.transaction(tx => {
tx.executeSql('DROP TABLE novels');
tx.executeSql('DROP TABLE chapters');
tx.executeSql('DROP TABLE history');
tx.executeSql('DROP TABLE downloads');
tx.executeSql('DROP TABLE updates');
});
export const createDatabase = async () => {
createTables();
createIndexes();
};
/**
* For Testing
*/
export const deleteDatabase = () => {
db.transaction(
tx => {
tx.executeSql('DROP TABLE novels');
tx.executeSql('DROP TABLE chapters');
tx.executeSql('DROP TABLE history');
tx.executeSql('DROP TABLE downloads');
tx.executeSql('DROP TABLE updates');
tx.executeSql('DROP TABLE categories');
},
dbTxnErrorCallback,
noop,
);
};
+97
View File
@@ -0,0 +1,97 @@
import * as SQLite from 'expo-sqlite';
import { noop } from 'lodash';
import { Category } from '../types';
import { txnErrorCallback } from '../utils/helpers';
const db = SQLite.openDatabase('lnreader.db');
const getCategoriesQuery = `
SELECT * FROM categories ORDER BY CASE WHEN id > 1 THEN 1 ELSE 0 END, IFNULL(sort, 9999)
`;
export const getCategoriesFromDb = (): Promise<Category[]> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getCategoriesQuery,
[],
(txObj, { rows }) => resolve((rows as any)._array),
txnErrorCallback,
);
}),
);
};
const createCategoryQuery = `
INSERT INTO categories (name)
VALUES
(?)
`;
export const createCategory = (categoryName: string): void =>
db.transaction(tx =>
tx.executeSql(createCategoryQuery, [categoryName], noop, txnErrorCallback),
);
const deleteCategoryQuery = `
DELETE FROM categories
WHERE
id = ?
`;
export const deleteCategoryById = (categoryId: number): void =>
db.transaction(tx =>
tx.executeSql(deleteCategoryQuery, [categoryId], noop, txnErrorCallback),
);
const updateCategoryQuery = `
UPDATE categories SET name = ? WHERE id = ?
`;
export const updateCategory = (
categoryId: number,
categoryName: string,
): void =>
db.transaction(tx =>
tx.executeSql(
updateCategoryQuery,
[categoryName, categoryId],
noop,
txnErrorCallback,
),
);
const isCategoryNameDuplicateQuery = `
SELECT COUNT(*) as isDuplicate FROM categories WHERE name = ?
`;
export const isCategoryNameDuplicate = (
categoryName: string,
): Promise<boolean> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
isCategoryNameDuplicateQuery,
[categoryName],
(txObj, { rows: { _array } }) =>
resolve(Boolean(_array[0]?.isDuplicate)),
txnErrorCallback,
);
}),
);
};
const updateCategoryOrderQuery = `
UPDATE categories SET sort = ? WHERE id = ?
`;
export const updateCategoryOrderInDb = (categories: Category[]): void =>
db.transaction(tx => {
categories.map(category => {
tx.executeSql(
updateCategoryOrderQuery,
[category.sort, category.id],
noop,
txnErrorCallback,
);
});
});
+240 -90
View File
@@ -1,18 +1,24 @@
/* 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';
import { txnErrorCallback } from '@database/utils/helpers';
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 +27,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 +38,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 +73,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 +82,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 +106,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 +137,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 +155,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 +172,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 +189,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 +206,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 +223,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 +236,9 @@ export const isChapterDownloaded = async chapterId => {
resolve(false);
}
},
(txObj, error) => {
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
}),
@@ -226,55 +248,171 @@ export const isChapterDownloaded = async chapterId => {
const downloadChapterQuery =
'INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)';
export const downloadChapter = async (
sourceId,
novelUrl,
chapterUrl,
chapterId,
) => {
const source = sourceManager(sourceId);
const chapter = await source.parseChapter(novelUrl, chapterUrl);
db.transaction(tx => {
tx.executeSql('UPDATE chapters SET downloaded = 1 WHERE chapterId = ?', [
chapterId,
]);
tx.executeSql(
downloadChapterQuery,
[chapterId, chapter.chapterName, chapter.chapterText],
(txObj, res) => {
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
},
(txObj, error) => {
// console.log('Error ', error)
},
);
});
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;
}
};
export const deleteChapter = async chapterId => {
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: number,
novelUrl: string,
novelId: number,
chapterUrl: string,
chapterId: number,
) => {
try {
const source = sourceManager(sourceId);
const chapter = await source.parseChapter(novelUrl, chapterUrl);
if (chapter.chapterText?.length) {
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, imagedChapterText],
(_txObj, _res) => {
return true;
},
txnErrorCallback,
);
});
} else {
throw new Error("Either chapter is empty or the app couldn't scrape it");
}
} catch (error) {
throw error;
}
};
const deleteDownloadedImages = async (
sourceId: number,
novelId: number,
chapterId: number,
) => {
try {
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]}`);
}
}
}
}
} catch (error) {
showToast(error.message);
}
};
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=?';
await deleteDownloadedImages(sourceId, novelId, chapterId);
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 +426,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 +445,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 +462,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 +482,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 +507,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),
);
});
};
-79
View File
@@ -1,79 +0,0 @@
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
const getLibraryQuery = (sort, filter) => `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 ${filter ? 'AND ' + filter : ''}
${sort ? 'ORDER BY ' + sort : ''}
`;
export const getLibrary = (sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
getLibraryQuery(sort, filter),
null,
(txObj, { rows: { _array } }) => {
// console.log(_array);
resolve(_array);
},
(txObj, error) => {
// console.log('Error ', error);
},
);
}),
);
};
const searchLibraryQuery = (searchText, sort, filter) =>
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' ${
filter ? 'AND ' + filter : ''
}
${sort ? 'ORDER BY ' + sort : ''} `;
export const searchLibrary = (searchText, sort, filter) => {
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
searchLibraryQuery(searchText, sort, filter),
null,
(txObj, { rows: { _array } }) => resolve(_array),
(txObj, error) => {
// console.log('Error ', error);
},
);
});
});
};
+96
View File
@@ -0,0 +1,96 @@
import { LibraryFilter } from '@screens/library/constants/constants';
import * as SQLite from 'expo-sqlite';
import { LibraryNovelInfo, NovelInfo } from '../types';
import { txnErrorCallback } from '../utils/helpers';
const db = SQLite.openDatabase('lnreader.db');
const getLibraryQuery = `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
FROM novels
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
FROM chapters
WHERE chapters.read = 0
GROUP BY chapters.novelId
) AS C
ON novels.novelId = C.novelId
LEFT JOIN (
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
FROM chapters
WHERE chapters.downloaded = 1
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
`;
export const getLibrary = ({
filter,
searchText,
sortOrder,
downloadedOnlyMode,
}: {
sortOrder?: string;
filter?: string;
searchText?: string;
downloadedOnlyMode?: boolean;
}): Promise<LibraryNovelInfo[]> => {
let query = getLibraryQuery;
if (filter) {
query += ` AND ${filter}`;
}
if (downloadedOnlyMode) {
query += ' ' + LibraryFilter.DownloadedOnly;
}
if (searchText) {
query += ` AND novelName LIKE '%${searchText}%'`;
}
if (sortOrder) {
query += ` ORDER BY ${sortOrder}`;
}
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
query,
undefined,
(txObj, { rows: { _array } }) => resolve(_array),
txnErrorCallback,
);
}),
);
};
export const getLibraryNovelsFromDb = (
onlyOngoingNovels?: boolean,
): Promise<NovelInfo[]> => {
let getLibraryNovelsQuery = 'SELECT * FROM novels WHERE followed = 1';
if (onlyOngoingNovels) {
getLibraryNovelsQuery += " AND status NOT LIKE 'Completed'";
}
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getLibraryNovelsQuery,
undefined,
(txObj, { rows: { _array } }) => resolve(_array),
txnErrorCallback,
);
}),
);
};
+5 -4
View File
@@ -8,9 +8,11 @@ import { fetchChapters, fetchNovel } from '../../services/Source/source';
import { insertChapters } from './ChapterQueries';
import { showToast } from '../../hooks/showToast';
import { txnErrorCallback } from '../utils/helpers';
import { noop } from 'lodash';
const insertNovelQuery =
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, categoryIds) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
export const insertNovel = async novel => {
return new Promise((resolve, reject) =>
@@ -29,11 +31,10 @@ export const insertNovel = async novel => {
novel.artist,
novel.status,
novel.genre,
JSON.stringify(novel.categoryIds),
],
(txObj, { insertId }) => resolve(insertId),
(txObj, error) => {
// console.log('Error ', error)
},
txnErrorCallback,
),
),
);
+205
View File
@@ -0,0 +1,205 @@
import { noop, xor } from 'lodash';
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
import { txnErrorCallback } from '../utils/helpers';
import { LibraryNovelInfo } from '../types';
import { fetchNovel } from '../../services/Source/source';
import { showToast } from '@hooks/showToast';
import { getString } from '@strings/translations';
import { checkNovelInCache } from './NovelQueries';
export const getCategoryNovelsFromDb = async (
categoryId: number,
onlyOngoingNovels?: boolean,
): Promise<LibraryNovelInfo[]> => {
let query = `
SELECT
*
FROM
novels
WHERE (
categoryIds LIKE '[${categoryId}]'
OR
categoryIds LIKE '[${categoryId},%'
OR
categoryIds LIKE '%,${categoryId}]'
OR
categoryIds LIKE '%,${categoryId},%'
)
AND
followed = 1
`;
if (onlyOngoingNovels) {
query += ' AND status NOT LIKE "Completed"';
}
return new Promise(resolve => {
db.transaction(tx => {
tx.executeSql(
query,
undefined,
(txObj, { rows }) => resolve((rows as any)._array),
txnErrorCallback,
);
});
});
};
export const resetCategoryIdsToDefault = async (deletedCategoryId: number) => {
const categoryNovels = await getCategoryNovelsFromDb(deletedCategoryId);
db.transaction(tx => {
categoryNovels.forEach(novel => {
let categoryIds = xor(JSON.parse(novel.categoryIds), [deletedCategoryId]);
categoryIds = categoryIds.length ? categoryIds : [1];
tx.executeSql('UPDATE novels SET categoryIds = ? WHERE novelId = ?', [
JSON.stringify(categoryIds),
novel.novelId,
]);
});
});
};
export const insertNovelInLibrary = async (
sourceId: number,
novelUrl: string,
inLibrary: boolean,
defaultCategoryId: number,
) => {
if (inLibrary) {
showToast(getString('browseScreen.removeFromLibrary'));
db.transaction(tx => {
tx.executeSql(
`
UPDATE novels SET
followed = 0
WHERE
sourceId = ?
AND
novelUrl = ?
`,
[sourceId, novelUrl],
noop,
txnErrorCallback,
);
});
} else {
showToast(getString('browseScreen.addedToLibrary'));
const novelSavedInDb = await checkNovelInCache(novelUrl);
if (novelSavedInDb) {
db.transaction(tx => {
tx.executeSql(
`
UPDATE novels SET
followed = 1
WHERE
sourceId = ?
AND
novelUrl = ?
`,
[sourceId, novelUrl],
noop,
txnErrorCallback,
);
});
return;
}
const novel = await fetchNovel(sourceId, novelUrl);
db.transaction(tx => {
tx.executeSql(
`
INSERT INTO novels
(
novelUrl,
sourceUrl,
sourceId,
source,
novelName,
novelCover,
novelSummary,
author,
artist,
status,
genre,
followed,
categoryIds
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
JSON.stringify([defaultCategoryId]),
],
(txObj, { insertId }) => {
novel.chapters?.map(chapter => {
tx.executeSql(
`
INSERT INTO chapters
(chapterUrl, chapterName, releaseDate, novelId)
VALUES
(?, ?, ?, ?)`,
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
insertId,
],
);
});
},
txnErrorCallback,
);
});
}
};
export const updateNovelCategoryById = async (
novelId: number,
categoryIds: number[],
) => {
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
noop,
txnErrorCallback,
);
});
};
export const updateNovelCategoryByIds = async (
novelIds: number[],
categoryIds: number[],
) => {
db.transaction(tx => {
novelIds.map(novelId =>
tx.executeSql(
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
noop,
txnErrorCallback,
),
);
});
};
+1 -1
View File
@@ -35,7 +35,7 @@ const getUpdatesFromDbQuery = `
WHERE
date(updates.updateTime) > date('now','-3 months')
ORDER BY
updates.updateTime DESC
updates.updateTime DESC, chapters.releaseDate DESC
`;
export const getUpdatesFromDb = async (): Promise<Update[]> => {
+17
View File
@@ -0,0 +1,17 @@
export const createCategoriesTableQuery = `
CREATE TABLE IF NOT EXISTS categories(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
lastUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
sort INTEGER DEFAULT NULL
)
`;
export const createDefaultCategoryQuery =
'INSERT INTO categories (id, name) VALUES (1, "Default");';
export const createCategorydIndexQuery =
'CREATE INDEX IF NOT EXISTS categoryIdIndex ON categories(id)';
export const addCategorySortColumnQuery =
'ALTER TABLE categories ADD COLUMN sort INTEGER DEFAULT NULL';
-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
)
`;
+5 -1
View File
@@ -13,7 +13,8 @@ export const createNovelTableQuery = `
status TEXT,
genre TEXT,
followed INTEGER DEFAULT 0,
unread INTEGER DEFAULT 1
unread INTEGER DEFAULT 1,
categoryIds TEXT DEFAULT "[1]"
)`;
export const createUrlIndexQuery =
@@ -21,3 +22,6 @@ export const createUrlIndexQuery =
export const createLibraryIndexQuery =
'CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)';
export const addCategoryColumnQuery =
'ALTER TABLE novels ADD COLUMN categoryIds TEXT DEFAULT "[1]"';
+8
View File
@@ -16,6 +16,7 @@ export interface NovelInfo {
export interface LibraryNovelInfo extends NovelInfo {
chaptersUnread: number;
chaptersDownloaded: number;
categoryIds: string;
}
export interface ChapterItem {
@@ -66,3 +67,10 @@ export interface Update {
bookmark: number;
releaseDate: string;
}
export interface Category {
id: number;
name: string;
lastUpdatedAt: Date;
sort: number | null;
}
+17
View File
@@ -0,0 +1,17 @@
import { showToast } from '@hooks/showToast';
import { SQLError, SQLTransaction } from 'expo-sqlite';
export const txnErrorCallback = (
txnObj: SQLTransaction,
error: SQLError,
): boolean => {
showToast(error.message);
return false;
};
export const txnErrorCallbackWithoutToast = (): boolean => {
return false;
};
export const dbTxnErrorCallback = (error: SQLError): void =>
showToast(error.message);
+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, '');
+4 -2
View File
@@ -4,6 +4,8 @@ export { default as useUpdates } from './useUpdates';
export { default as useFullscreenMode } from './useFullscreenMode';
export { default as useChapter } from './useChapter';
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';
export { useTheme } from './useTheme';
export { default as useCategories } from './useCategories';
export { useCategorySettings, useLibrarySettings } from './useSettings';
-29
View File
@@ -1,17 +1,4 @@
import { useSelector } from 'react-redux';
import { darkTheme } from '../theme/v1/theme/theme';
const useTheme = () => {
const theme = useSelector(state => state.settingsReducer.theme);
return theme || darkTheme;
};
const useReaderSettings = () => {
const readerSettings = useSelector(state => state.settingsReducer.reader);
return readerSettings;
};
const useSettings = () => {
const settings = useSelector(state => state.settingsReducer);
@@ -19,12 +6,6 @@ const useSettings = () => {
return settings;
};
const useLibrary = () => {
const library = useSelector(state => state.libraryReducer.novels);
return library;
};
const useNovel = () => {
const novel = useSelector(state => state.novelReducer);
@@ -110,16 +91,7 @@ const useTrackingStatus = () => {
return tracker;
};
const useLibraryFilters = () => {
const filters = useSelector(state => state.libraryReducer.filters);
return filters;
};
export {
useTheme,
useReaderSettings,
useLibrary,
useSettings,
usePreferences,
useContinueReading,
@@ -127,6 +99,5 @@ export {
useNovel,
useChapter,
useSavedSettings,
useLibraryFilters,
usePosition,
};
+10
View File
@@ -0,0 +1,10 @@
import { useEffect } from 'react';
import { BackHandler } from 'react-native';
export function useBackHandler(handler: () => boolean) {
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handler);
return () => BackHandler.removeEventListener('hardwareBackPress', handler);
}, [handler]);
}
+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;
+35
View File
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
import { Category } from '@database/types';
const useCategories = () => {
const [isLoading, setIsLoading] = useState(true);
const [categories, setCategories] = useState<Category[]>([]);
const [error, setError] = useState<string>();
const getCategories = async () => {
try {
const res = await getCategoriesFromDb();
setCategories(res);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
useEffect(() => {
getCategories();
}, []);
return {
isLoading,
categories,
error,
};
};
export default useCategories;
-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,
};
};
-18
View File
@@ -1,18 +0,0 @@
import { useWindowDimensions } from 'react-native';
export enum DeviceOrientation {
POTRAIT = 'potrait',
LANDSCAPE = 'landscape',
}
const useDeviceOrientation = (): DeviceOrientation => {
const window = useWindowDimensions();
if (window.width > window.height) {
return DeviceOrientation.LANDSCAPE;
} else {
return DeviceOrientation.POTRAIT;
}
};
export default useDeviceOrientation;
+9 -5
View File
@@ -1,29 +1,33 @@
import { useCallback, useEffect } from 'react';
import { StatusBar } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useReaderSettingsV1, useSettingsV1, useThemeV1 } from '../redux/hooks';
import color from 'color';
import { useReaderSettings, useSettingsV1 } from '../redux/hooks';
import {
changeNavigationBarColor,
hideNavigationBar,
showNavigationBar,
} from '../theme/NativeModules/NavigationBarColor';
import { readerBackground } from '../screens/reader/utils/readerStyles';
import { useTheme } from './useTheme';
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) {
StatusBar.setHidden(true);
hideNavigationBar();
} else {
StatusBar.setBarStyle(
color(backgroundColor).isDark() ? 'light-content' : 'dark-content',
);
StatusBar.setBackgroundColor(backgroundColor);
changeNavigationBarColor(backgroundColor as any);
}
@@ -41,7 +45,7 @@ const useFullscreenMode = () => {
useEffect(() => {
const unsubscribe = addListener('beforeRemove', () => {
showStatusAndNavBar();
StatusBar.setBarStyle(theme.statusBar);
StatusBar.setBarStyle(theme.isDark ? 'light-content' : 'dark-content');
});
return unsubscribe;
+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;
-10
View File
@@ -1,10 +0,0 @@
import { useState } from 'react';
export const useModal = () => {
const [visible, setVisible] = useState(false);
const showModal = () => setVisible(true);
const hideModal = () => setVisible(false);
return { visible, showModal, hideModal };
};
-34
View File
@@ -1,34 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useWindowDimensions } from 'react-native';
import { useDeviceOrientation } from '../hooks';
import { DeviceOrientation } from '../hooks/useDeviceOrientation';
import { useAppearanceSettings } from '../redux/hooks';
const useNovelCoverHeight = () => {
const window = useWindowDimensions();
const orientation = useDeviceOrientation();
const { novelsPerRowLandscape, novelsPerRowPotrait } =
useAppearanceSettings();
const [coverHeight, setCoverHeight] = useState<number>();
const getCoverHeight = useCallback(
() =>
(window.width /
(orientation === DeviceOrientation.POTRAIT
? novelsPerRowPotrait
: novelsPerRowLandscape)) *
(4 / 3),
[window.width, novelsPerRowPotrait, novelsPerRowLandscape, orientation],
);
useEffect(() => {
setCoverHeight(getCoverHeight());
}, [getCoverHeight]);
return coverHeight;
};
export default useNovelCoverHeight;
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;
+13 -3
View File
@@ -1,9 +1,19 @@
import { useCallback, useState } from 'react';
import { useIsFocused } from '@react-navigation/native';
import { useCallback, useEffect, useState } from 'react';
const useSearch = (defaultSearchText?: string) => {
const isFocused = useIsFocused();
const [searchText, setSearchText] = useState<string>(defaultSearchText || '');
const useSearch = () => {
const [searchText, setSearchText] = useState<string>('');
const clearSearchbar = useCallback(() => setSearchText(''), []);
useEffect(() => {
if (!isFocused) {
clearSearchbar();
}
}, [isFocused, clearSearchbar]);
return { searchText, setSearchText, clearSearchbar };
};
+28
View File
@@ -0,0 +1,28 @@
import { useMMKVObject } from 'react-native-mmkv';
import { CategorySettings, LibrarySettings } from '../utils/constants/settings';
export const useLibrarySettings = () => {
const [librarySettings, setSettings] =
useMMKVObject<LibrarySettings>('LibrarySettings');
const setLibrarySettings = (value: Partial<LibrarySettings>) =>
setSettings({ ...librarySettings, ...value });
return {
...librarySettings,
setLibrarySettings,
};
};
export const useCategorySettings = () => {
const [categorySettings, setSettings] =
useMMKVObject<CategorySettings>('CategorySettings');
const setCategorySettings = (value: Partial<CategorySettings>) =>
setSettings({ ...categorySettings, ...value });
return {
...categorySettings,
setCategorySettings,
};
};
+35
View File
@@ -0,0 +1,35 @@
import {
useMMKVBoolean,
useMMKVObject,
useMMKVString,
} from 'react-native-mmkv';
import { Appearance } from 'react-native';
import { defaultTheme } from '../theme/md3/defaultTheme';
import { MD3ThemeType } from '../theme/types';
import { useMemo } from 'react';
export const useTheme = (): MD3ThemeType => {
const [appTheme] = useMMKVObject<MD3ThemeType>('APP_THEME');
const [isAmoledBlack] = useMMKVBoolean('AMOLED_BLACK');
const [customAccent] = useMMKVString('CUSTOM_ACCENT_COLOR');
const theme: MD3ThemeType = useMemo(() => {
let colors =
appTheme ||
((Appearance.getColorScheme() === 'dark'
? defaultTheme.dark
: defaultTheme.light) as MD3ThemeType);
if (isAmoledBlack && colors.isDark) {
colors = { ...colors, background: '#000000', surface: '#000000' };
}
if (customAccent) {
colors = { ...colors, primary: customAccent, secondary: customAccent };
}
return colors;
}, [appTheme?.id, isAmoledBlack, customAccent]);
return theme;
};
@@ -0,0 +1,211 @@
import { SafeAreaProviderCompat } from '@react-navigation/elements';
import {
MaterialBottomTabDescriptorMap,
MaterialBottomTabNavigationConfig,
MaterialBottomTabNavigationHelpers,
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
import {
CommonActions,
Link,
ParamListBase,
Route,
TabNavigationState,
useLinkBuilder,
} from '@react-navigation/native';
import * as React from 'react';
import { Platform, StyleSheet, Text } from 'react-native';
import { BottomNavigation } from 'react-native-paper';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
type Props = MaterialBottomTabNavigationConfig & {
state: TabNavigationState<ParamListBase>;
navigation: MaterialBottomTabNavigationHelpers;
descriptors: MaterialBottomTabDescriptorMap;
};
type Scene = { route: { key: string } };
// Optionally require vector-icons referenced from react-native-paper:
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
let MaterialCommunityIcons: React.ComponentType<
React.ComponentProps<
typeof import('react-native-vector-icons/MaterialCommunityIcons').default
>
>;
try {
// Optionally require vector-icons
MaterialCommunityIcons =
require('react-native-vector-icons/MaterialCommunityIcons').default;
} catch (e: any) {
let isErrorLogged = false;
// Fallback component for icons
MaterialCommunityIcons = ({
name,
color,
size,
selectionColor: _0,
onLayout: _1,
...rest
}) => {
if (!isErrorLogged) {
if (
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
e.message,
)
) {
console.error(e);
}
console.warn(
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons/MaterialCommunityIcons' could not be loaded.`,
"To remove this warning, try installing 'react-native-vector-icons' or use another method to specify icon: https://reactnavigation.org/docs/material-bottom-tab-navigator/#tabbaricon.",
);
isErrorLogged = true;
}
return (
// @ts-expect-error: we're passing icon props to text, but we don't care about it since it's just fallback
<Text
{...rest}
style={[
styles.icon,
{
color: typeof color !== 'number' ? color : undefined,
fontSize: size,
},
]}
>
□
</Text>
);
};
}
function MaterialBottomTabViewInner({
state,
navigation,
descriptors,
...rest
}: Props) {
const insets = useSafeAreaInsets();
const buildLink = useLinkBuilder();
return (
<BottomNavigation
{...rest}
navigationState={state}
onIndexChange={(index: number) =>
navigation.dispatch({
...CommonActions.navigate({
name: state.routes[index].name,
merge: true,
}),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
renderTouchable={
Platform.OS === 'web'
? ({
onPress,
route,
accessibilityRole: _0,
borderless: _1,
centered: _2,
rippleColor: _3,
style,
...rest
}) => {
return (
<Link
{...rest}
// @ts-expect-error: to could be undefined, but it doesn't affect functionality
to={buildLink(route.name, route.params)}
accessibilityRole="link"
onPress={(e: any) => {
if (
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
(e.button == null || e.button === 0) // ignore everything but left clicks
) {
e.preventDefault();
onPress?.(e);
}
}}
style={[styles.touchable, style]}
/>
);
}
: undefined
}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (typeof options.tabBarIcon === 'string') {
return (
<MaterialCommunityIcons
name={options.tabBarIcon}
color={color}
size={24}
style={styles.icon}
/>
);
}
if (typeof options.tabBarIcon === 'function') {
return options.tabBarIcon({ focused, color });
}
return null;
}}
getLabelText={({ route }: Scene) => {
const { options } = descriptors[route.key];
return options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: (route as Route<string>).name;
}}
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (event.defaultPrevented) {
preventDefault();
}
}}
safeAreaInsets={insets}
/>
);
}
export default function MaterialBottomTabView(props: Props) {
return (
<SafeAreaProviderCompat>
<MaterialBottomTabViewInner {...props} />
</SafeAreaProviderCompat>
);
}
const styles = StyleSheet.create({
icon: {
backgroundColor: 'transparent',
},
touchable: {
display: 'flex',
justifyContent: 'center',
},
});
@@ -0,0 +1,71 @@
import {
createNavigatorFactory,
DefaultNavigatorOptions,
ParamListBase,
TabActionHelpers,
TabNavigationState,
TabRouter,
TabRouterOptions,
useNavigationBuilder,
} from '@react-navigation/native';
import * as React from 'react';
import {
MaterialBottomTabNavigationConfig,
MaterialBottomTabNavigationEventMap,
MaterialBottomTabNavigationOptions,
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
import MaterialBottomTabView from './MaterialBottomTabView';
type Props = DefaultNavigatorOptions<
ParamListBase,
TabNavigationState<ParamListBase>,
MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap
> &
TabRouterOptions &
MaterialBottomTabNavigationConfig;
function MaterialBottomTabNavigator({
id,
initialRouteName,
backBehavior,
children,
screenListeners,
screenOptions,
...rest
}: Props) {
const { state, descriptors, navigation, NavigationContent } =
useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap
>(TabRouter, {
id,
initialRouteName,
backBehavior,
children,
screenListeners,
screenOptions,
});
return (
<NavigationContent>
<MaterialBottomTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
/>
</NavigationContent>
);
}
export default createNavigatorFactory<
TabNavigationState<ParamListBase>,
MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap,
typeof MaterialBottomTabNavigator
>(MaterialBottomTabNavigator);
+18 -35
View File
@@ -1,6 +1,5 @@
import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import Library from '../screens/library/LibraryScreen';
@@ -9,8 +8,11 @@ import History from '../screens/history/HistoryScreen';
import Browse from '../screens/browse/BrowseScreen';
import More from '../screens/more/MoreScreen';
import { useSettings, useTheme } from '../hooks/reduxHooks';
import { useSettings } from '../hooks/reduxHooks';
import { getString } from '../../strings/translations';
import createMaterialBottomTabNavigator from './BottomNavigation/createMaterialBottomTabNavigator';
import { useTheme } from '@hooks/useTheme';
import Color from 'color';
const Tab = createMaterialBottomTabNavigator();
@@ -25,21 +27,22 @@ const BottomNavigator = () => {
return (
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
theme={{
colors: {
...theme,
background: theme.background,
elevation: {
level2: Color(theme.primary).alpha(0.08).string(),
},
},
}}
shifting={!showLabelsInNav}
>
<Tab.Screen
name={getString('library')}
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
tabBarIcon: 'book-variant-multiple',
}}
/>
{showUpdatesTab && (
@@ -47,13 +50,7 @@ const BottomNavigator = () => {
name={getString('updates')}
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
tabBarIcon: 'alert-decagram-outline',
}}
/>
)}
@@ -62,9 +59,7 @@ const BottomNavigator = () => {
name={getString('history')}
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="history" color={color} size={24} />
),
tabBarIcon: 'history',
}}
/>
)}
@@ -72,26 +67,14 @@ const BottomNavigator = () => {
name={getString('browse')}
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
tabBarIcon: 'compass-outline',
}}
/>
<Tab.Screen
name={getString('more')}
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
tabBarIcon: 'dots-horizontal',
}}
/>
</Tab.Navigator>
+26 -10
View File
@@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { setBarColor } from '../theme/utils/setBarColor';
import { useTheme } from '../hooks/reduxHooks';
import { useTheme } from '../hooks/useTheme';
import { useGithubUpdateChecker } from '../hooks/githubUpdateChecker';
import * as SplashScreen from 'expo-splash-screen';
@@ -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';
@@ -28,35 +28,51 @@ import MigrateNovel from '../screens/browse/migration/MigrationNovels';
import MalTopNovels from '../screens/browse/discover/MalTopNovels';
import NewUpdateDialog from '../components/NewUpdateDialog';
import BrowseSettings from '../screens/browse/BrowseSettings';
import { useAppDispatch } from '@redux/hooks';
import { updateLibraryAction } from '@redux/updates/updates.actions';
import { useSettings } from '@hooks/reduxHooks';
const Stack = createStackNavigator();
const MainNavigator = () => {
const theme = useTheme();
const dispatch = useAppDispatch();
const { updateLibraryOnLaunch = false } = useSettings();
useEffect(() => {
setTimeout(async () => {
const timer = setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
}, [theme]);
return () => {
clearTimeout(timer);
};
}, [theme.id]);
useEffect(() => {
if (updateLibraryOnLaunch) {
dispatch(updateLibraryAction());
}
}, []);
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
return (
<NavigationContainer
theme={{ colors: { background: theme.colorPrimaryDark } }}
>
<NavigationContainer theme={{ colors: { background: theme.background } }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="BottomNavigator" component={BottomNavigator} />
<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} />
+11 -7
View File
@@ -4,15 +4,17 @@ 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';
import CategoriesScreen from '@screens/Categories/CategoriesScreen';
import LibrarySettings from '@screens/settings/SettingsLibraryScreen/SettingsLibraryScreen';
const Stack = createStackNavigator();
@@ -27,6 +29,7 @@ const SettingsStack = () => (
<Stack.Screen name="BackupSettings" component={BackupSettings} />
<Stack.Screen name="AppearanceSettings" component={AppearanceSettings} />
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
<Stack.Screen name="LibrarySettings" component={LibrarySettings} />
</Stack.Navigator>
);
@@ -36,6 +39,7 @@ const MoreStack = () => (
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
<Stack.Screen name="Downloads" component={Downloads} />
<Stack.Screen name="Categories" component={CategoriesScreen} />
</Stack.Navigator>
);

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