Compare commits

...

268 Commits

Author SHA1 Message Date
Rajarshee Chatterjee ae8d055689 perf: Limit Chapter Neighbor Queries To One Row
Lint / ESLint (push) Has been cancelled
Tests / Jest (push) Has been cancelled
Type Check / TypeScript (push) Has been cancelled
2026-09-26 20:43:28 +05:30
trietng 1607f80a0a feat: Add Volume Button Navigation and Slide Animation Settings for Paged Reading (#2058)
* feat: volume navigation and disable animation settings for paged reading

* fix: transition style removal

* fix: pageReader incorrect initialization

* fix: remove unneeded check

* chore: update Vietnamese translations for paged reading settings
2026-09-26 20:40:43 +05:30
github-actions[bot] a0c20e209c chore: Release v2.1.4 2026-09-26 07:14:58 +00:00
Rajarshee Chatterjee 648477931b feat: Add Version and Build Information to Error Fallback 2026-09-26 11:05:34 +05:30
Palloxin e42e8f66f2 fix: Improve Code Editor Newline Regex (#2021)
* Code Editor, New Lines regex improvement

* Update SimpleCodeEditor.tsx
2026-09-25 11:15:13 +05:30
Palloxin de5358727a fix: Restore Broken Preview to Previous State (#2004)
* Fix broken preview

* Optimized base64 image (reduced)

* fix image base64
2026-09-25 11:06:24 +05:30
Rajarshee Chatterjee 706459783f feat: Add Optional Continue Reading Buttons To Library 2026-09-23 18:55:51 +05:30
Patrick Loser 1ca9a679cf perf: Optimize Background Task Memory Usage and Lifecycle Management (#2059)
* fix: bound background task memory usage

* Fix background task cancellation and updates

* Update TaskActionReceiver.kt
2026-09-23 18:41:50 +05:30
Rajarshee Chatterjee 920f88aee4 fix: Resolve Jest CI Failures 2026-09-23 12:08:49 +05:30
Rajarshee Chatterjee cef3ae5bdf feat: Polish Statistics Screen Layout 2026-09-23 11:53:50 +05:30
Rajarshee Chatterjee 39c788e576 feat: Improve Genre Taxonomy Screen UX 2026-09-23 11:26:21 +05:30
Rajarshee Chatterjee d40713ac99 feat: Improve Custom Code UX 2026-09-23 10:41:38 +05:30
Rajarshee Chatterjee f53bed7190 chore: Update Translations (#1981)
* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* fix: Restore Overwritten Locale Translations

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* fix: Restore Overwritten Locale Translations
2026-09-22 10:06:03 +05:30
Rajarshee Chatterjee 0b9c415681 fix: Prevent Chapter Drawer Blank Flash On Open (Closes #2038) (#2048)
Opening the reader's chapter drawer flashed an empty panel before the
chapter list appeared, most visibly on slower devices.

Three things combined to cause it:

- The panel carried `backgroundColor: 'transparent'`, which overrode the
  opaque default `react-native-drawer-layout` applies to its own
  `styles.drawer`. Since `drawerStyle` is applied last in the panel's
  style array, the panel had no background at all, so the reader showed
  through it for every frame before the content painted. It now carries
  the drawer's own `theme.surface`, matching what the content already
  paints, so the panel is opaque from the first frame.

- The chapter list was mounted by the same handler that opened the
  drawer, so the slide-in animation (UI thread) ran while the list was
  still mounting and measuring (JS thread). The list is now mounted once
  the chapter has loaded and interactions have settled, before the
  drawer is first opened. A closed panel stays laid out -- it is only
  translated off-screen -- so the list measures and renders its first
  rows out of sight instead of during the animation.

- When the list mounted after the chapters arrived, it was scrolled to
  the current chapter with an animation even though `initialScrollIndex`
  had already placed it there, adding a second visible jump. That scroll
  is now skipped on the initial mount and kept for later chapter
  changes.


Claude-Session: https://claude.ai/code/session_01Et1ihq4SBUhfvxgWJisHTe

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-22 09:15:02 +05:30
Rajarshee Chatterjee 38003c59e0 fix: Stop Bottom Nav Icons Trembling On Tab Press (Closes #2041) (#2047)
The focused tab indicator animated `width` directly, which triggered layout every frame — Yoga's per-node pixel rounding then shifted the icon and label inconsistently as the width swept through fractional values, causing a visible tremble. Fixed by giving the icon a fixed-size box and animating the indicator via `scaleX` on an absolutely positioned sibling instead, since transforms don't trigger layout.
2026-09-22 08:23:58 +05:30
Rajarshee Chatterjee f1119d8c30 feat: Add Always Ask Category Selection (Closes #537) 2026-09-16 10:14:29 +05:30
Rajarshee Chatterjee a5e2b4d4f0 feat: Add Crash Log Collection And Sharing (Closes #11) 2026-09-15 14:16:51 +05:30
Rajarshee Chatterjee a12cba030d fix: Skip Dash-Only TTS Dividers 2026-09-15 13:38:39 +05:30
Rajarshee Chatterjee 56d1140aad chore: Require Issue Templates 2026-09-14 08:44:15 +05:30
Rajarshee Chatterjee 0379141304 feat: Add Individual Task Cancellation (Closes #1985) 2026-09-11 18:41:51 +05:30
Rajarshee Chatterjee b6e9f7157d chore: Rename Feature Request Label 2026-09-08 07:03:28 +05:30
Rajarshee Chatterjee edb3df719f fix: Repair Invalid Default Category Names (Closes #2031) 2026-09-07 22:25:31 +05:30
Rajarshee Chatterjee 9bde3df74d fix: Prevent Novel Screen Empty State Flash (Closes #2034) 2026-09-07 21:13:47 +05:30
Rajarshee Chatterjee 2426e6fe3e chore: Lowercase Issue Template Labels 2026-09-07 20:49:59 +05:30
Rajarshee Chatterjee 93209abebd fix: Make Backup Restore Non-Destructive (Closes #2026) 2026-09-07 20:43:20 +05:30
Claude 158786ccd5 fix: Prevent Crash in parseChapterNumber When Chapter or Novel Name Is Missing
Guards against undefined/null chapterName and novelName so
parseChapterNumber no longer throws "Cannot read property
'toLowerCase' of undefined" when rendering the Updates screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011WW3AHpNPxoRVsbAMVRX6T
2026-09-02 19:15:46 +00:00
Patrick Loser 77be5624f2 feat: Add URL Novel Search and Intent-Based Novel Opening (#1987)
* Add native share intent handling module

* Handle shared URLs via intent handler

* i18n: add global search and novel error strings

Generated with omp (AI coding agent)

* novel store: track bootstrap failures as user-facing errors

Bootstrap failures no longer surface raw plugin/DB errors; missing novels map to a custom not-found message. Generated with omp (AI coding agent)

* novel screen: render error view when bootstrap fails

Shows a custom error with a back action instead of a blank screen, and hides the appbar actions in the error state. Generated with omp (AI coding agent)

* global search: never auto-navigate or search URL text

URLs are handled by the screen's explicit submit flow; navigation and search stay out of the hook. Generated with omp (AI coding agent)

* global search: add animated open-novel FAB with clipboard offer

Submit or FAB tap opens a matching URL's novel; unmatched URLs toast. The FAB floats above the keyboard via keyboard-controller and slides in from the right. Generated with omp (AI coding agent)

* fix review issues

* fix rebase issue
2026-09-01 18:59:32 +05:30
Rajarshee Chatterjee 4b580c90ef fix: Prevent TTS From Speaking Surrounding Quotes (Closes #2008) 2026-08-29 11:41:59 +05:30
github-actions[bot] cb1a5d9e82 chore: Release v2.1.3 2026-08-22 06:13:17 +00:00
Mohammad 7e20f7dccb feat: Increase Maximum Reader Text Size from 20 to 50 (#1998)
Increased maximum text size limit in Reader component, and settings page
2026-08-22 08:55:41 +05:30
Rajarshee Chatterjee 0fab0143b1 fix: Prevent Large EPUB Stalls And Optimize Imports
Closes #1932
Closes #2005
2026-08-22 08:41:48 +05:30
Rajarshee Chatterjee 5a0717d2c7 fix: Prevent Duplicate TTS Text From Nested Formatting (Closes #1994) 2026-08-18 11:50:06 +05:30
Patrick Loser 09652f9962 fix: Refetch Library After Category Update (#1988)
Add tests covering NovelScreenButtonGroup category assignment
flows to ensure the library is refreshed and the novel state
is updated when needed.
2026-08-18 09:28:07 +05:30
Rajarshee Chatterjee eef58f212f fix: Support Namespace-Prefixed EPUB Imports 2026-08-17 21:16:29 +05:30
Rajarshee Chatterjee 523aa90c19 fix: Include Novel Covers In Library Backups (Closes #1983) 2026-08-16 12:48:54 +05:30
Ahjie521 eb310f7821 feat: Add Empty Chapter Refresh in Reader (#1978)
* feat(reader): swipe down to re-fetch a chapter, never cache empty responses

- pull down at the top of a chapter to re-fetch it, recovering from
  transient empty responses without leaving the reader
- empty responses are evicted from the chapter cache so a refresh
  re-fetches instead of replaying the empty message
- add a refresh button to the reader's bottom bar and a 'Refresh
  chapter' link on the empty-chapter message
- register the typescript-eslint plugin and ignore assets/ in the
  eslint flat config so lint-staged can lint core.js

* feat(reader): move refresh option to appbar menu
2026-08-16 07:36:41 +05:30
Patrick Loser af2a224781 fix: Novel Screen Scroll Lag, Source Pagination Stalls, and Skeleton Overlap (#1971)
* fix

* Update pnpm-lock.yaml

* fix tests
2026-08-16 07:34:17 +05:30
Rajarshee Chatterjee 990cd4f2eb fix: Upgrade WorkManager for Repeated Library Updates (Closes #1972) 2026-08-11 15:58:51 +05:30
Rajarshee Chatterjee 3ac611f633 fix: Normalize EPUB SVG Images in Nitro Module (Closes #1622) 2026-08-11 14:51:50 +05:30
Rajarshee Chatterjee 909504a721 feat: Add Repository Enable And Disable Controls (Closes #1628) 2026-08-11 13:50:25 +05:30
Rajarshee Chatterjee 63349de1bb fix: Select All Chapters Across Lazy-Loaded Batches (Closes #1960) 2026-08-11 11:58:18 +05:30
Rajarshee Chatterjee c6679b7f41 fix: Draw Content Behind Android Navigation Bar (Closes #1260) 2026-08-11 11:53:03 +05:30
Ahjie521 15560b67b1 fix: Pass Plugin imageRequestInit Headers to Novel Detail Cover (#1977)
The novel detail header rendered its cover with a bare { uri } source,
so plugins whose cover CDN requires request headers (e.g. RanobeLib's
cover.cdnlibs.org, which 403s without a Referer) showed no cover on the
novel detail screen even though covers worked in browse/library lists.

Mirror the LibraryListView pattern: fetch getPlugin(novel.pluginId)?
.imageRequestInit and attach its headers to the cover source.
2026-08-11 08:26:12 +05:30
Alvan Noah 7883b28cd9 fix: Stop TTS During Incoming and Outgoing Phone Calls (#1976)
fix: TTS continues playing during incoming and outgoing phone calls

Implement audio focus handling for TTS playback on Android, including support for request and abandonment during playback and transient interruptions.

Closes #1975
2026-08-11 08:01:55 +05:30
Patrick Loser 64707409bb feat: Create Dedicated Custom Code Settings Page V2 (#1902)
* init

* text modification without webview rerender

* Fix SettingsReaderScreen

* remove unneeded

* solve ts issue

* add tests

* Add KeyboardProvider

* Update SimpleCodeEditor.tsx

* fix ts error

* add types package

* reworked SimpleCodeEditor

* Preserve highlightmode

* Update SimpleCodeEditor.tsx

* performance improvement

* simplified

* fix tsc

* fix alignment issues

* load current snippet into example view

* Merge branch 'master' into custom-code-clean

* fix type + lint

* Resolve can't read property id of undefined error

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2026-08-11 07:09:52 +05:30
Rajarshee Chatterjee 7f1f76408b docs: Add AGENTS.md with Agent Setup, Commands, and Conventions (#1974) 2026-08-10 09:17:43 +05:30
Rajarshee Chatterjee 51560195ba fix: Soften Skeleton Loading Colors (Closes #1964) 2026-08-10 08:25:10 +05:30
Rajarshee Chatterjee 084dcccabc fix: Match Chapter Drawer Read Color (Closes #1973)
[skip ci]
2026-08-10 08:17:22 +05:30
Rajarshee Chatterjee 3ad6e372fd fix: Correct Translation Strings Across Locales 2026-08-10 08:16:32 +05:30
Ali 179feb56ee fix: Include All Chapters Beyond the 1000-Row UI Limit (#1938)
* chapter backup and restore fix

* Update .gitignore

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

* test: update backup mocks for getAllNovelChaptersForBackup

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2026-08-09 16:28:12 +05:30
Patrick Loser 67e01bc2df fix: Resolve Stats Screen Logic Issue (#1968)
* fix genre distribution

* add tests

* open edit dialog when creatig new taxonomy category
2026-08-08 16:28:12 +05:30
Patrick Loser 3ece098b94 perf: Optimize APK Size (#1969)
* optimize android build

* update lodash imports

* remove react-navigation/stack

* remove flash-list
2026-08-08 16:24:07 +05:30
Patrick Loser 586e085141 fix: Resolve Migration Crash (#1967)
init
2026-08-08 15:38:25 +05:30
Rajarshee Chatterjee e0c89cdd9f fix: Neutralize Skeleton Loading Colors 2026-08-05 07:07:20 +05:30
Rajarshee Chatterjee 3bf025108a fix: Restore Indonesian Translations 2026-08-05 06:05:03 +05:30
Rajarshee Chatterjee f69e5d6a7e chore: Update Translations (#1934)
* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]
2026-08-05 05:37:25 +05:30
Rajarshee Chatterjee a727c229ca fix: Optimize Novel Counter Migration 2026-08-05 05:32:58 +05:30
Rajarshee Chatterjee 57eca11a9e fix: Pass Library Status Through Novel Navigation 2026-08-04 22:06:05 +05:30
Rajarshee Chatterjee 197d8670f8 fix: Resolve EPUB Cover Document Imports (Closes #1948) 2026-08-04 20:49:38 +05:30
Rajarshee Chatterjee 675f19ef90 fix: Make Update Chapters Reactive and Remove Display Limit (Closes #1953) 2026-08-04 20:25:19 +05:30
Rajarshee Chatterjee 8a12529ba8 fix: Prevent Update Clearing From Freezing App (Closes #1955) 2026-08-04 19:52:11 +05:30
Rajarshee Chatterjee c3482a8510 fix: Resolve Nitro EPUB Jest Alias 2026-08-04 19:41:27 +05:30
Rajarshee Chatterjee e4246dee59 fix: Replace EPUB Exports Atomically With NativeFile (Closes #1954) 2026-08-04 19:32:12 +05:30
Rajarshee Chatterjee 23f9b183be fix: Refresh Updates Screen on Focus (Closes #1956) 2026-08-04 19:13:05 +05:30
Rajarshee Chatterjee 1eb8c587c7 fix: Prevent Library From Remaining in Loading State 2026-08-04 18:45:16 +05:30
Rajarshee Chatterjee c3b75ebeb2 fix: Migrate Plugin Bundles to Reliable Internal Storage
Closes #1907
2026-08-04 16:34:19 +05:30
Rajarshee Chatterjee 91358ad3dd fix: Support All EPUB Image Formats (Closes #1946) 2026-08-04 10:59:53 +05:30
Rajarshee Chatterjee 13885320ae fix: Add Novels To Library When Setting Categories (Closes #1945) 2026-08-03 22:59:14 +05:30
Rajarshee Chatterjee b9d1abcf27 fix: Refresh Library Download Counts After Deletion 2026-08-03 20:56:05 +05:30
Rajarshee Chatterjee c3260e8e01 fix: Resolve Chapter Download Queue and Removal Issues (Closes #1937) 2026-08-01 17:10:29 +05:30
Rajarshee Chatterjee c0877a9b06 feat: Add Ignorable App Update Notifications
Closes #600
Closes #1632
2026-08-01 12:06:28 +05:30
Patrick Loser 96fb52df17 feat: Enhance Stats With Genre Taxonomy and Charts (#1919)
* Add react-native-svg and refresh stats UI

Normalize status counts, add donut chart with legend, and improve bars

* Add genre stats screen and taxonomy link

* Update StatsQueries.test.ts

* change genre taxonomy style

* Use LegendList instead of ScrollView

* added more colors to chart

* Add Highlight animation

* Add ChartLegend animation

* Maintainance

* updated stats queries

* Added animations to collapsibles and optimized performance

* Extract tabs

* update time tracking tab
2026-08-01 11:31:03 +05:30
Rajarshee Chatterjee 76106317b6 fix: Restore Midnight Dusk Chip Contrast (Closes #1939) 2026-07-31 08:11:08 +05:30
K1ngfish3r e75f4fba5d feat: Add More Novel Statuses and Icons (#1931)
add more novelStatus and icons
2026-07-29 22:30:20 +05:30
Rajarshee Chatterjee 4dc933ee5a fix: Remove White Flash During Screen Transitions (Closes #1926) (#1927)
`AppTheme` never set `android:windowBackground`, so it was inherited from
`Theme.AppCompat.DayNight.NoActionBar` and resolved to a near-white colour
whenever the system was in light mode — independently of the theme picked
inside the app. Nothing between that window and the navigators painted a
background of its own: the root view, `GestureHandlerRootView` and
`SafeAreaProvider` are all transparent, and only the individual screens carry
`contentStyle`. Any frame in which no screen was drawn — mounting a nested
navigator such as `MoreStack` or `ReaderStack`, or freezing the screen being
left — therefore showed the white window through.

Paint the root view with the active theme background so the window stays
covered, and give `AppTheme` a day/night `android:windowBackground` so the
frames before React draws are themed as well. The chapter screen additionally
uses the reader background instead of the app background, so pushing it no
longer flashes the app theme before the reader appears.


Claude-Session: https://claude.ai/code/session_014uxuC7JVkShhWnN1WhkTaH

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 21:27:22 +05:30
Rajarshee Chatterjee bfb12b04aa feat: Add Configurable Automatic Backup Scheduling 2026-07-29 21:20:19 +05:30
Patrick Loser b7639ab55a feat: Upload JavaScript Source Maps (#1924)
* init

* fix paths

* add hermes flag
2026-07-29 20:58:00 +05:30
github-actions[bot] ecc5b84a64 chore: Release v2.1.2 2026-07-29 10:17:01 +00:00
Rajarshee Chatterjee eb12bdfd62 fix: Prevent Novel Genre Rendering Crashes 2026-07-29 15:44:44 +05:30
Rajarshee Chatterjee 9612bf6f89 chore: Update Translations (#1928)
* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]
2026-07-29 14:36:36 +05:30
Rajarshee Chatterjee 0ed7d878b4 fix: Prevent White Reader Drawer Seam 2026-07-29 14:21:59 +05:30
Rajarshee Chatterjee 5f45e4c7f2 fix: Recover Interrupted Chapter Migrations 2026-07-28 20:36:59 +05:30
Rajarshee Chatterjee ca24671a2f fix: Support Fork-Safe Preview Builds 2026-07-28 15:38:09 +05:30
Rajarshee Chatterjee ee3a4f20bc fix: Preserve Default Category After Reordering (Closes #1922) 2026-07-28 14:46:03 +05:30
Rajarshee Chatterjee 0c3542836c fix: Synchronize Library Updates And Optimize Import Refreshes 2026-07-28 14:46:03 +05:30
Patrick Loser f71c9c8594 fix: Ignore Discord Notifications Outside the Main Repository (#1923) 2026-07-28 14:30:52 +05:30
github-actions[bot] f53546cc01 chore: Release v2.1.1 2026-07-28 04:28:46 +00:00
Rajarshee Chatterjee 594b078fad fix: Harden Database Migration Recovery 2026-07-28 07:32:55 +05:30
Rajarshee Chatterjee ac1b2f5ce4 feat: Add Durable Completion Notifications 2026-07-27 22:27:04 +05:30
Rajarshee Chatterjee 648d21062f feat: Add Parallel Task Queues With Per-Plugin Download Limits 2026-07-27 22:15:02 +05:30
Rajarshee Chatterjee 89d715ce6b docs: Replace Beta Badge With Build Preview Status 2026-07-27 21:54:45 +05:30
Rajarshee Chatterjee bd527a02ef feat: Improve Backup Selection, Notifications, And Plugin Restore 2026-07-27 20:03:03 +05:30
Rajarshee Chatterjee 11117c1b54 feat: Add GitHub-Style Embed For Nightly Notifications 2026-07-27 17:12:50 +05:30
Patrick Loser ba25d5b8fe feat: Add Rozenite DevTools Integration (#1912)
* add rozenite

* add opsqlite adapter

* fix lint

* Update scripts/generate-env-file.cjs

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

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2026-07-27 17:07:41 +05:30
Rajarshee Chatterjee 31cb4b994a fix: Align Bottom Navigation With Material 3 Specs 2026-07-27 15:11:38 +05:30
Rajarshee Chatterjee 72ef618af1 fix: Preserve Existing Data During Novel Counter Migration 2026-07-27 15:06:36 +05:30
Rajarshee Chatterjee 936fd223bb feat: Add Discord Notifications For Nightly Builds 2026-07-27 10:49:30 +05:30
Rajarshee Chatterjee 6d26438d2d test: Update Expected Database Migrations 2026-07-27 08:47:32 +05:30
Rajarshee Chatterjee 5eb5a55c4c fix: Repair Skipped Novel Counter Migrations 2026-07-27 08:41:55 +05:30
Rajarshee Chatterjee 6cd86d1422 chore: Update Translations (#1906)
* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

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

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]
2026-07-27 08:19:16 +05:30
Patrick Loser 9783c4d5e3 fix: Normalize Repository Back Navigation (#1905)
* remove useBackHandler

* Remove popToTop
2026-07-27 00:53:11 +05:30
github-actions[bot] 87f602a1fd chore: Release v2.1.0 2026-07-26 17:41:31 +00:00
Rajarshee Chatterjee 3d32ddd6e9 fix: Improve Release Selection And APK Verification 2026-07-26 23:10:38 +05:30
Rajarshee Chatterjee 7ddd934c2b fix: Handle Missing Tracker Client IDs 2026-07-26 22:46:23 +05:30
Rajarshee Chatterjee b53c91f748 chore: Separate Jest And Type Check Workflows 2026-07-26 22:33:56 +05:30
Rajarshee Chatterjee 9d106a3716 feat: Add Semantic Version Bump Selection 2026-07-26 22:23:46 +05:30
Patrick Loser 8b0a8dc4c0 chore: Update Husky Pre-Commit and Pre-Push Hooks 2026-07-26 22:22:38 +05:30
Patrick Loser 72bfcad034 fix: Resolve Type and Lint Issues 2026-07-26 22:22:24 +05:30
Rajarshee Chatterjee 345d084eba feat: Add Chapter Numbers To EPUB Titles (Closes #1405) 2026-07-26 21:07:04 +05:30
Rajarshee Chatterjee 10b6042a9c feat: Add EPUB Export Progress Notifications
Closes #799
2026-07-26 20:53:25 +05:30
Rajarshee Chatterjee bd71611725 feat: Improve Chapter Drawer And WebView App Bar 2026-07-26 20:26:34 +05:30
Rajarshee Chatterjee 8f53550d2c fix: Restore Tracker Search Requests 2026-07-26 19:42:48 +05:30
Rajarshee Chatterjee 52914b978d fix: Migrate Volume Button Events to Expo Listener API 2026-07-26 18:41:47 +05:30
Rajarshee Chatterjee ff12759a66 ci: Run Preview Builds Daily When Missing 2026-07-26 18:35:45 +05:30
Rajarshee Chatterjee e62868b82c feat: Automate Draft Android Releases
[skip ci]
2026-07-26 18:27:18 +05:30
Rajarshee Chatterjee 97be48dfe1 feat: Add Migration Review Options
Closes #1037
Closes #937
2026-07-26 15:44:09 +05:30
Rajarshee Chatterjee cbe7d70e2d fix: Restore Collapsed TTS Button Interaction
[skip ci]
2026-07-26 15:08:02 +05:30
Rajarshee Chatterjee 6548e9a6f8 fix: Improve Paged Reader Layout and Performance
Closes #1721
2026-07-26 15:07:46 +05:30
Rajarshee Chatterjee 138fdff2e5 fix: Prevent Duplicate WebView Bottom Insets
Closes #1757
2026-07-26 14:35:44 +05:30
Rajarshee Chatterjee f1dd4b77a8 feat: Improve Empty Chapter Reporting
[skip ci]
2026-07-26 14:19:37 +05:30
Rajarshee Chatterjee 9b356ffd17 fix: Preserve Client IDs And Improve Environment Generation
[skip ci]
2026-07-26 13:42:41 +05:30
Rajarshee Chatterjee 5cf7e15d76 feat: Add Reader Setting Descriptions 2026-07-26 12:59:53 +05:30
Rajarshee Chatterjee 6a7e90c179 fix: Align Advanced Reader Tabs And Localize Actions 2026-07-26 12:55:39 +05:30
Rajarshee Chatterjee 99c31d56bb fix: Use MD3 Outline Variant For Tab Borders 2026-07-26 12:38:39 +05:30
Rajarshee Chatterjee 4d98247980 feat: Add Android DNS Over HTTPS Support
[skip ci]

Closes #478
2026-07-26 00:23:07 +05:30
Rajarshee Chatterjee 164f5d2592 feat: Add Automatic Library Update Scheduling
Closes #13
Closes #953
2026-07-25 20:48:37 +05:30
Rajarshee Chatterjee 89cddf9e3d fix: Expand Discover Row Ripple 2026-07-25 20:34:30 +05:30
Rajarshee Chatterjee eac6ad584e feat: Improve Novel App Bar And EPUB Export UX
[skip ci]
2026-07-25 19:15:43 +05:30
Rajarshee Chatterjee 93fe04c28c feat: Polish Reader Controls And Navigation
[skip ci]
2026-07-25 18:16:08 +05:30
Rajarshee Chatterjee c9358b3a9f feat: Update Android Branding Assets 2026-07-25 15:52:13 +05:30
Rajarshee Chatterjee 67d2c7c01b feat: Add Collapsible TTS Controls And Stable Notification Progress
[skip ci]
2026-07-25 14:47:36 +05:30
Rajarshee Chatterjee c30441b949 feat: Add Smart Library Update Filters
Closes #423
2026-07-25 14:29:41 +05:30
Rajarshee Chatterjee 6d2a9f8e15 fix: Prevent Slider Flicker After Release 2026-07-25 14:13:25 +05:30
Rajarshee Chatterjee 799845426c feat: Add Parallel Library Updates Across Sources 2026-07-25 13:54:46 +05:30
Rajarshee Chatterjee 8ac628b3de fix: Improve History Removal Flow.
[skip ci]

Closes #885
2026-07-25 13:47:26 +05:30
Rajarshee Chatterjee d02291c22c feat: Remember Last Used Library Category
[skip ci]

Closes #1026
2026-07-25 12:21:53 +05:30
Rajarshee Chatterjee 7af0911f5b feat: Add High-Resolution Android Flavor Assets
[skip ci]
2026-07-25 11:54:21 +05:30
Rajarshee Chatterjee 098782d6aa refactor: Simplify Settings Layout And Theme Selection
[skip ci]
2026-07-25 11:23:41 +05:30
Rajarshee Chatterjee 732628c40b feat: Add Date Format And Relative Timestamp Settings.
Closes #834
2026-07-25 11:03:20 +05:30
Rajarshee Chatterjee 5e62c36a9b feat: Add Library Settings And Category-Based Global Updates
Closes #381
Closes #1423
Closes #1444
2026-07-25 10:48:39 +05:30
Rajarshee Chatterjee 3d102a52a7 perf(reader): Cut Chapter Open Latency and Reading-Time Re-Renders (#1899)
[skip ci]

* perf(reader): Cut Chapter Open Latency and Reading-Time Re-Renders

Opening a chapter blocked the first paint on work that is not needed to
show text, and mounted several heavy subtrees while that work was running.

Load path (useChapter):
- Render the chapter as soon as its text is ready. Resolving the next and
  previous chapter (two full-scan queries, plus a plugin page fetch over
  the network at a page boundary) now runs after the chapter is on screen
  and is pushed into the WebView instead of being baked into its HTML.
- Read downloaded chapters with a single native call instead of
  exists() + readFile(), and start that read in parallel with the
  database lookup instead of after it.
- Cache render-ready (sanitized) HTML, so prefetched chapters skip
  sanitize-html entirely, and prefetch when the JS thread is idle rather
  than in the middle of the current load.
- Drop failed loads from the cache so a retry actually retries.
- Guard every load with a generation id, keep getChapter and friends
  referentially stable, and defer history/last-read writes off the
  critical path.

Mounting:
- The chapter drawer (a list of every chapter) and the settings sheet
  (a tab view that enumerates TTS engines and voices) are mounted on
  first use instead of at reader open; the sheet's tabs are lazy.
- The loading skeleton shimmers its first lines only instead of
  animating a full screen of gradient views.

While reading:
- Progress and mark-as-read writes keep the chapter array identity when
  nothing changed, and a chapter is marked read (and pushed to the
  tracker) once instead of on every progress report.
- Drawer rows are a memoized component with stable props, the drawer
  stylesheet is memoized, and the WebView document is built once per
  chapter instead of on every reader re-render.

Also fixes the drawer's loading branch, which compared a ref object to
undefined and was therefore unreachable, and guards page-reader
transitions against a not-yet-resolved neighbour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ac6DNTqrKqsQjbdMSmU3nm

* perf(reader): Stop Double-Parsing the Chapter and Cut Per-Frame Work

Follow-up to the previous pass, mostly inside the reader document itself.

Chapter open:
- The chapter body was written back into the DOM on every open. The
  document already arrives with the chapter parsed, so unless bionic
  reading or paragraph-spacing removal is on, that re-parsed and
  re-laid-out the whole chapter (and restarted image loads) for nothing.
  It now only writes when the transformed markup actually differs.
- The reading position was restored from `window.load` via
  `document.fonts.ready`, both of which wait for every image in the
  chapter - measured at ~1.5s with a single slow image, during which the
  reader sat at the top of the chapter. It is now restored once the
  chapter is parsed and styled (~100ms in the same scenario) with an
  instant scroll, and corrected afterwards if fonts or images changed the
  height - but only while the reader is still where it was put, so it
  cannot yank the page from under someone already reading.

While reading:
- The scrollbar and the footer percentage each had their own unthrottled
  scroll listener recomputing the same ratio; they now share one
  listener coalesced to a frame. The footer clock no longer ticks state
  when the footer is hidden.
- Reading time is persisted without rebuilding the in-memory chapter
  list. It was rebuilt every few seconds for a field no screen renders -
  the statistics screen aggregates it from the database - and on Fabric
  react-navigation deliberately does not freeze the screen below the
  focused one, so it re-rendered the novel screen's chapter list too.
- `hidden` moved into its own context. It flips on every tap on the page,
  and a context value change re-renders all consumers, so the WebView,
  drawer, appbar and searchbar were all re-rendering for a flag only the
  reader screen reads.
- `openPage` no longer re-queries the chapter list when the requested
  page is already open, which the drawer does on every mount.

Smaller: the battery level is read asynchronously instead of through two
synchronous native calls per chapter, the sanitizer options object is
built once instead of per chapter, the general settings tab draws its
dozen fixed rows directly instead of virtualising them, and the reader
settings hook stops allocating a new settings object per render.

Verified the document changes by loading the real reader assets in
Chromium: position restored before load, no body rewrite when no
transform applies, both scroll indicators driven by the shared listener,
progress still reported, transforms still applied and reverted, and the
post-load correction only firing on an untouched reader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ac6DNTqrKqsQjbdMSmU3nm

* perf(reader): Optimize Search Functionality with Caching and Batch Processing

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-25 10:47:58 +05:30
Rajarshee Chatterjee 4dcf213037 fix: Restore Reader Changes Reverted During Expo Migration
[skip ci]
2026-07-25 09:20:13 +05:30
Rajarshee Chatterjee 4de82472ee fix: Resolve Require Cycles And LegendList Render Updates 2026-07-25 09:09:23 +05:30
Rajarshee Chatterjee 58db0352e6 fix: Restore Metro Reader Asset Middleware 2026-07-25 08:55:23 +05:30
Rajarshee Chatterjee 74808f086e feat: Replace Legacy Media Controls with Nitro-Powered Playback (#1896)
* feat: Replace Legacy Media Controls with Nitro-Powered Playback

* feat: Add TTS Engine Selection

* chore: Remove User-Facing Error Reporting

* refactor: Combine `Tts` and `TtsSession` Imports from `nitro-tts` Module

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 23:42:17 +05:30
amir-rezaei b96351ea81 docs: Fix Contraction and Missing Article in TESTING.md (#1895)
[skip ci]

* docs: fix trailing period, organization expansion, and comma in CONTRIBUTING.md

* docs: fix withoud typo in CONTRIBUTING-NIX.md

* docs: fix contraction and missing article in TESTING.md

---------

Co-authored-by: ferkans-amir <amir.rezaei@tu-berlin.de>
Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2026-07-24 22:20:20 +05:30
amir-rezaei eb8dc6df36 docs: Fix "Withoud" Typo in CONTRIBUTING-NIX.md (#1894)
[skip ci]

* docs: fix trailing period, organization expansion, and comma in CONTRIBUTING.md

* docs: fix withoud typo in CONTRIBUTING-NIX.md

---------

Co-authored-by: ferkans-amir <amir.rezaei@tu-berlin.de>
Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2026-07-24 22:19:15 +05:30
Rajarshee Chatterjee 0c8546188e feat: Add MD3 Slider Component. 2026-07-24 22:16:17 +05:30
Rajarshee Chatterjee e9f6bdaa20 fix: Modernize Menu Styling And Outside-Tap Dismissal 2026-07-24 22:16:17 +05:30
Rajarshee Chatterjee 3d34658d0f feat: Add Material 3 Top Tab Indicators
[skip ci]
2026-07-24 22:16:17 +05:30
amir-rezaei d44ce7412a docs: Fix Trailing Period, Organization Expansion, and Comma in CONTRIBUTING.md (#1893)
Co-authored-by: ferkans-amir <amir.rezaei@tu-berlin.de>
2026-07-24 20:22:03 +05:30
Rajarshee Chatterjee 17c891e133 refactor: Standardize Bottom Sheet UX and Styling
[skip ci]
2026-07-24 20:14:33 +05:30
Rajarshee Chatterjee 9805da63cf refactor: Optimize Novel Screen Performance And Architecture
[skip ci]
2026-07-24 17:07:06 +05:30
Rajarshee Chatterjee afc20277e0 fix: Restore Android Build Types For CI 2026-07-24 15:46:29 +05:30
amir-rezaei 14061d207b docs: Fix Title Case Headings, GitHub Capitalization, and E.g. Formatting in CONTRIBUTING.md (#1892)
Co-authored-by: ferkans-amir <amir.rezaei@tu-berlin.de>
2026-07-24 15:19:37 +05:30
Rajarshee Chatterjee 916374b5df refactor: Reorganize Repository Structure
[skip ci]
2026-07-24 15:13:50 +05:30
Rajarshee Chatterjee 0a2a6c96ca feat: Add Variant-Specific Android Splash Screens
[skip ci]
2026-07-24 14:59:28 +05:30
Rajarshee Chatterjee 65e8610030 refactor: Replace Bottom Navigation Shift With Fade
[skip ci]
2026-07-24 13:40:41 +05:30
Rajarshee Chatterjee 1322065290 feat: Add Variant-Specific Android App Icons 2026-07-24 13:18:44 +05:30
Rajarshee Chatterjee 22a1efd1e5 perf: Optimize React Native Skeleton Loading Components
[skip ci]
2026-07-24 13:00:51 +05:30
Rajarshee Chatterjee 269606ebb8 fix: Resolve Nitro EPUB Import Alias
[skip ci]
2026-07-24 12:45:47 +05:30
Rajarshee Chatterjee 4cb1890b62 feat: Add Nitro EPUB Exporter 2026-07-24 12:30:43 +05:30
Rajarshee Chatterjee a421177f0c fix: Improve Category Badge Layout And Drag Reordering
[skip ci]
2026-07-24 11:23:59 +05:30
Rajarshee Chatterjee a16ec87631 perf: Optimize Novel Covers, Lists, and Chapter Groups 2026-07-24 11:15:35 +05:30
Rajarshee Chatterjee dfd620aa29 feat: Standardize Dialogs with Material 3 Patterns 2026-07-23 23:05:53 +05:30
Rajarshee Chatterjee a5a6e8326f chore: Update @legendapp/list to 3.3.3 2026-07-23 22:08:26 +05:30
Rajarshee Chatterjee 7f48a8ba82 refactor: Improve Plugins Screen UX and Code 2026-07-23 21:44:01 +05:30
Rajarshee Chatterjee ae34d7b4ec feat: Add Dynamic Preview APK Metadata 2026-07-23 10:07:36 +05:30
Rajarshee Chatterjee 44c8e54ed8 feat: Add Dynamic Material Theme Colors (Closes #1863) 2026-07-23 09:10:48 +05:30
Ecasept 4a4208e336 feat: Add Time-Tracking Toggle and Inactivity Timeout (#1890)
* feat: implement on-off switch and inactivity timeout for time-tracking

* chore: remove stray console.log
2026-07-23 09:10:14 +05:30
CD-Z d2385b8dc2 Update build.yml 2026-07-23 00:08:46 +02:00
Rajarshee Chatterjee 9babe1676d refactor: Remove React Native Background Actions Manifest Customization 2026-07-22 23:59:11 +05:30
Rajarshee Chatterjee 8c38f499e4 chore: Update AppState Test Setup And pnpm Configuration 2026-07-22 23:54:22 +05:30
Ecasept 89c43c1eef feat: Implement Time Tracking and Statistics (#1886) 2026-07-22 23:30:38 +05:30
Patrick Loser c3c891cea0 refactor: Migrate to Expo Managed Workflow (#1885)
* enable predictive back gesture

* upgrade to rn 85

* remove deprecated

* added timeout to theme switch

* fix plugin items hit registration

* fix SearchbarV2 hit registration

* fix loading spinner

* migrated to expo

* migrated to expo

* Fix reader footer animation

* working again

* added icons back

* added splashscreen

* clean up

* nitro epub

* fix

* remove .env.local from git tracking

* fix rn background actions not working

* moving custom permissions to app.json

* more cleanup

* fix epub import

* remove unused deps

* Further cleanup for gradle build types

* Update build.yml

* ignore android/ and ios/ as they are generated by CNG

* fix lint

* Delete ios directory

* Delete android directory

* fix lint

* fix test mocks

* update packages

* update eslint

* fix ThemeProvider placement

* fix test and lint

* Add BackgroundTask native module

* Add NativeFile changes

* first round of fixing compatibility issues

* added types for native modules
2026-07-22 23:29:53 +05:30
Rajarshee Chatterjee e4cf64a112 refactor: Migrate Background Tasks to Native Android Scheduling (#1889)
* refactor: Restructure Application Architecture

* feat: Support Batched Downloads And EPUB Imports

* feat: Add Download Checkpoint Recovery
2026-07-22 09:21:18 +05:30
Luigi 3e1e211098 feat: Add Address Bar in WebView (#1870) 2026-07-19 19:25:58 +05:30
Ecasept 02b6b9848f fix: Prevent WebView Reset When Reader Settings Change (#1884)
fix: reader being reset when reader settings change
2026-07-19 19:23:10 +05:30
Shisii 71da09ed44 feat(reader): Add Current Chapter Search (#1877)
* feat(reader): add search button and search input for word search

* fix(reader): make keyboard close on hideHeader

* chore(reader): format reader files

* chore(strings): regenerate string types

* feat(reader): add chapter word search

* fix(reader): improve chapter search reliability

* fix(reader): hide ReaderFooter when search is open

* fix(reader): add support for search in bionic reading mode

* fix(reader): localize chapter search placeholder

* feat(reader): block short word search queries

* feat(reader): make android back button exit search

* feat(reader): add exception for special character search

* refactor(reader): split chapter search script from core
2026-07-19 19:22:40 +05:30
Rajarshee Chatterjee 4404494ec9 fix(database): Repair Migration History For Scanlator Column 2026-07-19 19:21:19 +05:30
Luigi 98a2a1d725 feat: Implement Scanlator Filtering (#1864)
* feat: implement scanlator filtering

* fix test
2026-07-15 16:42:35 +05:30
Palloxin 788f6cede7 fix: Prevent Table Overflow and Clean Up CSS (#1876) (#1876) 2026-07-13 16:11:50 +05:30
Rajarshee Chatterjee 7abee71ac3 test: Generate Debug Environment Variables In Testing Workflow 2026-07-13 16:03:02 +05:30
Rajarshee Chatterjee 52d5c99c46 fix: Order EPUB Chapters Across Paginated Pages (Closes #1762) 2026-07-02 01:27:44 +05:30
Rajarshee Chatterjee 93bc5e5e4b fix: Handle Missing MAL List Entries and Null Progress (Closes #1875) 2026-07-02 01:27:44 +05:30
Jeon JaeHyeong 5d996f1388 feat(downloads): Make Chapter Download Cooldown Configurable (#1834)
* feat(downloads): make chapter download cooldown configurable

The chapter download service used a hard-coded 1s sleep between
sequential chapter writes. That worked as a polite default but is
either too long for users with patient sources or too short for
sources that 429 on tight loops. Surface the value as a single
global setting instead.

- AppSettings.chapterDownloadCooldownMs (optional, ms)
- getChapterDownloadCooldownMs(): synchronous fallback used by the
  background task runner; returns the historical 1000ms when the
  user has not configured an override (no regression on upgrade)
- downloadChapter.ts: sleep(getChapterDownloadCooldownMs()) replaces
  the previous hard-coded sleep(1000)
- New "Chapter download cooldown" entry in General Settings, placed
  directly above "Disable haptic feedback". Tapping opens a modal
  that takes seconds (decimal allowed) and writes back as ms
- The input is sanitised on every keystroke to digits + a single
  decimal point so paste / hardware keyboard input cannot smuggle
  in non-numeric characters
- Reset writes the default explicitly so the user sees feedback
  even when the override happened to equal the default

* feat(downloads): warn about source rate limits in cooldown modal

Per review feedback (#1834): the cooldown modal should make the
trade-off explicit so users don't lower the value below what their
sources can take. Add a single error-coloured line under the input.
2026-07-01 16:39:56 +05:30
Palloxin bbb2645a6f chore: Clean Reader Preview Content (#1867)
* Remove pointless break-lines (just use soft wrap to view this file)

* Encase the table in a div
2026-07-01 12:46:30 +05:30
Vikas Movva 54fedc2f53 fix(tts): Remove Leading and Trailing Quotes from Normalized Text (#1869) 2026-07-01 12:35:13 +05:30
Palloxin e8c5550256 perf: Optimize Regex for Removing Extra Paragraph Spacing (#1872) 2026-07-01 12:32:28 +05:30
Ali Hamza Malik 57b9d41b5c fix(service): Correct Notification Throttle Logic in ServiceManager.setMeta (#1873)
fix(service): correct notification throttle in ServiceManager.setMeta
2026-07-01 12:30:57 +05:30
Jeon JaeHyeong 34be7a1234 fix(native-file): prevent fatal crash when unlinking missing files (#1832)
fix(native-file): prevent crash when unlinking missing files

NativeFileSpec.unlink is codegened as a void-returning TurboModule
method, which dispatches asynchronously on the mqt_v_native thread.
Throwing "File does not exist" there bypasses every JS try/catch and
surfaces as a fatal AndroidRuntime exception, killing the process.

Make unlink idempotent (POSIX rm -f semantics): no-op when the path
does not exist. The Downloads "delete all" and individual delete
flows previously crashed whenever a chapter row had isDownloaded=true
while its on-disk folder had already been removed (e.g. after backup
restore or external cleanup). The same fix also protects useNovel,
pluginManager, fetch, epub/import, backup/local, backup/utils, and
AdvancedTab call sites that share the same native handler.
2026-05-24 10:41:17 +05:30
Jeon JaeHyeong 60f42ed624 chore(build): patch @rock-js/plugin-metro for Windows compatibility (#1830)
- patches/@rock-js__plugin-metro@0.12.12.patch: wrap absolute paths
  in pathToFileURL() so dynamic imports work on Windows
- pnpm-workspace.yaml: register the patch and pin nodeLinker: hoisted
- pnpm-lock.yaml: lock the patched version with hash
- android/settings.gradle: invoke npx.cmd on Windows for rock autolink
2026-05-24 10:40:24 +05:30
Anthony Truong 25c93afabe fix(reader): restore saved chapter progress from db if available (#1850) 2026-05-24 10:38:23 +05:30
Gaurav Tupe 2cd5a97ee8 fix: await database operations inside transactions (#1845)
fix:await db write operations in transactions
2026-05-10 19:31:30 +02:00
Patrick Loser 8f47e8fd1e Theme switcher refactor (#1839)
* use new ids

* refactor in appearance settings

* fix lint

* remove setThemeMode from handleThemeSelect

* fix type logic

* use context for theme

* shorten animation

* adjusted LanguagePickerModal

* Fix Accent Color picker

* fix bottom navigation coloring

* log error instead of crashing when no theme context is available
2026-05-07 20:25:47 +02:00
Patrick Loser eafa545b2a Use novel refactor (#1838)
* init

* feat: add zustand dependency and persistence key contract

* refactor: extract bootstrap data loading into reusable service

* refactor: move chapter mutations into store-ready action helpers

* feat: add zustand novel store with cache and core actions

* refactor: bridge novel persistence contracts for migration safety

* refactor: migrate NovelScreen domain flows to zustand selectors

* refactor: migrate NovelScreenList to selector-based store access

* refactor: move reader chapter flows onto store boundaries

* refactor: decouple useNovelSettings from broad context domain state

* refactor: align migrateNovel with stable persistence contracts

* refactor: cut novel-reader consumers to store-only context boundary

* refactor: retire legacy useNovel and route cache cleanup export

* test: update suites for store-only context boundary cutover

* test: modernize store-era mocks and add contract coverage

* test: finalize Task-15 sweep—remove dead useNovelData and lint clear mocksContract

Final validation confirms mock-contract test suite clean and target file deletion verified with zero stale references in src/ scope.

* remove imports from NovelScreen

* reworked ai output

* improvements

* implemented synchronus novel and chapter fetch

* refactor tests

* fix db tests

* Update remaining tests.

* Harden chapter actions and bootstrap flows

* Only count filtered chapters

* improved chapter insert speed by optimizing triggers

* Improved the batching function

* Added drizzle support to dbManager.batch

* removed better-sqlite3 for testing

* fix tests

* Update updateNovelChapters fucntion

* reverse read filter

* fix snackbar

* fixed page bottomsheet

* resolved paged novels showing wrong chapter number on opening

* use openPage instead of setPageIndex in chapterDrawer

* fix lint & tests

* fix type issues

* fix various smaller issues

* updated novel restore

* Delete tsconfig.tsbuildinfo
2026-05-06 16:55:55 +02:00
Jeon JaeHyeong b680a07750 fix(browse): use gesture-handler Pressable in PluginListItem (#1828)
The plugin row sits inside a react-native-gesture-handler Swipeable but
used the React Native Pressable, whose JS responder lost mouse-derived
tap events on WSA. Switch to the gesture-handler Pressable so taps share
the same responder system as the surrounding Swipeable.
2026-05-03 10:37:52 +02:00
Jeon JaeHyeong 2a919ec0f9 chore: gitignore tsconfig.tsbuildinfo build cache (#1829)
* chore: ignore *.tsbuildinfo build cache

* chore: stop tracking tsconfig.tsbuildinfo build cache
2026-05-03 10:37:08 +02:00
Definisi 01596c045a fix: Apply active filters to chapter count display (Closes #1793) (#1797)
* fix: Apply active filters to chapter count display (Closes #1793)

getChapterCount was always returning the total count regardless of
the active filter. When filtering by unread/downloaded/bookmarked,
the count shown in the novel header stayed the same as the total.

Added an optional filter parameter to getChapterCount and pass the
current settingsFilter from useNovel so the displayed count matches
the filtered chapter list.

* fix lint warning and add tests for filtered chapter count

- Use settingsFilter instead of novelSettings.filter in useNovel
  to fix the react-hooks/exhaustive-deps warning
- Add tests for getChapterCount with filter parameter
- Update chapterFilterToSQL mock in test setup to actually apply
  filter conditions so filtered count tests work correctly
2026-04-13 20:39:48 +02:00
Patrick Loser 3de93c5c09 feat: migrate from rn community cli to rock (#1812)
* migrate to rock

* Update build.yml

* Update build.yml again and changed build.gradle

* fixed actions build crash

* add flashlist to avoid bottomsheet crash

* fix Novelscreen filter highlight

* upload artifact

* update rock config

* update build.yml apk path

* update rock.config again

* Test env file

* Update build.yml

* remove react-native-config

* Update to use split apk and enable custom rock provider paths.

* added upload artifact

* remove split apk

* remove test env
2026-04-13 20:37:47 +02:00
Patrick Loser 306f5b27e2 fix: install the right version of hermes compiler
* install the correct version of the hermes compiler

* update node version of gh workflows
2026-04-06 13:59:13 +02:00
Patrick Loser 06f0f66e3e fix: Add hermes-compiler dev dep 2026-04-05 22:40:21 +02:00
Patrick Loser 64921a054e feat: Expo 55 upgrade
* init

* Change from react-native-dotenv to react-native-config as dotenv leaks
env variables that set mmkv in test mode.

* upgrade debs

* upgrade mmkv

* fix db migration for new app builds

* update reanimated

* fix tests

* fix lint

* cleanup

* Update JumpToChapterModal.tsx

* fix settings.gradle
2026-04-05 21:39:52 +02:00
Rajarshee Chatterjee 44e035ad38 chore: Update Translations (#1683)
* New translations strings.json (Polish)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations strings.json (Portuguese, Brazilian)
[ci skip]
2026-03-12 08:15:12 +05:30
Patrick Loser 1803cd071d feat: Add testing support for react code (#1763)
* first attempt

* first attempt

* added first useNovel tests

* more tests

* more tests

* resolved all test issues

* added test-utils

* add a few component tests

* add TESTIGN.md

* add testing gh action

* fixed minor issues

* fixed minor issues
2026-02-25 14:51:46 +01:00
Patrick Loser e4d7ba306b fix: Safety level may not be changed inside a transaction error (#1758) 2026-02-24 12:22:46 +01:00
Rajarshee Chatterjee cc0428752b feat: Replace TTS Notifications with Android MediaSession Controls (Closes #1711)
Replace expo-notifications based TTS controls with a native
NativeTTSMediaControl TurboModule using Android's MediaSessionCompat
and MediaStyle notification. This provides proper media integration
with lock screen controls, cover art display, and seekbar.

- Add NativeTTSMediaControl Kotlin TurboModule with MediaSession
- Support Prev, Rewind, Play/Pause, Next notification buttons
- Show novel cover as notification album art
- Add seekbar with seek-to-element support
- Replace MMKV polling with NativeEventEmitter for instant response
- Add rewind and seekTo methods to TTS in core.js
- Clean up expo-notifications TTS setup from App.tsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 09:15:54 +05:30
Patrick Loser b8c177bc55 refactor: Migrate to Drizzle and op-sqlite (#1735)
* added .jj to .gitignore

* chore: Add drizzle-orm and drizzle-kit dependencies to package.json and pnpm-lock.yaml

* update debs

* Edit configs

* create drizzle schema

* generate drizzle migration

* db manager stuff

* Delete unused files

* Update DB Manager

* Rework CategoryQueries and related

* rewrite chapterQueries

* Type safety for chapter order and filter

* Better filter handling

* added queue

* added back database triggers

* Refactor HistoryQueries and LibraryQueries

* fix type issues

* refactor StatsQueries

* refactor NovelQueries

* renamed schemas

* refactor more db calls

* refactor libraryUpdateQueries

* removed deprecated code

* added extracted queries

* fix updates screen issue

* change to op-sqlite

* Update manager.ts

* Update RepositoryQueries

* update packages

* Update NovelQueries

* Update CategoryQueries.ts

* db testing environment v1 (AI)

* Refactor App component to use Suspense for loading state and update database initialization logic. Change drizzle-orm version to 0.45.1 and adjust related database queries for improved error handling and async operations. Clean up unused code and update types for better type safety.

* updated tests

* change db location to default expo-sqlite location

* fix async/sync bugs

* resolve rebase problems

* Added tests for db creation + migration

* update drizzle

* add missing journals to migrations.js

* wrap testDbManager in Proxy to be async

* add db initialization test

* fix lint errors

* improved dbManager.batch function

* remove unused file

* fix rebase conflicts

* use getColumns instead of getTableColumns

* fix lint errors

* fix release build not being able to start

* remove disable lint rule

* improved useLiveQuery

* Resolve issue with the db queue constructor options

* Fix database query issues in library and stats

- Add missing await to all stats query functions
- Remove unnecessary type casts on query results
- Fix downloaded-only filter to use gt(0) instead of eq(1)

---------

Co-authored-by: CD-Z <github.verify097@passmail.com>
2026-02-20 01:17:01 +05:30
Rajarshee Chatterjee 7083c51dea fix: Include selectedIds in Chapter List extraData for Selection Highlight (Closes #1750)
LegendList's extraData only included downloadingChapterIds, so chapter
items weren't re-rendered when selection state changed via long press.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:37:33 +05:30
tomsLt0 f0693204e1 fix: Improve Next Chapter Selection Logic (#1744)
improve next chapter selection logic
2026-02-19 01:18:17 +05:30
Rajarshee Chatterjee 3f8071e48e chore: Release v2.0.3 2026-02-18 22:31:06 +05:30
Rajarshee Chatterjee a1c5350e8e fix: Miscellaneous UI Tweaks 2026-02-12 21:23:40 +05:30
Rajarshee Chatterjee 8f237909d5 feat: Show First Unread Chapter in ReadButton and FAB When No Last Read
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:19:52 +05:30
Rajarshee Chatterjee 11ff69d4f5 fix: Pre-Fetch Adjacent Page Chapters in Reader and Library Updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:40:37 +05:30
Rajarshee Chatterjee d15cf98742 fix: Order Chapter Pages Numerically in Queries for Accurate Retrieval (Closes #1742) 2026-02-12 01:01:20 +05:30
Rajarshee Chatterjee 29bd6612e9 refactor: Optimize Library Screen Performance and Fix Category Bug
Decouple selection state from renderScene via SelectionContext to prevent
all TabView tabs from re-rendering on every selection toggle. Extract
LibraryNovelItem as a memo-wrapped component with stable callbacks and
primitive props so only affected items re-render. Replace O(n) array
lookups with Set-based O(1) checks throughout. Memoize all inline
closures, list furniture, and style objects. Fix library[index] bug that
used the wrong array for "Update Category" menu action.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:41:59 +05:30
Rajarshee Chatterjee ab1e36d280 refactor: Update Components to Follow Material Design 3 Guidelines
Update 13 components to comply with MD3 specs:
- Switch: rewrite with MD3 dimensions (52x32 track, 16/24 thumb),
  animated thumb size, proper color tokens and border
- BottomTabBar: fix active indicator (64x32, borderRadius 16),
  label weight/color, container padding
- SearchBar: fix placeholder color, explicit borderRadius 28
- Chip: fix fontSize 14, consistent MD3 color tokens
- Actionbar: remove rounded corners, tonal elevation
- Menu: borderRadius 4, elevation 2, surfaceContainer bg
- SegmentedControl: height 40, borderRadius 20
- List: icon/subheader use onSurfaceVariant, description fontSize 14,
  divider uses outlineVariant
- BottomSheet: add 28dp top corner radius
- SwitchItem: add paddingHorizontal 16, description fontSize 14
- ConfirmationDialog: content fontSize 14
- IconButtonV2: disabled color uses onSurface at 38% opacity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:41:00 +05:30
Rajarshee Chatterjee f1fdafd37f fix: Prevent Tab Bar Height Jump and Shadow Flash on Browse Screen
Fixes initial render height jump by adding initialLayout with window dimensions to TabView. Removes shadow flash on tab switch by setting elevation to 0 and adding consistent border styling.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 22:54:35 +05:30
Rajarshee Chatterjee 4c7684a3a7 refactor: Optimize Novel Screen Performance and Fix Download State Tracking
- Optimize Novel screen components with useCallback, useMemo, and memo to prevent unnecessary re-renders
- Replace download queue array iteration with downloadingChapterIds Set for O(1) lookups
- Add loading shimmer to chapter count display (replaces "Loading..." text)
- Fix state mutation bug in useNovel updateChapter (now creates new array)
- Refactor ChapterItem API (onDeleteChapter, onDownloadChapter props for better stability)
- Track download completion via useEffect to update chapter isDownloaded state
- Fix pagination control logic and improve spacing
- Remove unnecessary local state in ChapterDownloadButtons

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 22:48:14 +05:30
Rajarshee Chatterjee 2f45db659b fix: Replace Sync Database Operations With Async to Prevent Database Lock (Closes #1716)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:49:57 +05:30
Rajarshee Chatterjee 9e38ad114d fix: Sanitize EPUB FileName to Remove Filesystem-Unsafe Characters (Closes #1480)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:31:31 +05:30
Rajarshee Chatterjee 45c4ea8ca0 fix: Handle IOException in NativeFile to Prevent EPUB Import Crash Loop (Closes #1315)
Synchronous TurboModule methods throwing IOException (FileNotFoundException)
crash the app process before JS catch blocks execute. Wrap file I/O in
try/catch to convert IOException to Exception for proper bridge serialization,
breaking the crash loop caused by stale tasks in the MMKV queue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:19:07 +05:30
Rajarshee Chatterjee d2b95ae316 fix: Use NativeFile for Custom CSS/JS Import to Fix Content URI Read Failure (Closes #1661)
Replace expo StorageAccessFramework.readAsStringAsync with NativeFile
copyFile+readFile to properly handle content:// URIs from DocumentPicker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:57:14 +05:30
Rajarshee Chatterjee 02121152b3 fix: Stabilize Scroll Handler References to Prevent Infinite Re-render Loop (Closes #1679)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:40:14 +05:30
Rajarshee Chatterjee caed965b6b fix: Prevent EPUB Import Crash from DocumentPicker Cache Cleanup (Closes #1725)
Use content:// URI directly instead of copying to cache directory, which
could be cleaned up by the system before the background import task runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:20:16 +05:30
Rajarshee Chatterjee 4088d5966b fix: Close OkHttp Response and Streams in NativeFile.downloadFile (Closes #1740)
Unclosed Response objects leaked HTTP connections and memory buffers,
causing OOM crashes when batch-downloading chapters with images.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:11:18 +05:30
Rajarshee Chatterjee a062beee87 fix: Add Max Height to Set Categories Modal (Closes #1738) 2026-02-06 14:56:32 +05:30
Patrick Loser 3095882516 Replace react-native-paper menus with custom implementation (#1733)
* improve custom Menu component

* replace react-native-paper menu

---------

Co-authored-by: CD-Z <github.verify097@passmail.com>
2026-02-01 17:29:23 +01:00
Patrick Loser 4ad0639796 Fix: Chapter loading for jump to chapter modal and chapter drawer
* updated debs

* Jump to chapter now loads not preloaded chapters

* chapter drawer scrolls to current chapter

* Load new chapters when reaching end of chapter drawer

* fix JumpToChapterModal rebase

* fix Chapter drawer current chapter position detection

---------

Co-authored-by: CD-Z <github.verify097@passmail.com>
2026-01-31 13:11:07 +01:00
Rajarshee Chatterjee b53ec3352f feat: Support Additional Plugin Setting Types (Dropdown and Checkboxes) 2026-01-28 22:23:00 +05:30
Rajarshee Chatterjee c40edd5b2c feat: Add Kitsu Tracker (Closes #416) 2026-01-25 14:50:17 +05:30
Rajarshee Chatterjee bdd42caaaf chore: Update Repository Links to Use lneader Instead of LNReader 2026-01-20 12:20:15 +05:30
Rajarshee Chatterjee ab4eee31ce feat: Allow All Safe Tags and Attributes When Sanitizing Chapter HTML (Closes #1715) 2026-01-20 09:26:32 +05:30
Rajarshee Chatterjee 3f9886a2e5 chore: Update Repository Links to Use lneader Instead of LNReader 2026-01-20 08:45:12 +05:30
kingstar1111 198b7443ae feat: Add RTL (Right-to-Left) Language Support (#1717)
feat: RTL support
2026-01-20 08:32:58 +05:30
Rajarshee Chatterjee f9c5ca303d chore: Update Indonesian Translations (Closes #1653) 2026-01-16 09:28:56 +05:30
Rajarshee Chatterjee 9922d43940 fix: Remove Missing Image References During EPUB Export (Closes #1696)
This is a cherry-pick from @IstarVin’s PR #1558. Thanks for the fix and for contributing. I cherry-picked it for the next release since the original PR had merge conflicts.
2026-01-15 09:24:43 +05:30
Rajarshee Chatterjee ae99b077d4 feat: Add @noble/ciphers for Plugins (Closes #1709) 2026-01-15 09:08:43 +05:30
Rajarshee Chatterjee 0cb9da9027 refactor: Replace withTransactionAsync With withExclusiveTransactionAsync for Improved Concurrency Handling (Closes #1636) 2026-01-15 08:20:37 +05:30
Mousica 467a97dcf7 feat: Add Volume Button Offset Setting and Fix Related UI Issues (#1685) 2025-12-24 19:13:52 +05:30
Rajarshee Chatterjee 8d15e418e7 chore: Update Translations (#1631)
* New translations strings.json (Russian)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations strings.json (Odia)
[ci skip]
2025-12-08 20:51:08 +05:30
Rajarshee Chatterjee 3849b797c9 fix: Show Plugin Settings When Update Adds New Settings (Closes #1674)
[skip ci]
2025-12-08 20:50:30 +05:30
Rajarshee Chatterjee d43c95f98c chore: Release v2.0.2
[skip ci]
2025-12-07 00:44:30 +05:30
Rajarshee Chatterjee 52d9d9ea1d feat: Continue Reading While App Runs in Background 2025-12-06 19:03:44 +05:30
Rajarshee Chatterjee 2cabc9849c fix: Skip Failed Entities During Backup Creation and Add Error Handling 2025-12-06 15:16:22 +05:30
Rajarshee Chatterjee 17049aca1c feat: Use All Chapters in Jump-to-Chapter Modal 2025-12-06 14:39:17 +05:30
Rajarshee Chatterjee 9608d59efe fix: Update Battery Level on Chapter Change (Closes #1650) 2025-12-06 13:27:19 +05:30
Rajarshee Chatterjee a7942212a0 fix: Show Page Names Instead of Numbers (Closes #1647) 2025-12-06 13:09:39 +05:30
Rajarshee Chatterjee a22aa53c51 fix: Add Unique Keys to All LegendList Implementations 2025-12-06 13:03:00 +05:30
Rajarshee Chatterjee 1ce16dc643 chore: Cleanup Unused Imports 2025-12-06 12:56:51 +05:30
Rajarshee Chatterjee 0ed6e9a463 fix: Start TTS From First Paragraph on Initial Press 2025-12-06 12:55:02 +05:30
Rajarshee Chatterjee b7ecd1d749 chore: Update Website Link (Closes #1666) 2025-12-06 12:48:42 +05:30
Rajarshee Chatterjee 44e177a5ca feat: Allow <ref> Links in Sanitised HTML (Closes #1672) 2025-12-06 12:48:21 +05:30
Rajarshee Chatterjee b09cd3d587 fix: Sort Updates Correctly (Closes #1652) 2025-12-06 12:44:11 +05:30
Rajarshee Chatterjee b70ebcf5f9 chore: Bump Version Patch to 1 in build.gradle (Closes #1662) 2025-12-06 12:20:19 +05:30
Gabriel Vasconcelos 9d58161729 feat(tts): Refactor Reading Logic to Fix Overflow and Add New Settings (#1664)
feat(tts): refactor reading logic and add new settings (voice, speed, pitch, auto page advance)

- Improved how TTS reads page content for smoother playback
- Added settings for voice selection, speed, and pitch control
- Added auto page advance support
- Added scroll-to-top behavior after advancing pages
- Updated UI to expose new TTS configuration options

[skip ci]
2025-12-06 12:18:35 +05:30
Rajarshee Chatterjee e07eed4a95 fix: Prevent App Crash in Offline Mode (Closes #1651) 2025-12-06 12:14:54 +05:30
Patrick Loser e09704c711 fix: Replace react native paper components
* updated debs

* Added Custom Menu component

* Added new BottomTabBar

---------

Co-authored-by: CD-Z <github.verify097@passmail.com>
2025-11-29 12:39:01 +01:00
Rajarshee Chatterjee 3af24f0815 fix: Improve Migration Error Handling 2025-11-16 13:18:58 +05:30
Rajarshee Chatterjee 4c0e7f6013 fix: Add Backward Compatibility to Tasks Queue 2025-11-16 12:36:42 +05:30
Rajarshee Chatterjee ea298273af fix: Categories Not Sorting
Closes #1642
2025-11-15 20:54:55 +05:30
Rajarshee Chatterjee f60a5af3f2 fix: Add Error Handling Around PRAGMA Query 2025-11-15 20:29:05 +05:30
Rajarshee Chatterjee 6ae5c1cdac chore: Release v2.0.1
[skip ci]
2025-11-15 15:37:34 +05:30
Rajarshee Chatterjee dcf667b176 fix: Rerun Migrations and Update Insert Chapter Queries 2025-11-15 14:36:57 +05:30
Rajarshee Chatterjee 3a313b9441 feat: Add Progress Notifications for Local Backup Creation and Restore 2025-11-15 11:07:48 +05:30
Rajarshee Chatterjee ae8211a4c0 fix: Use INSERT OR IGNORE for Library Updates 2025-11-15 10:08:55 +05:30
Rajarshee Chatterjee 3452478aa1 fix: Create Default Category Only If It Does Not Exist
[skip ci]
2025-11-14 21:32:01 +05:30
Rajarshee Chatterjee 0c14018996 chore: Release v2.0.0
[skip ci]
2025-11-14 21:22:04 +05:30
1199 changed files with 108797 additions and 38017 deletions
-23
View File
@@ -1,23 +0,0 @@
module.exports = {
root: true,
extends: '@react-native',
overrides: [
{
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
rules: {
'no-shadow': 'off',
'no-undef': 'off',
'no-console': 'error',
'@typescript-eslint/no-shadow': 'warn',
'react-hooks/exhaustive-deps': 'warn',
'curly': ['error', 'multi-line', 'consistent'],
'no-useless-return': 'error',
'block-scoped-var': 'error',
'no-var': 'error',
'prefer-const': 'error',
'no-dupe-else-if': 'error',
'no-duplicate-imports': 'error',
},
},
],
};
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+4 -4
View File
@@ -2,7 +2,7 @@
name: Issue report
description: Report an issue in LNReader
labels: [Bug]
labels: [bug]
body:
- type: textarea
id: reproduce-steps
@@ -45,7 +45,7 @@ body:
label: LNReader version
description: You can find your LNReader version in **More → About**.
placeholder: |
Example: "2.0.0"
Example: "2.1.4"
validations:
required: true
@@ -86,9 +86,9 @@ body:
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).
- label: If this is an issue with a plugin, I should be opening an issue in the [plugins repository](https://github.com/lnreader/lnreader-plugins/issues/new/choose).
required: true
- label: I have updated the app to version **[2.0.0](https://github.com/lnreader/lnreader/releases/latest)**.
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+3 -3
View File
@@ -2,7 +2,7 @@
name: Feature request
description: Suggest a feature to improve LNReader
labels: [Feature Request]
labels: [feature]
body:
- type: textarea
id: feature-description
@@ -32,9 +32,9 @@ body:
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).
- 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 **[2.0.0](https://github.com/LNReader/lnreader/releases/latest)**.
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+181 -67
View File
@@ -1,94 +1,208 @@
name: Build
name: Build Preview
on:
push:
branches:
- master
schedule:
- cron: '30 0 * * *'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
actions: read
contents: read
env:
CI: 'true'
EXPO_NO_TELEMETRY: '1'
NODE_ENV: 'production'
jobs:
check-preview:
name: Check Preview
runs-on: ubuntu-24.04
outputs:
should-build: ${{ steps.preview.outputs.should-build }}
steps:
- name: Check For Existing Preview
id: preview
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'workflow_dispatch') {
core.notice('Manual dispatch requested; building a new preview.')
core.setOutput('should-build', 'true')
return
}
const shortSha = context.sha.slice(0, 8)
const artifactName = `LNReader-${shortSha}`
const { data: { artifacts } } = await github.rest.actions.listArtifactsForRepo(
{
owner: context.repo.owner,
repo: context.repo.repo,
name: artifactName,
per_page: 100,
},
)
const previewExists = artifacts.some(
artifact =>
!artifact.expired &&
artifact.workflow_run?.head_sha === context.sha,
)
core.notice(
previewExists
? `A preview artifact already exists for ${shortSha}.`
: `No preview artifact exists for ${shortSha}; starting a build.`,
)
core.setOutput('should-build', String(!previewExists))
build-android:
name: Build App
runs-on: ubuntu-latest
name: APK
needs: check-preview
if: needs.check-preview.outputs.should-build == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Set Up pnpm
uses: pnpm/action-setup@v6
- name: Set Up Node.js
uses: actions/setup-node@v7
with:
node-version: '20'
node-version: '22'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm Store Directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm Cache
uses: actions/cache@v4
- name: Set Up Java
uses: actions/setup-java@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
distribution: temurin
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
- name: Install Dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile --prefer-offline
- name: Create Environment File
run: |
cat > .env << EOF
MYANIMELIST_CLIENT_ID=${{ vars.MYANIMELIST_CLIENT_ID }}
ANILIST_CLIENT_ID=${{ vars.ANILIST_CLIENT_ID }}
GIT_HASH=$(git rev-parse --short HEAD)
RELEASE_DATE=$(date --utc +'%d/%m/%y %I:%M %p %Z')
BUILD_TYPE=Github Action
EOF
- name: Make Gradlew Executable
run: cd android && chmod +x ./gradlew
- name: Set Environment Variables
run: |
set -x
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> $GITHUB_ENV
echo "COMMIT_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build Android Release
- name: Configure Release Environment
env:
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
COMMIT_ID: ${{ env.COMMIT_ID }}
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_ID }}
run: |
sed -i 's/LNReader/LNReader-r${{ env.COMMIT_COUNT }}(${{ env.COMMIT_ID }})/g' android/app/src/main/res/values/strings.xml
cd android && ./gradlew assembleRelease -PcustomAppId=com.rajarsheechatterjee.LNReader.commit_${{ env.COMMIT_ID }} --build-cache
mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}.apk
pnpm generate:env:release \
--build-type "GitHub Action" \
--node-env "production"
- name: Upload Release Artifact
env:
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
COMMIT_ID: ${{ env.COMMIT_ID }}
- name: Generate Native Android Project
run: |
pnpm exec expo prebuild \
--platform android \
--clean \
--no-install \
--non-interactive
- name: Configure Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-provider: enhanced
cache-read-only: >-
${{ github.ref_name != github.event.repository.default_branch }}
cache-cleanup: on-success
add-job-summary: always
- name: Generate Preview Metadata
run: |
short_sha="${GITHUB_SHA::8}"
base_version_name="$(node --print "require('./app.json').expo.version")"
base_version_code="$(node --print "require('./app.json').expo.android.versionCode")"
echo "PRERELEASE_VERSION_CODE=$((base_version_code + GITHUB_RUN_NUMBER))" >> "$GITHUB_ENV"
echo "PRERELEASE_VERSION_NAME=${base_version_name}-pre.r${GITHUB_RUN_NUMBER}.${short_sha}" >> "$GITHUB_ENV"
echo "PRERELEASE_ARTIFACT_NAME=LNReader-${short_sha}" >> "$GITHUB_ENV"
echo "PRERELEASE_APK_NAME=LNReader-r${GITHUB_RUN_NUMBER}-${short_sha}.apk" >> "$GITHUB_ENV"
- name: Build Preview APK
working-directory: android
run: |
mkdir -p app/build/intermediates/sourcemaps/react/preRelease
./gradlew :app:assemblePreRelease \
--build-cache \
--parallel \
-PpreReleaseVersionCode="$PRERELEASE_VERSION_CODE" \
-PpreReleaseVersionName="$PRERELEASE_VERSION_NAME" \
--stacktrace
mv \
app/build/outputs/apk/preRelease/app-preRelease.apk \
"app/build/outputs/apk/preRelease/$PRERELEASE_APK_NAME"
- name: Upload Preview Sourcemap
uses: actions/upload-artifact@v4
with:
name: LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}
path: android/app/build/outputs/apk/release/LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}.apk
retention-days: 30
name: ${{ env.PRERELEASE_ARTIFACT_NAME }}-sourcemap
path: android/app/build/generated/sourcemaps/react/preRelease/index.android.bundle.map
if-no-files-found: error
retention-days: 14
compression-level: 0
- name: Upload Preview APK
id: upload-nightly
uses: actions/upload-artifact@v4
with:
name: ${{ env.PRERELEASE_ARTIFACT_NAME }}
path: android/app/build/outputs/apk/preRelease/${{ env.PRERELEASE_APK_NAME }}
if-no-files-found: error
retention-days: 14
compression-level: 0
- name: Check Discord Notification
id: discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
run: |
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
fi
- name: Notify Discord
if: steps.discord.outputs.configured == 'true'
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
ARTIFACT_URL: ${{ steps.upload-nightly.outputs.artifact-url }}
run: |
short_sha="${GITHUB_SHA::8}"
message="[${GITHUB_REPOSITORY}] New nightly build published: \`${short_sha}\`"
payload=$(jq -n \
--arg title "$message" \
--arg url "$ARTIFACT_URL" \
'{
username: "GitHub",
avatar_url: "https://github.com/github.png",
embeds: [{
author: {
name: "github-actions[bot]",
icon_url: "https://github.com/github.png"
},
title: $title,
url: $url,
color: 3092790
}]
}')
curl --fail-with-body \
--request POST \
--header "Content-Type: application/json" \
--data "$payload" \
"$DISCORD_WEBHOOK_URL"
+4 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
@@ -44,5 +44,8 @@ jobs:
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Generate Environment Variables
run: pnpm generate:env:debug
- name: Run ESLint
run: pnpm run lint
+232
View File
@@ -0,0 +1,232 @@
name: Release Android
run-name: Draft ${{ inputs.bump }} Android release
on:
workflow_dispatch:
inputs:
bump:
description: Version component to increment
required: true
default: Patch
type: choice
options:
- Patch
- Minor
- Major
concurrency:
group: release-android
cancel-in-progress: false
permissions:
contents: write
env:
CI: 'true'
EXPO_NO_TELEMETRY: '1'
NODE_ENV: 'production'
RELEASE_BUMP: ${{ inputs.bump }}
jobs:
release-android:
name: Build Draft Release
if: >-
github.ref == 'refs/heads/master' &&
github.actor_id == vars.RELEASE_ACTOR_ID &&
github.triggering_actor == vars.RELEASE_ACTOR_LOGIN
environment: release
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- name: Checkout Repository
uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: true
- name: Set Up pnpm
uses: pnpm/action-setup@v6
- name: Set Up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Set Up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
- name: Install Dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Resolve Release Version
id: version
run: |
RELEASE_VERSION="$(
node scripts/resolve-release-version.cjs "$RELEASE_BUMP"
)"
echo "Releasing $RELEASE_BUMP version: $RELEASE_VERSION"
echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV"
echo "version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
- name: Validate Release
env:
GH_TOKEN: ${{ github.token }}
run: |
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Version must use stable semver, for example 2.0.4"
exit 1
fi
if git rev-parse --verify --quiet "refs/tags/v$RELEASE_VERSION"; then
echo "Tag v$RELEASE_VERSION already exists"
exit 1
fi
if gh release view "v$RELEASE_VERSION" >/dev/null 2>&1; then
echo "Release v$RELEASE_VERSION already exists"
exit 1
fi
- name: Prepare Release Commit
run: |
pnpm release:prepare "$RELEASE_VERSION"
if git diff --quiet -- \
package.json \
app.json \
.github/ISSUE_TEMPLATE/report_issue.yml \
.github/ISSUE_TEMPLATE/request_feature.yml; then
CURRENT_VERSION="$(node -p "require('./package.json').version")"
if [ "$CURRENT_VERSION" != "$RELEASE_VERSION" ]; then
echo "No release changes were generated for $RELEASE_VERSION"
exit 1
fi
echo "Release version $RELEASE_VERSION is already committed; resuming"
else
git diff --check
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add \
package.json \
app.json \
.github/ISSUE_TEMPLATE/report_issue.yml \
.github/ISSUE_TEMPLATE/request_feature.yml
git commit -m "chore: Release v$RELEASE_VERSION"
fi
git fetch origin master
REMOTE_VERSION="$(
node -e "
const { execFileSync } = require('child_process');
const contents = execFileSync(
'git',
['show', 'origin/master:package.json'],
{ encoding: 'utf8' },
);
process.stdout.write(JSON.parse(contents).version);
"
)"
if [ "$REMOTE_VERSION" = "$RELEASE_VERSION" ]; then
echo "Release commit is already on master; resuming from it"
git checkout --detach origin/master
echo "RELEASE_ALREADY_PUSHED=true" >> "$GITHUB_ENV"
else
echo "RELEASE_ALREADY_PUSHED=false" >> "$GITHUB_ENV"
fi
echo "RELEASE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Configure Release Environment
env:
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_ID }}
run: |
pnpm generate:env:release \
--build-type "Release" \
--node-env "production" \
--myanimelist-client-id "$MYANIMELIST_CLIENT_ID" \
--anilist-client-id "$ANILIST_CLIENT_ID"
- name: Generate Native Android Project
run: |
pnpm exec expo prebuild \
--platform android \
--clean \
--no-install
- name: Configure Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-provider: enhanced
cache-cleanup: on-success
add-job-summary: always
- name: Build Release APKs
working-directory: android
run: |
mkdir -p app/build/intermediates/sourcemaps/react/release
./gradlew :app:assembleRelease \
--build-cache \
--parallel \
-PreleaseAbiSplits=true \
--stacktrace
- name: Package Release APKs
run: pnpm release:package "$RELEASE_VERSION"
- name: Upload Sourcemaps
run: |
cp android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
"release-artifacts/LNReader-v${RELEASE_VERSION}-sourcemap.map"
- name: Verify APK Signatures
run: |
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner |
sort -V |
tail -1)"
if [ -z "$APKSIGNER" ]; then
echo "Android apksigner was not found"
exit 1
fi
for APK in release-artifacts/*.apk; do
"$APKSIGNER" verify "$APK"
done
- name: Upload Release APKs
uses: actions/upload-artifact@v4
with:
name: LNReader-v${{ steps.version.outputs.version }}
path: release-artifacts/
if-no-files-found: error
retention-days: 30
compression-level: 0
- name: Push Release Commit
run: |
if [ "$RELEASE_ALREADY_PUSHED" = "true" ]; then
echo "Release commit is already on master"
else
git push origin HEAD:master
fi
- name: Create Draft GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "v$RELEASE_VERSION" \
release-artifacts/* \
--target "$RELEASE_SHA" \
--title "LNReader v$RELEASE_VERSION" \
--generate-notes \
--draft
+51
View File
@@ -0,0 +1,51 @@
name: Tests
on:
push:
branches:
- master
pull_request:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Jest
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm Store Path
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Generate Environment
run: pnpm generate:env:debug
- name: Run Tests
run: pnpm run test
+20 -14
View File
@@ -1,36 +1,39 @@
# Temporarily Disabled
name: Type Check
on:
workflow_dispatch:
# push:
# branches:
# - master
# pull_request:
# branches:
# - master
push:
branches:
- master
pull_request:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Type Checking
type-check:
name: TypeScript
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout Repository
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm Store Directory
- name: Get pnpm Store Path
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm Cache
- name: Cache pnpm
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
@@ -41,5 +44,8 @@ jobs:
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Generate Environment
run: pnpm generate:env:debug
- name: Check Types
run: pnpm run type-check
+27 -1
View File
@@ -8,7 +8,8 @@ npm-debug.*
*.mobileprovision
*.orig.*
web-build/
.env
.*env
.env.local
reader_playground/index.html
# macOS
.DS_Store
@@ -88,3 +89,28 @@ flake.lock
# pnpm
.pnpm-store
src/generated/**/*
.cursor/
.agents/
.claude/
.jj/
.sisyphus/
# Rock
.rock/
# TypeScript
*.tsbuildinfo
*/**/org.eclipse.buildship.core.prefs
*/**/org.eclipse.jdt.core.prefs
*/**/.classpath
*/**/.project
android/
release-artifacts/
!modules/**/android/
!plugins/android/
ios/
!modules/**/ios/
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm run type-check
+132
View File
@@ -0,0 +1,132 @@
# AGENTS.md
Guidance for AI coding agents working in this repository.
## Project overview
LNReader is a free, open-source light novel reader for Android, built with
React Native (Expo) and TypeScript. Content is fetched at runtime by
JavaScript "plugins" (per-source scrapers) rather than bundled with the app.
Plugin _requests_ live in a separate repo, [lnreader-plugins](https://github.com/lnreader/lnreader-plugins);
this repo is the reader app itself.
## Setup
```bash
pnpm install
```
- Node >= 22.11.0, pnpm (see `packageManager` in `package.json`, currently pnpm@11.20.0).
- This is a React Native/Expo app targeting Android; there is no web or iOS
build target maintained here. Building/running the app requires an Android
SDK and a device/emulator, which is generally not available in an agent
sandbox — prefer running lint, type-check, and tests to verify changes.
- See `CONTRIBUTING.md` (and `CONTRIBUTING-NIX.md` for a Nix-based dev shell)
for full local Android setup instructions.
## Commands
Run these from the repo root:
```bash
pnpm run lint # eslint ./src
pnpm run lint:fix # eslint --fix
pnpm run format:check # prettier --check
pnpm run format # prettier --write
pnpm run type-check # tsc --noEmit
pnpm run check # format:check + lint + type-check (run before finishing any change)
pnpm test # jest (all projects)
pnpm run test:rn # jest --selectProjects rn (React Native/component/hook tests)
pnpm run test:db # jest --selectProjects db (database/query tests, node env)
pnpm run test:watch
pnpm run test:coverage
```
Always run `pnpm run check` (or at minimum lint + type-check + the relevant
test project) before considering a change complete. A pre-commit hook
(`husky` + `lint-staged`) runs prettier/eslint on staged files, and a
pre-push hook runs `type-check`, so failures here will also block commits/pushes.
## Code style
- TypeScript, functional React components, hooks-based state.
- Formatting is enforced by Prettier (`.prettierrc.js`) — don't hand-format,
run `pnpm run format` instead.
- ESLint config is `eslint.config.js` (flat config, based on `eslint-config-expo`).
Notable rules: `no-console` is an error (use the project's logging/toast
utilities, not `console.*`), `prefer-const`, `no-var`, and several
`react-hooks/*` correctness rules are enforced.
- Use the `@alias/*` path aliases defined in `tsconfig.json` (and mirrored in
`jest.config.js`) instead of deep relative imports, e.g. `@components`,
`@hooks`, `@database/*`, `@screens/*`, `@services/*`, `@utils/*`,
`@api/*`, `@theme/*`, `@plugins/*`, `@navigators/*`, `@i18n/*`, `@type/*`.
- Don't add comments that just restate what the code does; only comment
non-obvious "why" (workarounds, invariants, native/platform quirks).
## Localization (i18n)
- `src/i18n/languages/en/strings.json` is the source-of-truth string file —
it's the only locale you should hand-edit. Every other folder under
`src/i18n/languages/*` is a translation synced automatically from Crowdin
(see `crowdin.yml`); don't edit or "fix" translated strings in those files.
- After adding/changing keys in `en/strings.json`, run
`pnpm run generate:string-types` to regenerate the associated TypeScript types.
## Repository layout
- `src/api` – external API clients (drive/cloud backup providers, etc.)
- `src/components` – shared/reusable UI components
- `src/database` – SQLite (op-sqlite/Drizzle) schema, queries, and migrations
helpers; `src/database/queries/*Queries.ts` are the main data-access layer
- `src/hooks` – shared and "persisted" (MMKV-backed) React hooks
- `src/i18n` – translated strings (see Crowdin integration in `crowdin.yml`)
- `src/navigators` – React Navigation stack/tab setup
- `src/plugins` – plugin manager and plugin type definitions (not the plugin
source scrapers themselves, which live in the separate lnreader-plugins repo)
- `src/screens` – feature screens (library, browse, novel, reader, settings, etc.)
- `src/services` – background tasks, downloads, backups, trackers, EPUB export, updates
- `src/theme` – Material3 theming
- `src/utils` – general utilities
- `drizzle/` – generated SQL migrations (see `pnpm run generate:db-migration`)
- `modules/` – native modules (Nitro modules: epub, tts, file, zip-archive, etc.)
- `test/` – Jest global mocks and the shared `test-utils.tsx` render helpers
- `scripts/` – Node scripts for env file generation, release packaging, etc.
## Testing notes
- Don't add tests for the sake of coverage. Write tests for user-facing
interactions (screens, components a user directly touches) and for core
complex logic (parsing, database queries, sync/backup, chapter/novel
matching, etc.), not for trivial passthroughs, simple type/prop plumbing,
or generated/boilerplate code.
- Tests live in `__tests__` directories next to the code under test and use
Jest + `@testing-library/react-native`.
- Jest is split into two projects (see `jest.config.js`):
- `db` — pure logic/query tests under `src/database`, run in a Node environment.
- `rn` — component/hook/integration tests under `src/`, run with the `jest-expo` preset.
- Import the shared render helpers from `@test-utils` (`render`, `renderNovel`,
`AllTheProviders`) rather than using `@testing-library/react-native`'s
`render` directly, so providers (theme, gesture handler, safe area, novel
context) are set up consistently.
- Mock at module level with `jest.mock(...)`, not inside `beforeEach` — see
`TESTING.md` for the reasoning and common pitfalls (ESM transform errors,
`mockReturnValue` failures, test isolation).
- Global mocks for native modules and database queries live in `test/mocks/`;
hook-specific mocks live in `src/hooks/__mocks__` and `src/hooks/__tests__/mocks.ts`.
## Database changes
- Schema lives under `src/database/schema`; the ORM is Drizzle
(`drizzle-orm`, config in `drizzle.config.ts`).
- After changing the schema, generate a migration rather than hand-writing
SQL: `pnpm run generate:db-migration`. Do not edit files under `drizzle/`
by hand.
## PR / commit conventions
- Keep changes scoped and consistent with existing patterns in the touched
directory; prefer editing existing files over introducing new abstractions.
- Match existing code style; don't reformat unrelated code.
- If you are an AI agent opening a PR or making a commit, attribute it as
such (e.g. a "Generated with <agent/tool name>" note in the commit
message/PR description, and a co-author trailer if your tool supports one).
+93 -89
View File
@@ -1,118 +1,122 @@
import 'react-native-url-polyfill/auto';
import { enableFreeze } from 'react-native-screens';
enableFreeze(true);
import React, { useEffect } from 'react';
import { PropsWithChildren, Suspense, useEffect, useMemo } from 'react';
import { StatusBar, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import LottieSplashScreen from 'react-native-lottie-splash-screen';
import * as SplashScreen from 'expo-splash-screen';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider as PaperProvider } from 'react-native-paper';
import * as Notifications from 'expo-notifications';
import {
MD3DarkTheme,
MD3LightTheme,
Provider as PaperProvider,
} from 'react-native-paper';
import AppErrorBoundary, {
ErrorFallback,
} from '@components/AppErrorBoundary/AppErrorBoundary';
import { useDatabaseInitialization } from '@hooks';
import CrashRecoveryGate from '@components/AppErrorBoundary/CrashRecoveryGate';
import Main from './src/navigators/Main';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { useInitDatabase } from '@database/db';
import { useInitializeAppServices } from '@hooks/common/useInitializeAppServices';
import { opSqliteAdapter } from './src/rozenite/opSqliteAdapter';
import { useRozeniteSqlitePlugin } from '@rozenite/sqlite-plugin';
import { ThemeProvider, useTheme } from '@hooks/persisted/useTheme';
import { KeyboardProvider } from 'react-native-keyboard-controller';
enableFreeze(true);
const sqliteAdapters = __DEV__ && opSqliteAdapter ? [opSqliteAdapter] : [];
/**
* The Android window background is resolved from the system light/dark setting,
* so it is bright white whenever the system is light – regardless of the theme
* picked inside the app. Nothing between it and the navigators paints a
* background of its own, so every frame in which no screen is drawn (mounting a
* nested navigator, freezing the screen being left) shows through as a flash.
* Painting the root view keeps the window covered at all times.
*/
const ThemedRootView = ({ children }: PropsWithChildren) => {
const theme = useTheme();
return (
<GestureHandlerRootView
style={[styles.flex, { backgroundColor: theme.background }]}
>
{children}
</GestureHandlerRootView>
);
};
const ThemedPaperProvider = ({ children }: PropsWithChildren) => {
const theme = useTheme();
const paperTheme = useMemo(() => {
const baseTheme = theme.isDark ? MD3DarkTheme : MD3LightTheme;
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldPlaySound: false,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true,
};
},
});
Notifications.setNotificationCategoryAsync('TTS_CONTROLS', [
{
identifier: 'TTS_PLAY_PAUSE',
buttonTitle: '▶️ Play',
options: {
opensAppToForeground: false,
},
},
{
identifier: 'TTS_STOP',
buttonTitle: '⏹️ Stop',
options: {
opensAppToForeground: false,
},
},
{
identifier: 'TTS_NEXT',
buttonTitle: '⏭️ Next',
options: {
opensAppToForeground: false,
},
},
]);
Notifications.setNotificationChannelAsync('tts-controls', {
name: 'TTS Controls',
description: 'Text-to-Speech playback controls',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [],
enableLights: false,
enableVibrate: false,
});
const App = () => {
const { isDbReady, dbError, retryInitialization } =
useDatabaseInitialization();
useEffect(() => {
if (isDbReady || dbError) {
LottieSplashScreen.hide();
}
}, [isDbReady, dbError]);
useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener(
response => {
const actionIdentifier = response.actionIdentifier;
if (actionIdentifier.startsWith('TTS_')) {
const { setTTSAction } = require('@utils/ttsNotification');
setTTSAction(actionIdentifier);
}
...baseTheme,
colors: {
...baseTheme.colors,
...theme,
},
);
return () => {
subscription.remove();
};
}, []);
}, [theme]);
if (dbError) {
return <ErrorFallback error={dbError} resetError={retryInitialization} />;
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
};
const Application = () => {
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
const { success: databaseReady, error: databaseError } = useInitDatabase();
const { ready: servicesReady, error: servicesError } =
useInitializeAppServices(Boolean(databaseReady));
useEffect(() => {
if ((databaseReady && servicesReady) || databaseError || servicesError) {
SplashScreen.hideAsync();
}
}, [databaseReady, databaseError, servicesReady, servicesError]);
const initializationError = databaseError || servicesError;
if (initializationError) {
return (
<ErrorFallback error={initializationError} resetError={() => null} />
);
}
if (!isDbReady) {
if (!databaseReady || !servicesReady) {
return null;
}
return (
<GestureHandlerRootView style={styles.flex}>
<AppErrorBoundary>
<SafeAreaProvider>
<PaperProvider>
<BottomSheetModalProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<Main />
</BottomSheetModalProvider>
</PaperProvider>
</SafeAreaProvider>
</AppErrorBoundary>
</GestureHandlerRootView>
<ThemedRootView>
<KeyboardProvider>
<AppErrorBoundary>
<SafeAreaProvider>
<ThemedPaperProvider>
<BottomSheetModalProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<Main />
</BottomSheetModalProvider>
</ThemedPaperProvider>
</SafeAreaProvider>
</AppErrorBoundary>
</KeyboardProvider>
</ThemedRootView>
);
};
const App = () => (
<Suspense fallback={null}>
<ThemeProvider>
<CrashRecoveryGate>
<Application />
</CrashRecoveryGate>
</ThemeProvider>
</Suspense>
);
export default App;
const styles = StyleSheet.create({
+1 -1
View File
@@ -6,7 +6,7 @@ This document outlines how to set up your React Native development environment u
This was tested on an Arch Linux system with AMD CPU and GPU. There are likely problems with NVIDIA GPUs.
Compatibility with Microsoft WSL2 is not guaranteed, but should work withoud major issues, except for the emulator.
Compatibility with Microsoft WSL2 is not guaranteed, but should work without major issues, except for the emulator.
## Prerequisites
+6 -6
View File
@@ -2,14 +2,14 @@
Contributions are welcome and are greatly appreciated!
## Setup your environment with nix
## Setup Your Environment with Nix
If you are on a Linux system, you can install the nix package manager and use the nix flakes to set up your development environment.
See [CONTRIBUTING-NIX.md](CONTRIBUTING-NIX.md)
See [CONTRIBUTING-NIX.md](CONTRIBUTING-NIX.md).
## Setting up your environment
## Setting Up Your Environment
After forking to your own github org or account, do the following steps to get started:
After forking to your own GitHub organization or account, take the following steps to get started:
```bash
# prerequisites
@@ -35,7 +35,7 @@ pnpm run build:release:android
### Developing on Android
You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
You will need an Android device or emulator connected to your computer as well as an IDE of your choice (e.g., VS Code).
```bash
# prerequisites
@@ -57,7 +57,7 @@ pnpm run dev:android
This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
It is recommended that you install an eslint plugin for your editor of choice when working on this
codebase, however you can always check to see if the source code is compliant by running:
codebase, however, you can always check to see if the source code is compliant by running:
```bash
pnpm run lint
+73
View File
@@ -0,0 +1,73 @@
# Debugging Production Builds
Symbolicate production stack traces from LNReader using uploaded sourcemaps.
## Prerequisites
- **Node.js** (v18 or later)
- The **sourcemap** matching the crashing APK version
- A **stack trace** from either `adb logcat` (device attached) or pasted from a Discord / GitHub issue report
## Getting the Sourcemap
### Release builds
Sourcemaps are uploaded as GitHub release assets. Download the one matching the crashing version:
```
LNReader-v{version}-sourcemap.map
```
Example for `v2.5.0`:
1. Go to the [releases page](https://github.com/lnreader/lnreader/releases)
2. Find release `v2.5.0`
3. Under **Assets**, download `LNReader-v2.5.0-sourcemap.map`
### Preview / nightly builds
Sourcemaps are uploaded as a separate artifact alongside the preview APK, named `LNReader-{sha}-sourcemap`.
1. Go to the [Actions tab](https://github.com/lnreader/lnreader/actions) → **Build Preview**
2. Find the workflow run matching the build hash
3. Under **Artifacts**, download the one with the `-sourcemap` suffix
## Symbolicating a Stack Trace
Save the raw stack trace to a file (from `adb logcat`, or paste from Discord/GitHub into `trace.txt`), then run:
```bash
npx metro-symbolicate <sourcemap.map> < trace.txt
```
Or pipe it directly:
```bash
adb logcat -d | npx metro-symbolicate <sourcemap.map>
```
## Example
Before (Hermes-obfuscated):
```
com.lnreader E Error: Unexpected token
at 93795 (address at index.js:1:729193)
at 28471 (address at index.js:1:219482)
at 10384 (address at index.js:1:81729)
```
After symbolication:
```
com.lnreader E Error: Unexpected token
at handleResponse (src/api/plugins.ts:120:12)
at fetchPlugin (src/hooks/usePlugins.ts:45:8)
at getPluginList (src/screens/browse/BrowseScreen.tsx:89:20)
```
## Troubleshooting
- **Version mismatch**: the sourcemap must match the exact APK build. A mismatched version produces wrong file names and line numbers.
- **No sourcemap artifact**: verify the workflow that built the APK ran successfully and that the "Upload Sourcemaps" / "Upload Preview Sourcemap" step completed without errors.
- **Hermes bytecode**: Hermes compiles JS to bytecode. The sourcemap is generated during this step and maps bytecode offsets back to source. `metro-symbolicate` handles this transparently.
+2 -1
View File
@@ -13,4 +13,5 @@ gem 'concurrent-ruby', '< 1.3.4'
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
gem 'mutex_m'
gem 'nkf'
+12 -12
View File
@@ -14,15 +14,15 @@
<a href="https://discord.gg/QdcWN4MD63">
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=5865F2&color=4752C4&label=discord&style=flat">
</a>
<a href="https://github.com/LNReader/lnreader/releases">
<img alt="GitHub Downloads" src="https://img.shields.io/github/downloads/LNReader/lnreader/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat">
<a href="https://github.com/lnreader/lnreader/releases">
<img alt="GitHub Downloads" src="https://img.shields.io/github/downloads/lnreader/lnreader/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat">
</a>
</div>
<div align="center">
<img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LNReader/lnreader/build.yml?labelColor=27303D&style=flat">
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader?labelColor=27303D&color=1a73e8&style=flat">
<img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/lnreader/lnreader/build.yml?labelColor=27303D&style=flat">
<a href="https://github.com/lnreader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/lnreader/lnreader?labelColor=27303D&color=1a73e8&style=flat">
</a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader">
<img src="https://badges.crowdin.net/lnreader/localized.svg">
@@ -32,16 +32,16 @@
<h2 align="center">Download</h2>
<p align="center">
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader?label=Stable&labelColor=0d7377&color=084c4e&style=flat">
<a href="https://github.com/lnreader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/lnreader/lnreader?label=Stable&labelColor=0d7377&color=084c4e&style=flat">
</a>
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader?include_prereleases&sort=semver&label=Beta&labelColor=3d3d5c&color=2a2a47&style=flat">
<a href="https://github.com/lnreader/lnreader/actions/workflows/build.yml">
<img alt="Build Preview" src="https://github.com/lnreader/lnreader/actions/workflows/build.yml/badge.svg">
</a>
</p>
<p align="center">
Get the app from our <a href="https://github.com/LNReader/lnreader/releases">releases page</a>.
Get the app from our <a href="https://github.com/lnreader/lnreader/releases">releases page</a>.
</p>
<p align="center">
@@ -58,7 +58,7 @@
LNReader does not have any affiliation with the content providers available.
Plugin requests should be created at [lnreader-plugins](https://github.com/LNReader/lnreader-plugins).
Plugin requests should be created at [lnreader-plugins](https://github.com/lnreader/lnreader-plugins).
## Translation
@@ -70,4 +70,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md)
## License
[MIT](https://github.com/LNReader/lnreader/blob/main/LICENSE)
[MIT](https://github.com/lnreader/lnreader/blob/main/LICENSE)
+80
View File
@@ -0,0 +1,80 @@
# Testing Guide for LNReader
This guide explains how to write tests in this React Native project using Jest and React Testing Library.
## Existing Mocks
### Global Mocks
The project has global mocks configured in Jest. These are automatically applied:
- `test/mocks/` - Global mocks for native modules (react-native-mmkv, react-navigation, all database queries, etc.)
- `src/hooks/__mocks__/index.ts` - Hook-specific mocks (showToast, getString, parseChapterNumber, etc.)
- `src/hooks/__tests__/mocks.ts` - Extended mocks for persisted hooks
### Using @test-utils
There is a custom render wrapper at `test/test-utils.tsx` with:
- `render` - wraps with GestureHandlerRootView, SafeAreaProvider, PaperProvider, etc.
- `renderNovel` - includes NovelContextProvider
- `AllTheProviders` - the full provider wrapper
Usage:
```typescript
import { render, renderNovel } from '@test-utils';
```
## Common Issues
### 1. ESM Modules Not Transforming
If you see `Cannot use import statement outside a module`, you need to add mocks for the module:
```typescript
jest.mock('@hooks/persisted/usePlugins');
// Add more specific mocks as needed
```
### 2. Mock Functions Not Working
If `mockReturnValue` throws "not a function", create mock functions at the module level:
```typescript
// CORRECT: Module-level mock functions
const mockUseTheme = jest.fn();
jest.mock('@hooks/persisted', () => ({
useTheme: () => mockUseTheme(),
}));
// INCORRECT: Trying to use jest.Mock type casting
// (useTheme as jest.Mock).mockReturnValue(...) // This fails!
```
### 3. Test Isolation
Tests must mock at module level, not in `beforeEach`:
```typescript
// CORRECT
const mockFn = jest.fn();
jest.mock('module', () => ({ useHook: () => mockFn() }));
// INCORRECT - mocks get reset between tests
jest.mock('module');
beforeEach(() => {
// This doesn't work properly
});
```
## Running Tests
```bash
pnpm test # Run all tests
pnpm test:watch # Watch mode
pnpm test:coverage # With coverage
pnpm test:rn # React Native only
pnpm test:db # Database only
```
-136
View File
@@ -1,136 +0,0 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
apply plugin: 'org.jetbrains.kotlin.android'
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
// cliFile = file("../../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle"
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/index.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC).
*
* For example, to use the international variant, you can use:
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
ext {
versionMajor = 2
versionMinor = 0
versionPatch = 0
}
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.rajarsheechatterjee.LNReader"
defaultConfig {
applicationId project.hasProperty('customAppId') ? project.getProperty('customAppId') : 'com.rajarsheechatterjee.LNReader'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
// Generated version code. Supports versions up to 1024.1024.2048
versionCode ((((versionMajor << 10) | versionMinor) << 11) | versionPatch)
versionName "$versionMajor.$versionMinor.$versionPatch"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
applicationIdSuffix 'debug'
versionNameSuffix '-debug'
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
kotlinOptions {
jvmTarget = '17'
}
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation 'androidx.core:core-ktx:1.15.0'
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
Binary file not shown.
-47
View File
@@ -1,47 +0,0 @@
{
"project_info": {
"project_number": "523872485654",
"project_id": "lnreader-backup",
"storage_bucket": "lnreader-backup.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:523872485654:android:169cd062458db962ce1037",
"android_client_info": {
"package_name": "com.rajarsheechatterjee.LNReader"
}
},
"oauth_client": [
{
"client_id": "523872485654-14jtut8orr7dbrk2chea279d7k8889sr.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.rajarsheechatterjee.LNReader",
"certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625"
}
},
{
"client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBH_j-0Jyo9aFJ_KV8Nr3te2hs_L_ZYhrE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
-14
View File
@@ -1,14 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
-keep class com.facebook.hermes.unicode.** { *; }
-keep class com.facebook.jni.** { *; }
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning"/>
</manifest>
@@ -1,3 +0,0 @@
<resources>
<string name="app_name">LNReader debug</string>
</resources>
-70
View File
@@ -1,70 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
<application
android:name=".MainApplication"
android:largeHeap="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:supportsRtl="true">
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
android:resource="@drawable/notification_icon" />
<meta-data
android:name="expo.modules.notifications.default_notification_color"
android:resource="@color/notification_icon_color" />
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true"
android:theme="@style/Theme.App.SplashScreen"
android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lnreader" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" android:foregroundServiceType="shortService" />
</application>
</manifest>
-48
View File
@@ -1,48 +0,0 @@
#TTS-Controller {
position: fixed;
top: 50%;
left: 20px;
opacity: 0.5;
}
#TTS-Controller button {
background: color-mix(
in srgb,
var(--readerSettings-theme) 85%,
var(--readerSettings-textColor) 15%
);
outline: none;
border-width: 1px;
border-style: solid;
border-color: color-mix(
in srgb,
var(--readerSettings-textColor) 30%,
var(--readerSettings-theme) 70%
);
display: flex;
justify-items: center;
align-items: center;
padding: 4px;
border-radius: 100%;
transition: 0.5s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
#TTS-Controller.active {
opacity: 1;
}
#TTS-Controller.active button {
padding: 16px;
}
#TTS-Controller svg {
fill: var(--readerSettings-textColor);
width: 20px;
height: 20px;
}
#TTS-Controller.active svg {
width: 24px;
height: 24px;
}
-643
View File
@@ -1,643 +0,0 @@
/* eslint-disable no-console */
window.reader = new (function () {
const {
readerSettings,
chapterGeneralSettings,
novel,
chapter,
nextChapter,
prevChapter,
batteryLevel,
autoSaveInterval,
DEBUG,
strings,
} = initialReaderConfig;
// state
this.hidden = van.state(true);
this.batteryLevel = van.state(batteryLevel);
this.readerSettings = van.state(readerSettings);
this.generalSettings = van.state(chapterGeneralSettings);
this.chapterElement = document.querySelector('#LNReader-chapter');
this.selection = window.getSelection();
this.viewport = document.querySelector('meta[name=viewport]');
this.novel = novel;
this.chapter = chapter;
this.nextChapter = nextChapter;
this.prevChapter = prevChapter;
this.strings = strings;
this.autoSaveInterval = autoSaveInterval;
this.rawHTML = this.chapterElement.innerHTML;
//layout props
this.paddingTop = parseInt(
getComputedStyle(document.querySelector('body')).getPropertyValue(
'padding-top',
),
10,
);
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
this.layoutHeight = window.screen.height;
this.layoutWidth = window.screen.width;
this.layoutEvent = undefined;
this.chapterEndingVisible = van.state(false);
this.post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
this.refresh = () => {
if (this.generalSettings.val.pageReader) {
this.chapterWidth = this.chapterElement.scrollWidth;
} else {
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
}
};
van.derive(() => {
const settings = this.readerSettings.val;
document.documentElement.style.setProperty(
'--readerSettings-theme',
settings.theme,
);
document.documentElement.style.setProperty(
'--readerSettings-padding',
settings.padding + 'px',
);
document.documentElement.style.setProperty(
'--readerSettings-textSize',
settings.textSize + 'px',
);
document.documentElement.style.setProperty(
'--readerSettings-textColor',
settings.textColor,
);
document.documentElement.style.setProperty(
'--readerSettings-textAlign',
settings.textAlign,
);
document.documentElement.style.setProperty(
'--readerSettings-lineHeight',
settings.lineHeight,
);
document.documentElement.style.setProperty(
'--readerSettings-fontFamily',
settings.fontFamily,
);
if (settings.fontFamily) {
new FontFace(
settings.fontFamily,
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
)
.load()
.then(function (loadedFont) {
document.fonts.add(loadedFont);
});
} else {
// have no affect with a font declared in head
document.fonts.forEach(fontFace => document.fonts.delete(fontFace));
}
});
document.onscrollend = () => {
if (!this.generalSettings.val.pageReader) {
this.post({
type: 'save',
data: parseInt(
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
10,
),
});
}
};
if (DEBUG) {
// eslint-disable-next-line no-global-assign, no-new-object
console = new Object();
console.log = function (...data) {
reader.post({ 'type': 'console', 'msg': data?.join(' ') });
};
console.debug = console.log;
console.info = console.log;
console.warn = console.log;
console.error = console.log;
}
// end reader
})();
window.tts = new (function () {
this.readableNodeNames = [
'#text',
'B',
'I',
'SPAN',
'EM',
'BR',
'STRONG',
'A',
];
this.prevElement = null;
this.currentElement = reader.chapterElement;
this.started = false;
this.reading = false;
this.readable = element => {
const ele = element ?? this.currentElement;
if (
ele.nodeName !== 'SPAN' &&
this.readableNodeNames.includes(ele.nodeName)
) {
return false;
}
if (!ele.hasChildNodes()) {
return false;
}
for (let i = 0; i < ele.childNodes.length; i++) {
if (!this.readableNodeNames.includes(ele.childNodes.item(i).nodeName)) {
return false;
}
}
return true;
};
this.normalizeText = text => {
if (!text) return '';
return text
.replace(/\s+/g, ' ')
.replace(/\s*([.,!?;:])\s*/g, '$1 ')
.trim();
};
// if can find a readable node, else stop tts
this.findNextTextNode = () => {
if (this.currentElement.isSameNode(reader.chapterElement) && this.started) {
return false;
} else {
this.started = true;
}
// is read, have to go next or go back
if (this.currentElement.isSameNode(this.prevElement)) {
this.prevElement = this.currentElement;
if (this.currentElement.nextElementSibling) {
this.currentElement = this.currentElement.nextElementSibling;
} else {
this.currentElement = this.currentElement.parentElement;
}
return this.findNextTextNode();
} else {
// can read? read it
if (this.readable()) {
return true;
}
if (
!this.prevElement?.parentElement?.isSameNode(this.currentElement) &&
this.currentElement.firstElementChild
) {
// go deep
this.prevElement = this.currentElement;
this.currentElement = this.currentElement.firstElementChild;
} else if (this.currentElement.nextElementSibling) {
this.prevElement = this.currentElement;
this.currentElement = this.currentElement.nextElementSibling;
} else {
this.prevElement = this.currentElement;
this.currentElement = this.currentElement.parentElement;
}
return this.findNextTextNode();
}
};
this.next = () => {
try {
this.currentElement?.classList?.remove('highlight');
if (this.findNextTextNode()) {
const text = this.normalizeText(this.currentElement?.innerText);
if (text) {
this.reading = true;
this.speak();
} else {
this.next();
}
} else {
this.reading = false;
this.stop();
const controller = document.getElementById('TTS-Controller');
if (controller?.firstElementChild) {
controller.firstElementChild.innerHTML = volumnIcon;
}
if (reader.nextChapter) {
reader.post({ type: 'next', autoStartTTS: true });
}
}
} catch (e) {
alert(e);
}
};
this.start = element => {
this.stop();
this.currentElement = element ?? reader.chapterElement;
this.next();
};
this.resume = () => {
if (!this.reading) {
if (
this.currentElement &&
this.currentElement.id !== 'LNReader-chapter'
) {
this.speak();
this.reading = true;
} else {
this.next();
}
}
};
this.pause = () => {
this.reading = false;
reader.post({ type: 'stop-speak' });
reader.post({ type: 'tts-state', data: { isReading: false } });
};
this.stop = () => {
reader.post({ type: 'stop-speak' });
this.currentElement?.classList?.remove('highlight');
this.prevElement = null;
this.currentElement = reader.chapterElement;
this.started = false;
this.reading = false;
reader.post({ type: 'tts-state', data: { isReading: false } });
};
this.isElementInViewport = element => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const windowHeight =
window.innerHeight || document.documentElement.clientHeight;
const windowWidth =
window.innerWidth || document.documentElement.clientWidth;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowHeight &&
rect.right <= windowWidth
);
};
this.scrollToElement = element => {
if (!element) return;
// Check if element is partially visible (at least some part is in viewport)
const rect = element.getBoundingClientRect();
const windowHeight =
window.innerHeight || document.documentElement.clientHeight;
const isPartiallyVisible =
rect.top < windowHeight &&
rect.bottom > 0 &&
rect.left < window.innerWidth &&
rect.right > 0;
// Only scroll if element is not visible or barely visible
if (!isPartiallyVisible || rect.top < 0 || rect.bottom > windowHeight) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}
};
this.speak = () => {
if (!this.currentElement) return;
this.prevElement = this.currentElement;
this.scrollToElement(this.currentElement);
this.currentElement.classList.add('highlight');
const text = this.normalizeText(this.currentElement.innerText);
if (text) {
reader.post({ type: 'speak', data: text });
reader.post({ type: 'tts-state', data: { isReading: true } });
} else {
this.next();
}
};
})();
window.pageReader = new (function () {
this.page = van.state(0);
this.totalPages = van.state(0);
this.chapterEndingVisible = van.state(
initialPageReaderConfig.nextChapterScreenVisible,
);
this.chapterEnding = document.getElementsByClassName('transition-chapter')[0];
this.showChapterEnding = (bool, instant, left) => {
if (!this.chapterEnding) {
this.chapterEnding =
document.getElementsByClassName('transition-chapter')[0];
if (!this.chapterEnding) return;
}
this.chapterEnding.style.transition = 'unset';
if (bool) {
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
requestAnimationFrame(() => {
if (!instant) this.chapterEnding.style.transition = '200ms';
this.chapterEnding.style.transform = 'translateX(-100vw)';
});
this.chapterEndingVisible.val = true;
} else {
if (!instant) this.chapterEnding.style.transition = '200ms';
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
this.chapterEndingVisible.val = false;
}
};
this.movePage = destPage => {
if (this.chapterEndingVisible.val) {
if (destPage < 0) {
this.showChapterEnding(false);
return;
}
if (destPage < this.totalPages.val) {
this.showChapterEnding(false, false, true);
return;
}
if (destPage >= this.totalPages.val) {
return reader.post({ type: 'next' });
}
}
destPage = parseInt(destPage, 10);
if (destPage < 0) {
document.getElementsByClassName('transition-chapter')[0].innerText =
reader.prevChapter.name;
this.showChapterEnding(true, false, true);
setTimeout(() => {
reader.post({ type: 'prev' });
}, 200);
return;
}
if (destPage >= this.totalPages.val) {
document.getElementsByClassName('transition-chapter')[0].innerText =
reader.nextChapter.name;
this.showChapterEnding(true);
setTimeout(() => {
reader.post({ type: 'next' });
}, 200);
return;
}
this.page.val = destPage;
reader.chapterElement.style.transform =
'translateX(-' + destPage * 100 + '%)';
const newProgress = parseInt(
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
10,
);
if (newProgress > reader.chapter.progress) {
reader.post({
type: 'save',
data: parseInt(
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
10,
),
});
}
};
van.derive(() => {
// ignore if initial or other states change
if (
reader.generalSettings.val.pageReader ===
reader.generalSettings.oldVal.pageReader
) {
return;
}
if (reader.generalSettings.val.pageReader) {
const ratio = Math.min(
0.99,
(window.scrollY + reader.layoutHeight) / reader.chapterHeight,
);
document.body.classList.add('page-reader');
setTimeout(() => {
reader.refresh();
this.totalPages.val = parseInt(
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
reader.layoutWidth,
10,
);
this.movePage(this.totalPages.val * ratio);
}, 100);
} else {
reader.chapterElement.style = '';
document.body.classList.remove('page-reader');
setTimeout(() => {
reader.refresh();
window.scrollTo({
top:
(reader.chapterHeight * (this.page.val + 1)) / this.totalPages.val -
reader.layoutHeight,
behavior: 'smooth',
});
}, 100);
}
});
})();
document.addEventListener('DOMContentLoaded', () => {
if (pageReader.chapterEndingVisible.val) {
pageReader.showChapterEnding(true, true);
}
});
function calculatePages() {
reader.refresh();
if (reader.generalSettings.val.pageReader) {
pageReader.totalPages.val = parseInt(
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
reader.layoutWidth,
10,
);
if (initialPageReaderConfig.nextChapterScreenVisible) return;
pageReader.movePage(
Math.max(
0,
Math.round(
(pageReader.totalPages.val * reader.chapter.progress) / 100,
) - 1,
),
);
} else {
window.scrollTo({
top:
(reader.chapterHeight * reader.chapter.progress) / 100 -
reader.layoutHeight,
behavior: 'smooth',
});
}
}
const ro = new ResizeObserver(() => {
if (pageReader.totalPages.val) {
calculatePages();
}
});
ro.observe(reader.chapterElement);
// Also call once on load
window.addEventListener('load', () => {
document.fonts.ready.then(() => {
requestAnimationFrame(() => setTimeout(calculatePages, 0));
});
});
// click handler
(function () {
const detectTapPosition = (x, y, horizontal) => {
if (horizontal) {
if (x < 0.33) {
return 'left';
}
if (x > 0.66) {
return 'right';
}
} else {
if (y < 0.33) {
return 'top';
}
if (y > 0.66) {
return 'bottom';
}
}
return 'center';
};
document.onclick = e => {
const { clientX, clientY } = e;
const { x, y } = {
x: clientX / reader.layoutWidth,
y: clientY / reader.layoutHeight,
};
if (reader.generalSettings.val.pageReader) {
const position = detectTapPosition(x, y, true);
if (position === 'left') {
pageReader.movePage(pageReader.page.val - 1);
return;
}
if (position === 'right') {
pageReader.movePage(pageReader.page.val + 1);
return;
}
} else {
if (reader.generalSettings.val.tapToScroll) {
const position = detectTapPosition(x, y, false);
if (position === 'top') {
window.scrollBy({
top: -reader.layoutHeight * 0.75,
behavior: 'smooth',
});
return;
}
if (position === 'bottom') {
window.scrollBy({
top: reader.layoutHeight * 0.75,
behavior: 'smooth',
});
return;
}
}
}
reader.post({ type: 'hide' });
};
})();
// swipe handler
(function () {
this.initialX = null;
this.initialY = null;
reader.chapterElement.addEventListener('touchstart', e => {
this.initialX = e.changedTouches[0].screenX;
this.initialY = e.changedTouches[0].screenY;
});
reader.chapterElement.addEventListener('touchmove', e => {
if (reader.generalSettings.val.pageReader) {
const diffX =
(e.changedTouches[0].screenX - this.initialX) / reader.layoutWidth;
reader.chapterElement.style.transition = 'unset';
reader.chapterElement.style.transform =
'translateX(-' + (pageReader.page.val - diffX) * 100 + '%)';
}
});
reader.chapterElement.addEventListener('touchend', e => {
const diffX = e.changedTouches[0].screenX - this.initialX;
const diffY = e.changedTouches[0].screenY - this.initialY;
if (reader.generalSettings.val.pageReader) {
reader.chapterElement.style.transition = '200ms';
const diffXPercentage = diffX / reader.layoutWidth;
if (diffXPercentage < -0.3) {
pageReader.movePage(pageReader.page.val + 1);
} else if (diffXPercentage > 0.3) {
pageReader.movePage(pageReader.page.val - 1);
} else {
pageReader.movePage(pageReader.page.val);
}
return;
}
if (
e.target.id?.startsWith('scrollbar') ||
e.target.id === 'Image-Modal-img'
) {
return;
}
if (
reader.generalSettings.val.swipeGestures &&
Math.abs(diffX) > Math.abs(diffY) * 2 &&
Math.abs(diffX) > 180
) {
if (diffX < 0 && this.initialX >= window.innerWidth / 2) {
e.preventDefault();
reader.post({ type: 'next' });
} else if (diffX > 0 && this.initialX <= window.innerWidth / 2) {
e.preventDefault();
reader.post({ type: 'prev' });
}
}
});
})();
// text options
(function () {
van.derive(() => {
let html = reader.rawHTML;
if (reader.generalSettings.val.bionicReading) {
html = textVide.textVide(reader.rawHTML);
}
if (reader.generalSettings.val.removeExtraParagraphSpacing) {
html = html
.replace(/(?:&nbsp;\s*|[\u200b]\s*)+(?=<\/?p[> ])/g, '')
.replace(/<br>\s*<br>\s*(?:<br>\s*)+/g, '<br><br>') //force max 2 consecutive <br>, chaining regex
.replace(
/<br>\s*<br>[^]+/,
_ =>
`${
/\/p>/.test(_)
? _.replace(
/<br>\s*<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p\b[^>]*><br>\s*<br>))\s*/g,
'',
)
: _
}`,
) //if p found, delete all double br near p
.replace(/<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p>\s*<br>))\s*/g, '');
}
reader.chapterElement.innerHTML = html;
});
})();
-6
View File
@@ -1,6 +0,0 @@
const volumnIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M552-152v-75q86-23 139-93.5T744-480q0-89-53.5-158.5T552-734v-75q116 25 190 117t74 211q0 119-73.5 211.5T552-152ZM144-385v-192h144l192-192v576L288-385H144Zm408 55v-302q45 20 70.5 61t25.5 90q0 49-25.5 89.5T552-330Z"/></svg>';
const pauseIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M564-228v-504h168v504H564Zm-336 0v-504h168v504H228Z"/></svg>';
const resumeIcon =
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M240-240v-480h72v480h-72Zm144 0 384-240-384-240v480Z"/></svg>';
-339
View File
@@ -1,339 +0,0 @@
const { div, p, img, button } = van.tags;
const ChapterEnding = () => {
return () =>
reader.generalSettings.val.pageReader
? div()
: div(div({ class: 'info-text' }, reader.strings.finished), () =>
reader.nextChapter
? button(
{
class: 'next-button',
onclick: e => {
e.stopPropagation();
reader.post({ type: 'next' });
},
},
reader.strings.nextChapter,
)
: div({ class: 'info-text' }, reader.strings.noNextChapter),
);
};
const Scrollbar = () => {
const horizontal = van.derive(
() => !reader.generalSettings.val.verticalSeekbar,
);
let lock = false;
const percentage = van.state(0);
const update = ratio => {
if (ratio === undefined) {
ratio = (window.scrollY + reader.layoutHeight) / reader.chapterHeight;
}
if (ratio > 1) {
ratio = 1;
}
if (reader.generalSettings.val.pageReader) {
pageReader.movePage(
parseInt(pageReader.totalPages.val * Math.min(0.99, ratio)),
);
return;
}
percentage.val = parseInt(ratio * 100);
if (lock) {
window.scrollTo({
top: reader.chapterHeight * ratio - reader.layoutHeight,
behavior: 'instant',
});
}
};
window.addEventListener(
'scroll',
() => !lock && !reader.generalSettings.val.pageReader && update(),
);
return div(
{ id: 'ScrollBar' },
div(
{ class: 'scrollbar-item scrollbar-text', id: 'scrollbar-percentage' },
() =>
reader.generalSettings.val.pageReader
? pageReader.page.val + 1
: percentage.val,
),
div(
{ class: 'scrollbar-item', id: 'scrollbar-slider' },
div(
{ id: 'scrollbar-track' },
div(
{
id: 'scrollbar-progress',
style: () => {
const percentageValue = reader.generalSettings.val.pageReader
? ((pageReader.page.val + 1) / pageReader.totalPages.val) * 100
: percentage.val;
return horizontal.val
? `width: ${percentageValue}%; height: 100%;`
: `height: ${percentageValue}%; width: 100%;`;
},
},
div(
{
id: 'scrollbar-thumb-wrapper',
ontouchstart: () => {
lock = true;
},
ontouchend: () => {
lock = false;
},
ontouchmove: function (e) {
const slider = this.parentElement.parentElement.parentElement;
const sliderHeight = horizontal.val
? slider.clientWidth
: slider.clientHeight;
const sliderOffsetY = horizontal.val
? slider.getBoundingClientRect().left
: slider.getBoundingClientRect().top;
const ratio =
((horizontal.val
? e.changedTouches[0].clientX
: e.changedTouches[0].clientY) -
sliderOffsetY) /
sliderHeight;
update(ratio < 0 ? 0 : ratio);
},
},
div({ id: 'scrollbar-thumb' }),
),
),
),
),
div(
{
class: 'scrollbar-item scrollbar-text',
id: 'scrollbar-percentage-max',
},
() =>
reader.generalSettings.val.pageReader ? pageReader.totalPages.val : 100,
),
);
};
const ToolWrapper = () => {
const horizontal = van.derive(
() => !reader.generalSettings.val.verticalSeekbar,
);
return div(
{
id: 'ToolWrapper',
class: () =>
`${reader.hidden.val ? 'hidden' : ''} ${
horizontal.val ? 'horizontal' : ''
}`,
},
Scrollbar(),
);
};
const ImageModal = ({ src }) => {
return div(
{
id: 'Image-Modal',
class: () => (src.val ? 'show' : ''),
onclick: e => {
if (e.target.id !== 'Image-Modal-img') {
e.stopPropagation();
src.val = '';
}
},
},
img({
id: 'Image-Modal-img',
src: src,
alt: () => (src.val ? `Cant not render image from ${src.val}` : ''),
}),
);
};
const ModalWrapper = () => {
const imgSrc = van.state('');
const showImage = src => {
imgSrc.val = src;
reader.viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=10',
);
};
const hideImage = () => {
imgSrc.val = '';
reader.viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0',
);
};
document.addEventListener('contextmenu', e => {
if (e.target instanceof HTMLImageElement) {
if (!imgSrc.val) {
showImage(e.target.src);
} else {
hideImage();
}
}
});
return div(ImageModal({ src: imgSrc }));
};
const Footer = () => {
const percentage = van.state(0);
const time = van.state(
new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
);
window.addEventListener('scroll', () => {
let ratio = (window.scrollY + reader.layoutHeight) / reader.chapterHeight;
if (ratio > 1) {
ratio = 1;
}
percentage.val = parseInt(ratio * 100);
});
setInterval(() => {
time.val = new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}, 10000);
return div(
{
id: 'reader-footer-wrapper',
class: () =>
reader.generalSettings.val.showBatteryAndTime ||
reader.generalSettings.val.showScrollPercentage
? ''
: 'd-none',
},
div(
{ id: 'reader-footer' },
div(
{
id: 'reader-battery',
class: () =>
`reader-footer-item ${
reader.generalSettings.val.showBatteryAndTime ? '' : 'hidden'
}`,
},
() => Math.ceil(reader.batteryLevel.val * 100) + '%',
),
div(
{
id: 'reader-percentage',
class: () =>
`reader-footer-item ${
reader.generalSettings.val.showScrollPercentage ? '' : 'hidden'
}`,
},
() =>
reader.generalSettings.val.pageReader
? `${pageReader.page.val + 1}/${pageReader.totalPages.val}`
: percentage.val + '%',
),
div(
{
id: 'reader-time',
class: () =>
`reader-footer-item ${
reader.generalSettings.val.showBatteryAndTime ? '' : 'hidden'
}`,
},
time,
),
),
);
};
const TTSController = () => {
let controllerElement = null;
let hoverElement = null;
let clientX = null;
let clientY = null;
return div(
{
id: 'TTS-Controller',
class: () => `${reader.generalSettings.val.TTSEnable ? '' : 'hidden'}`,
ontouchstart: () => {
if (!controllerElement) {
controllerElement = document.getElementById('TTS-Controller');
}
controllerElement.classList.add('active');
controllerElement.style.transition = '';
},
ontouchmove: e => {
e.preventDefault();
e.stopPropagation();
clientX = e.changedTouches[0].clientX;
clientY = e.changedTouches[0].clientY;
controllerElement.style.left = `${clientX}px`;
controllerElement.style.top = `${clientY}px`;
const hoverElements = document.elementsFromPoint(clientX, clientY);
const newHoverElement = hoverElements.reverse().find(e => {
if (e.id.includes('scrollbar')) {
return false;
}
return tts.readable(e);
});
hoverElement?.classList.remove('highlight');
if (newHoverElement) {
newHoverElement.classList.add('highlight');
hoverElement = newHoverElement;
} else {
hoverElement = null;
}
},
ontouchend: () => {
controllerElement.style.transition = '1s';
controllerElement.classList.remove('active');
controllerElement.style.left = '20px';
if (clientX && clientY) {
let top = clientY < 120 ? 120 : clientY;
if (top + 120 > reader.layoutHeight) {
top = reader.layoutHeight - 120;
}
controllerElement.style.top = `${top}px`;
if (hoverElement) {
tts.start(hoverElement);
controllerElement.firstElementChild.innerHTML = pauseIcon;
}
}
clientX = null;
clientY = null;
},
onclick: e => {
e.stopPropagation();
if (tts.reading) {
tts.pause();
controllerElement.firstElementChild.innerHTML = resumeIcon;
} else {
tts.resume();
controllerElement.firstElementChild.innerHTML = pauseIcon;
}
},
},
button({ innerHTML: volumnIcon }),
);
};
const ReaderUI = () => {
return div(
ToolWrapper(),
TTSController(),
ModalWrapper(),
Footer(),
ChapterEnding(),
);
};
van.add(document.getElementById('reader-ui'), ReaderUI());
@@ -1,74 +0,0 @@
package com.rajarsheechatterjee.LNReader
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.WindowManager
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import com.rajarsheechatterjee.NativeVolumeButtonListener.NativeVolumeButtonListener
import expo.modules.ReactActivityDelegateWrapper
import org.devio.rn.splashscreen.SplashScreen
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val layoutParams = WindowManager.LayoutParams()
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
window.attributes = layoutParams
}
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
super.onCreate(null)
SplashScreen.show(this, R.style.SplashScreenTheme, R.id.lottie)
SplashScreen.setAnimationFinished(true)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (NativeVolumeButtonListener.isActive) {
val action = event.action
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
if (action == KeyEvent.ACTION_DOWN) {
NativeVolumeButtonListener.sendEvent(true)
}
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (action == KeyEvent.ACTION_DOWN) {
NativeVolumeButtonListener.sendEvent(false)
}
true
}
else -> super.dispatchKeyEvent(event)
}
}
return super.dispatchKeyEvent(event)
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
override fun getMainComponentName(): String = "main"
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(
this,
mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer.
fabricEnabled
)
)
}
}
@@ -1,52 +0,0 @@
package com.rajarsheechatterjee.LNReader
import android.app.Application
import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.rajarsheechatterjee.NativeFile.NativePackage
import com.rajarsheechatterjee.NativeVolumeButtonListener.NativeVolumeButtonListenerPackage
import com.rajarsheechatterjee.NativeZipArchive.NativeZipArchivePackage
import expo.modules.ApplicationLifecycleDispatcher
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(NativePackage())
add(NativeVolumeButtonListenerPackage())
add(NativeZipArchivePackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}
@@ -1,227 +0,0 @@
package com.rajarsheechatterjee.NativeFile
import android.net.Uri
import android.os.Build
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.modules.network.CookieJarContainer
import com.facebook.react.modules.network.ForwardingCookieHandler
import com.facebook.react.modules.network.OkHttpClientProvider
import com.lnreader.spec.NativeFileSpec
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Headers
import okhttp3.JavaNetCookieJar
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.PushbackInputStream
import java.util.zip.GZIPInputStream
class NativeFile(context: ReactApplicationContext) :
NativeFileSpec(context) {
private val BUFFER_SIZE = 4096
private val okHttpClient = OkHttpClientProvider.createClient()
private val coroutineScope = CoroutineScope(Dispatchers.IO)
init {
val cookieContainer = okHttpClient.cookieJar as CookieJarContainer
val cookieHandler = ForwardingCookieHandler(reactApplicationContext)
cookieContainer.setCookieJar(JavaNetCookieJar(cookieHandler))
}
private fun getFileUri(filepath: String): Uri {
var uri = Uri.parse(filepath)
if (uri.scheme == null) {
// No prefix, assuming that provided path is absolute path to file
val file = File(filepath)
if (file.isDirectory) {
throw Exception("Invalid file, folder found!")
}
uri = Uri.parse("file://$filepath")
}
return uri
}
private fun getInputStream(filepath: String): InputStream {
val uri = getFileUri(filepath)
return reactApplicationContext.contentResolver.openInputStream(uri)
?: throw Exception("ENOENT: could not open an input stream for '$filepath'")
}
private val writeAccessByAPILevel: String
get() = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) "w" else "rwt"
private fun getOutputStream(filepath: String): OutputStream {
val uri = getFileUri(filepath)
return reactApplicationContext.contentResolver.openOutputStream(uri, writeAccessByAPILevel)
?: throw Exception("ENOENT: could not open an output stream for '$filepath'")
}
override fun writeFile(path: String, content: String) {
val fw = FileWriter(path)
fw.write(content)
fw.close()
}
override fun readFile(path: String): String {
return File(path).bufferedReader().readText()
}
override fun copyFile(filepath: String, destPath: String) {
copyFileContent(filepath, destPath)
}
override fun moveFile(filepath: String, destPath: String) {
val inFile = File(filepath)
copyFileContent(filepath, destPath, { inFile.delete() })
}
private fun copyFileContent(
filepath: String,
destPath: String,
onDone: (() -> Unit)? = null,
) {
val inputStream = getInputStream(filepath)
val outputStream = getOutputStream(destPath)
val buffer = ByteArray(BUFFER_SIZE)
var length: Int
while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
}
inputStream.close()
outputStream.close()
if (onDone != null) {
onDone()
}
}
override fun exists(filepath: String) = File(filepath).exists()
override fun mkdir(filepath: String) {
val file = File(filepath)
if (!file.exists()) {
val created = file.mkdirs()
if (!created) throw Exception("Directory could not be created")
}
}
private fun deleteRecursive(fileOrDirectory: File) {
if (fileOrDirectory.isDirectory) {
for (child in fileOrDirectory.listFiles()!!) {
deleteRecursive(child)
}
}
fileOrDirectory.delete()
}
override fun unlink(filepath: String) {
val file = File(filepath)
if (!file.exists()) throw Exception("File does not exist")
deleteRecursive(file)
}
override fun readDir(directory: String): WritableArray {
val file = File(directory)
if (!file.exists()) throw Exception("Folder does not exist")
val files = file.listFiles()
val fileMaps: WritableArray = WritableNativeArray()
for (childFile in files!!) {
val fileMap: WritableMap = WritableNativeMap()
fileMap.putString("name", childFile.name)
fileMap.putString("path", childFile.absolutePath)
fileMap.putBoolean("isDirectory", childFile.isDirectory)
fileMaps.pushMap(fileMap)
}
return fileMaps
}
private fun decompressStream(input: InputStream?): InputStream {
val pb = PushbackInputStream(input, 2)
val signature = ByteArray(2)
val len = pb.read(signature)
if(len == -1) return pb;
pb.unread(signature, 0, len)
return if (signature[0] == 0x1f.toByte() && signature[1] == 0x8b.toByte())
GZIPInputStream(pb) else pb
}
override fun downloadFile(
url: String,
destPath: String,
method: String,
headers: ReadableMap,
body: String?,
promise: Promise
) {
coroutineScope.launch {
try {
val headersBuilder = Headers.Builder()
headers.entryIterator.forEach { entry ->
headersBuilder.add(entry.key, entry.value.toString())
}
val requestBuilder = Request.Builder()
.url(url)
.headers(headersBuilder.build())
if (method.lowercase() == "get") {
requestBuilder.get()
} else if (body != null) {
requestBuilder.post(body.toRequestBody())
}
okHttpClient.newCall(requestBuilder.build())
.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
promise.reject(e)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful || response.body == null) {
promise.reject(Exception("Failed to download load: ${response.code}"))
return
}
try {
val inputStream = decompressStream(response.body!!.byteStream())
FileOutputStream(destPath).use { fos ->
inputStream.copyTo(fos, BUFFER_SIZE)
}
promise.resolve(null)
} catch (e: Exception) {
promise.reject(e)
}
}
})
} catch (e: Exception) {
promise.reject(e)
}
}
}
override fun getTypedExportedConstants(): MutableMap<String, Any> {
val constants: MutableMap<String, Any> = HashMap()
val externalDirectory = this.reactApplicationContext.getExternalFilesDir(null)
if (externalDirectory != null) {
constants["ExternalDirectoryPath"] = externalDirectory.absolutePath
}
val externalCachesDirectory = this.reactApplicationContext.externalCacheDir
if (externalCachesDirectory != null) {
constants["ExternalCachesDirectoryPath"] = externalCachesDirectory.absolutePath
}
return constants
}
}
@@ -1,30 +0,0 @@
package com.rajarsheechatterjee.NativeFile
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.lnreader.spec.NativeFileSpec
class NativePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeFileSpec.NAME) {
NativeFile(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeFileSpec.NAME to ReactModuleInfo(
NativeFileSpec.NAME,
NativeFileSpec.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
@@ -1,32 +0,0 @@
package com.rajarsheechatterjee.NativeVolumeButtonListener
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.lnreader.spec.NativeVolumeButtonListenerSpec
class NativeVolumeButtonListener(appContext: ReactApplicationContext) :
NativeVolumeButtonListenerSpec(appContext) {
init {
NativeVolumeButtonListener.appContext = appContext
}
override fun addListener(eventName: String?) {
isActive = true
}
override fun removeListeners(count: Double) {
isActive = false
}
companion object {
lateinit var appContext: ReactApplicationContext
var isActive = false
fun sendEvent(up: Boolean) {
if (isActive) {
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(if (up) "VolumeUp" else "VolumeDown", null)
}
}
}
}
@@ -1,30 +0,0 @@
package com.rajarsheechatterjee.NativeVolumeButtonListener
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.lnreader.spec.NativeVolumeButtonListenerSpec
class NativeVolumeButtonListenerPackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeVolumeButtonListenerSpec.NAME) {
NativeVolumeButtonListener(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeVolumeButtonListenerSpec.NAME to ReactModuleInfo(
NativeVolumeButtonListenerSpec.NAME,
NativeVolumeButtonListenerSpec.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
@@ -1,137 +0,0 @@
package com.rajarsheechatterjee.NativeZipArchive
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.lnreader.spec.NativeZipArchiveSpec
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class NativeZipArchive(context: ReactApplicationContext) : NativeZipArchiveSpec(context) {
@ReactMethod
override fun unzip(sourceFilePath: String, distDirPath: String, promise: Promise) {
Thread {
try {
ZipFile(sourceFilePath).use { zis ->
zis.entries().asSequence().filterNot { it.isDirectory }.forEach { zipEntry ->
val newFile = File(distDirPath, zipEntry.name)
newFile.parentFile?.mkdirs()
zis.getInputStream(zipEntry).use { inputStream ->
FileOutputStream(newFile).use { fos -> inputStream.copyTo(fos, 4096) }
}
Thread.yield()
}
}
promise.resolve(null)
} catch (e: Exception) {
promise.reject(e)
}
}.start()
}
@ReactMethod
override fun zip(sourceDirPath: String, zipFilePath: String, promise: Promise) {
Thread {
try {
FileOutputStream(zipFilePath).use { fos ->
ZipOutputStream(fos).use { zos -> zipProcess(sourceDirPath, zos) }
}
promise.resolve(null)
} catch (e: Exception) {
promise.reject(e)
}
}.start()
}
@ReactMethod
override fun remoteUnzip(
distDirPath: String,
urlString: String,
headers: ReadableMap,
promise: Promise
) {
val connection = URL(urlString).openConnection() as HttpURLConnection
Thread {
try {
connection.requestMethod = "GET"
val it = headers.entryIterator
while (it.hasNext()) {
val (key, value) = it.next()
connection.setRequestProperty(key, value.toString())
}
ZipInputStream(connection.inputStream).use { zis ->
generateSequence { zis.nextEntry }
.filterNot { it.isDirectory }
.forEach { zipEntry ->
val newFile = File(distDirPath, zipEntry.name)
newFile.parentFile?.mkdirs()
FileOutputStream(newFile).use { fos -> zis.copyTo(fos, 4096) }
Thread.yield()
}
}
if (connection.responseCode == 200) {
promise.resolve(null)
} else {
throw Exception("Network request failed")
}
} catch (e: Exception) {
promise.reject(e)
} finally {
connection.disconnect()
}
}.start()
}
private fun zipProcess(sourceDirPath: String, zos: ZipOutputStream) {
val sourceDir = File(sourceDirPath)
sourceDir.walkBottomUp().filter { it.isFile }.forEach { file ->
val zipFileName =
file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/")
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
zos.putNextEntry(entry)
file.inputStream().use { fis ->
fis.copyTo(zos, 4096)
fis.close()
}
Thread.yield()
}
}
@ReactMethod
override fun remoteZip(
sourceDirPath: String,
urlString: String,
headers: ReadableMap,
promise: Promise
) {
Thread {
val connection = URL(urlString).openConnection() as HttpURLConnection
try {
connection.requestMethod = "POST"
val it = headers.entryIterator
while (it.hasNext()) {
val (key, value) = it.next()
connection.setRequestProperty(key, value.toString())
}
ZipOutputStream(connection.outputStream).use { zipProcess(sourceDirPath, it) }
if (connection.responseCode == 200) {
promise.resolve(
connection.inputStream.bufferedReader().use { it.readText() })
} else {
throw Exception("Network request failed")
}
} catch (e: Exception) {
promise.reject(e)
} finally {
connection.disconnect()
}
}.start()
}
}
@@ -1,30 +0,0 @@
package com.rajarsheechatterjee.NativeZipArchive
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.lnreader.spec.NativeZipArchiveSpec
class NativeZipArchivePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeZipArchiveSpec.NAME) {
NativeZipArchive(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeZipArchiveSpec.NAME to ReactModuleInfo(
NativeZipArchiveSpec.NAME,
NativeZipArchiveSpec.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
-17
View File
@@ -1,17 +0,0 @@
cmake_minimum_required(VERSION 3.13)
# Define the library name here.
project(appmodules)
# This file includes all the necessary to let you build your React Native application
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
# Define where the additional source code lives. We need to crawl back the jni, main, src, app, android folders
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
../../../../../shared/NativeEpub.cpp
../../../../../shared/Epub.cpp
../../../../../shared/pugixml.cpp
)
# Define where CMake can find the additional header files. We need to crawl back the jni, main, src, app, android folders
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)
-138
View File
@@ -1,138 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// This C++ file is part of the default configuration used by apps and is placed
// inside react-native to encapsulate it from user space (so you won't need to
// touch C++/Cmake code at all on Android).
//
// If you wish to customize it (because you want to manually link a C++ library
// or pass a custom compilation flag) you can:
//
// 1. Copy this CMake file inside the `android/app/src/main/jni` folder of your
// project
// 2. Copy the OnLoad.cpp (in this same folder) file inside the same folder as
// above.
// 3. Extend your `android/app/build.gradle` as follows
//
// android {
// // Other config here...
// externalNativeBuild {
// cmake {
// path "src/main/jni/CMakeLists.txt"
// }
// }
// }
#include <DefaultComponentsRegistry.h>
#include <DefaultTurboModuleManagerDelegate.h>
#include <autolinking.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <FBReactNativeSpec.h>
#include <NativeEpub.hpp>
#ifdef REACT_NATIVE_APP_CODEGEN_HEADER
#include REACT_NATIVE_APP_CODEGEN_HEADER
#endif
#ifdef REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
#include REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
#endif
namespace facebook::react
{
void registerComponents(
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry)
{
// Custom Fabric Components go here. You can register custom
// components coming from your App or from 3rd party libraries here.
//
// providerRegistry->add(concreteComponentDescriptorProvider<
// MyComponentDescriptor>());
// We link app local components if available
#ifdef REACT_NATIVE_APP_COMPONENT_REGISTRATION
REACT_NATIVE_APP_COMPONENT_REGISTRATION(registry);
#endif
// And we fallback to the components autolinked
autolinking_registerProviders(registry);
}
std::shared_ptr<TurboModule> cxxModuleProvider(
const std::string &name,
const std::shared_ptr<CallInvoker> &jsInvoker)
{
// Here you can provide your CXX Turbo Modules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a module called `NativeCxxModuleExample`):
//
// if (name == NativeCxxModuleExample::kModuleName) {
// return std::make_shared<NativeCxxModuleExample>(jsInvoker);
// }
if (name == NativeEpub::kModuleName)
{
return std::make_shared<NativeEpub>(jsInvoker);
}
// And we fallback to the CXX module providers autolinked
return autolinking_cxxModuleProvider(name, jsInvoker);
}
std::shared_ptr<TurboModule> javaModuleProvider(
const std::string &name,
const JavaTurboModule::InitParams &params)
{
// Here you can provide your own module provider for TurboModules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a library called `samplelibrary`):
//
// auto module = samplelibrary_ModuleProvider(name, params);
// if (module != nullptr) {
// return module;
// }
// return FBReactNativeSpec_ModuleProvider(name, params);
// We link app local modules if available
#ifdef REACT_NATIVE_APP_MODULE_PROVIDER
auto module = REACT_NATIVE_APP_MODULE_PROVIDER(name, params);
if (module != nullptr)
{
return module;
}
#endif
// We first try to look up core modules
if (auto module = FBReactNativeSpec_ModuleProvider(name, params))
{
return module;
}
// And we fallback to the module providers autolinked
if (auto module = autolinking_ModuleProvider(name, params))
{
return module;
}
return nullptr;
}
} // namespace facebook::react
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *)
{
return facebook::jni::initialize(vm, []
{
facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
&facebook::react::cxxModuleProvider;
facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
&facebook::react::javaModuleProvider;
facebook::react::DefaultComponentsRegistry::
registerComponentDescriptorsFromEntryPoint =
&facebook::react::registerComponents; });
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,93 +0,0 @@
<vector android:alpha="0" android:height="1dp"
android:viewportHeight="1024" android:viewportWidth="1024"
android:width="1dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="0.994" android:fillColor="#132c33"
android:pathData="M479.5,-0.5C500.8,-0.5 522.2,-0.5 543.5,-0.5C704.4,13.5 832.9,84.8 929,213.5C985.1,293.3 1016.5,382 1023.5,479.5C1023.5,500.8 1023.5,522.2 1023.5,543.5C1009.2,709.4 934.5,840.2 799.5,936C722,987.8 636.6,1017 543.5,1023.5C522.2,1023.5 500.8,1023.5 479.5,1023.5C333.8,1011.4 213.3,950.4 118,840.5C47.3,754.3 7.8,655.3 -0.5,543.5C-0.5,522.2 -0.5,500.8 -0.5,479.5C13.5,318.6 84.8,190.1 213.5,94C293.3,37.9 382,6.4 479.5,-0.5Z" android:strokeAlpha="0.994"/>
<path android:fillColor="#106476" android:pathData="M870.5,325.5C876.8,340.6 882.7,355.9 888,371.5C889.7,373.2 891.1,375.2 892,377.5C895.1,386.3 897.4,395.3 899,404.5C900,406.7 901.5,408.4 903.5,409.5C903.5,410.2 903.5,410.8 903.5,411.5C907.2,436.2 910.8,460.8 914.5,485.5C915.4,500.5 915.7,515.5 915.5,530.5C911.7,612.8 885.4,686.8 836.5,752.5C836.5,750.7 837,749 838,747.5C894.4,667.2 918.8,577.8 911,479.5C899,365 848.5,271.8 759.5,200C652.9,120.8 534.9,97.5 405.5,130C299,161.6 218.1,225.4 163,321.5C108.7,424.4 99.1,531.4 134,642.5C147,680.2 165,715.2 188,747.5C189,749 189.5,750.7 189.5,752.5C141.4,688 115.1,615.3 110.5,534.5C110.3,518.5 110.5,502.5 111,486.5C112.8,477.3 113.8,467.9 114,458.5C119,435.8 124.4,413.1 130,390.5C140,359.6 153.3,330.6 170,303.5C184.2,283.1 199.4,263.5 215.5,244.5C231.1,229.1 246.7,213.9 262.5,199C273.8,191.7 285.1,184.4 296.5,177C297.3,175.5 298.3,174.2 299.5,173C303.2,172.6 306.5,171.3 309.5,169C313,166 316.3,162.9 319.5,159.5C330.1,155.2 340.1,149.9 349.5,143.5C360.1,142.3 370.1,139.2 379.5,134C385.7,132.1 392.1,130.5 398.5,129C400.5,128 402.5,127 404.5,126C452.1,113.9 500.4,109.7 549.5,113.5C567,115.2 584.3,117.7 601.5,121C620.6,126.7 639.8,132.6 659,138.5C668,141.9 676.8,145.8 685.5,150C695.4,156.1 705.8,161.5 716.5,166C717.2,167 717.8,168 718.5,169C733.7,175.2 747.1,184.2 758.5,196C764.2,199.9 769.5,204.2 774.5,209C776.6,210.8 778.9,212.1 781.5,213C785.3,218.2 789.8,222.7 795,226.5C797.1,228.3 798.4,230.7 799,233.5C807,241.6 815,249.6 823,257.5C823.3,258.8 823.7,260.2 824,261.5C828,266.5 832.4,271.2 837,275.5C841.8,284.4 847.1,293.1 853,301.5C858.3,307.4 862.6,314.1 866,321.5C867,323.4 868.5,324.7 870.5,325.5Z"/>
<path android:fillColor="#126e81" android:pathData="M836.5,752.5C791,813.4 733,858.3 662.5,887C545.5,931 431.5,924.3 320.5,867C268.3,838.7 224.7,800.5 189.5,752.5C189.5,750.7 189,749 188,747.5C165,715.2 147,680.2 134,642.5C99.1,531.4 108.7,424.4 163,321.5C218.1,225.4 299,161.6 405.5,130C534.9,97.5 652.9,120.8 759.5,200C848.5,271.8 899,365 911,479.5C918.8,577.8 894.4,667.2 838,747.5C837,749 836.5,750.7 836.5,752.5Z"/>
<path android:fillColor="#a1abaf" android:pathData="M660.5,203.5C657.7,203.4 655,202.8 652.5,201.5C641.6,195.4 629.9,191.5 617.5,190C609.7,188 602.4,185 595.5,181C580.9,179.2 566.6,176.6 552.5,173C521.2,169 490.2,169.9 459.5,175.5C447.6,178.3 435.6,180.5 423.5,182C418.2,183.8 412.9,185.3 407.5,186.5C388.5,192.1 370.1,199.6 352.5,209C351.2,209.5 349.9,209.7 348.5,209.5C423.4,170 502.4,159.1 585.5,177C611.8,182.6 636.8,191.4 660.5,203.5Z"/>
<path android:fillColor="#d7e2e6" android:pathData="M688.5,223.5C690.9,224.3 692.9,225.7 694.5,227.5C695.4,229.8 696.7,232 698.5,234C734,257.8 763.8,287.3 788,322.5C789.3,323.9 790.8,324.9 792.5,325.5C793.2,325.8 793.8,326.2 794.5,326.5C794.3,327.9 794.5,329.2 795,330.5C851.6,424.2 862.9,523.2 829,627.5C824.9,638.7 820.3,649.7 815,660.5C812.6,667.1 810.4,673.7 808.5,680.5C806.4,682.7 803.9,684.7 801,686.5C790.6,704.3 778.8,720.7 765.5,735.5C765.5,737.2 765.5,738.8 765.5,740.5C721,790.4 666.4,824.1 601.5,841.5C586.8,843.9 572.2,846.4 557.5,849C523.2,853.2 488.9,853.4 454.5,849.5C364.7,834.1 292.3,790.4 237.5,718.5C236.2,715.7 235.4,712.7 235,709.5C188.1,642.5 167.8,567.8 174,485.5C176.7,456.8 182.5,428.8 191.5,401.5C194.1,391.7 197.3,382 201,372.5C201.9,372 202.8,371.3 203.5,370.5C208.1,361.7 212.3,352.7 216,343.5C220.2,338.6 223.9,333.3 227,327.5C242.1,304 260,282.5 280.5,263C290.7,256.1 300.3,248.4 309.5,240C314.3,237.6 318.9,235.1 323.5,232.5C408.4,179.1 499.7,164.2 597.5,188C611.5,191.7 625.2,196.3 638.5,202C641.5,202.3 644.5,202.7 647.5,203C661.2,209.8 674.9,216.7 688.5,223.5Z"/>
<path android:fillColor="#cfdade" android:pathData="M556.5,177.5C577.5,179.3 597.9,184.2 617.5,192C621.3,192.3 624.9,192.9 628.5,194C636.6,198 645,201.2 653.5,203.5C663.6,208.4 673.6,213.6 683.5,219C685.6,220 687.3,221.5 688.5,223.5C674.9,216.7 661.2,209.8 647.5,203C644.5,202.7 641.5,202.3 638.5,202C625.2,196.3 611.5,191.7 597.5,188C499.7,164.2 408.4,179.1 323.5,232.5C330.8,226.2 338.8,220.9 347.5,216.5C348.5,215.1 349.5,213.8 350.5,212.5C363.9,207 377.2,201.1 390.5,195C402.2,191.3 413.8,187.7 425.5,184C448.7,180 472.1,177 495.5,175C505.5,174.3 515.5,174.3 525.5,175C535.8,176.1 546.2,176.9 556.5,177.5Z"/>
<path android:fillColor="#c4ced2" android:pathData="M556.5,177.5C546.2,176.9 535.8,176.1 525.5,175C515.5,174.3 505.5,174.3 495.5,175C472.1,177 448.7,180 425.5,184C413.8,187.7 402.2,191.3 390.5,195C377.2,201.1 363.9,207 350.5,212.5C368.9,202.5 387.9,193.8 407.5,186.5C412.9,185.3 418.2,183.8 423.5,182C435.6,180.5 447.6,178.3 459.5,175.5C491.2,171.4 522.9,171.5 554.5,176C555.4,176.3 556.1,176.8 556.5,177.5Z"/>
<path android:fillColor="#bac3c7" android:pathData="M652.5,201.5C652.8,202.2 653.2,202.8 653.5,203.5C645,201.2 636.6,198 628.5,194C624.9,192.9 621.3,192.3 617.5,192C597.9,184.2 577.5,179.3 556.5,177.5C556.1,176.8 555.4,176.3 554.5,176C522.9,171.5 491.2,171.4 459.5,175.5C490.2,169.9 521.2,169 552.5,173C566.6,176.6 580.9,179.2 595.5,181C602.4,185 609.7,188 617.5,190C629.9,191.5 641.6,195.4 652.5,201.5Z"/>
<path android:fillColor="#0e5565" android:pathData="M870.5,325.5C868.5,324.7 867,323.4 866,321.5C862.6,314.1 858.3,307.4 853,301.5C847.1,293.1 841.8,284.4 837,275.5C832.4,271.2 828,266.5 824,261.5C823.7,260.2 823.3,258.8 823,257.5C815,249.6 807,241.6 799,233.5C798.4,230.7 797.1,228.3 795,226.5C789.8,222.7 785.3,218.2 781.5,213C778.9,212.1 776.6,210.8 774.5,209C769.5,204.2 764.2,199.9 758.5,196C747.1,184.2 733.7,175.2 718.5,169C717.8,168 717.2,167 716.5,166C705.8,161.5 695.4,156.1 685.5,150C676.8,145.8 668,141.9 659,138.5C639.8,132.6 620.6,126.7 601.5,121C584.3,117.7 567,115.2 549.5,113.5C500.4,109.7 452.1,113.9 404.5,126C402.5,127 400.5,128 398.5,129C392.1,130.5 385.7,132.1 379.5,134C370.1,139.2 360.1,142.3 349.5,143.5C420.7,111.5 495,101.3 572.5,113C678.8,130.2 765.3,180.7 832,264.5C846.7,283.7 859.5,304 870.5,325.5Z"/>
<path android:fillColor="#a9b3b7" android:pathData="M652.5,201.5C655,202.8 657.7,203.4 660.5,203.5C704.9,224.6 742.6,254.2 773.5,292.5C765,283.5 756.4,274.7 747.5,266C744,263.6 741,260.8 738.5,257.5C733.8,254.1 729.1,250.8 724.5,247.5C722.4,245.3 720.1,243.3 717.5,241.5C709.8,236.7 702.1,232 694.5,227.5C692.9,225.7 690.9,224.3 688.5,223.5C687.3,221.5 685.6,220 683.5,219C673.6,213.6 663.6,208.4 653.5,203.5C653.2,202.8 652.8,202.2 652.5,201.5Z"/>
<path android:fillColor="#cfdadd" android:pathData="M694.5,227.5C702.1,232 709.8,236.7 717.5,241.5C720.1,243.3 722.4,245.3 724.5,247.5C740.5,264 756.6,280.3 773,296.5C779.5,304.9 785.8,313.3 792,321.5C792.5,322.8 792.7,324.1 792.5,325.5C790.8,324.9 789.3,323.9 788,322.5C763.8,287.3 734,257.8 698.5,234C696.7,232 695.4,229.8 694.5,227.5Z"/>
<path android:fillColor="#a8b3b6" android:pathData="M407.5,186.5C387.9,193.8 368.9,202.5 350.5,212.5C348.1,212.5 345.7,213 343.5,214C280.9,251.6 233.3,303.1 200.5,368.5C200,369.3 199.4,369.6 198.5,369.5C231,299.7 281,246.4 348.5,209.5C349.9,209.7 351.2,209.5 352.5,209C370.1,199.6 388.5,192.1 407.5,186.5Z"/>
<path android:fillColor="#c7d1d5" android:pathData="M350.5,212.5C349.5,213.8 348.5,215.1 347.5,216.5C338.8,220.9 330.8,226.2 323.5,232.5C318.9,235.1 314.3,237.6 309.5,240C300.3,248.4 290.7,256.1 280.5,263C260,282.5 242.1,304 227,327.5C223.9,333.3 220.2,338.6 216,343.5C212.3,352.7 208.1,361.7 203.5,370.5C202.8,371.3 201.9,372 201,372.5C197.3,382 194.1,391.7 191.5,401.5C187.7,410.3 184.4,419.3 181.5,428.5C183.2,412.5 188.1,397.5 196,383.5C196.7,378.7 197.5,374.1 198.5,369.5C199.4,369.6 200,369.3 200.5,368.5C233.3,303.1 280.9,251.6 343.5,214C345.7,213 348.1,212.5 350.5,212.5Z"/>
<path android:fillColor="#0f5969" android:pathData="M349.5,143.5C340.1,149.9 330.1,155.2 319.5,159.5C316.3,162.9 313,166 309.5,169C306.5,171.3 303.2,172.6 299.5,173C298.3,174.2 297.3,175.5 296.5,177C285.1,184.4 273.8,191.7 262.5,199C246.7,213.9 231.1,229.1 215.5,244.5C199.4,263.5 184.2,283.1 170,303.5C153.3,330.6 140,359.6 130,390.5C124.4,413.1 119,435.8 114,458.5C113.8,467.9 112.8,477.3 111,486.5C110.5,502.5 110.3,518.5 110.5,534.5C105.6,408 149,301.1 240.5,214C273,184.7 309.3,161.2 349.5,143.5Z"/>
<path android:fillColor="#b5bfc3" android:pathData="M724.5,247.5C729.1,250.8 733.8,254.1 738.5,257.5C741,260.8 744,263.6 747.5,266C756.4,274.7 765,283.5 773.5,292.5C804,327.4 825.6,367.4 838.5,412.5C837.5,412 836.7,411.4 836,410.5C833.5,403.3 831,396.2 828.5,389C820.3,372.7 812.4,356.2 805,339.5C803.9,338.7 802.7,338 801.5,337.5C799.9,333.3 797.6,329.6 794.5,326.5C793.8,326.2 793.2,325.8 792.5,325.5C792.7,324.1 792.5,322.8 792,321.5C785.8,313.3 779.5,304.9 773,296.5C756.6,280.3 740.5,264 724.5,247.5Z"/>
<path android:fillColor="#d0dade" android:pathData="M558.5,314.5C570.2,314.3 581.8,314.5 593.5,315C603.6,318.9 605.1,325 598,333.5C597.2,342.4 597.3,351.2 598.5,360C629.8,360.3 661.2,360.7 692.5,361C694.7,361.8 696.2,363.3 697,365.5C697.7,376.5 697.7,387.5 697,398.5C696.2,400 695,401.2 693.5,402C661.6,403.5 629.6,404 597.5,403.5C597.2,411.8 597.5,419.9 598.5,428C623.3,428 647.9,428.7 672.5,430C673.7,430.9 674.5,432.1 675,433.5C675.7,444.2 675.7,454.8 675,465.5C674.2,468.8 672,470.7 668.5,471C638.7,471.2 608.8,471.3 579,471.5C547.8,471.7 516.6,471.2 485.5,470C483.5,468.7 482.3,466.8 482,464.5C481.3,454.5 481.3,444.5 482,434.5C482.3,432.7 483.1,431.2 484.5,430C507.1,428.5 529.7,428 552.5,428.5C552.5,420.2 552.5,411.8 552.5,403.5C525.2,403.7 497.8,403.5 470.5,403C467.2,403.1 465.1,401.6 464,398.5C462,387.2 462,375.8 464,364.5C465.2,363 466.7,361.8 468.5,361C496.5,360.5 524.5,360.3 552.5,360.5C552.1,346.5 552.6,332.5 554,318.5C555.4,316.9 556.9,315.6 558.5,314.5Z"/>
<path android:fillColor="#849094" android:pathData="M591.5,317.5C580.5,317.5 569.5,317.5 558.5,317.5C558.8,332.7 558.5,347.7 557.5,362.5C557.5,347.2 557.5,331.8 557.5,316.5C569,316.2 580.3,316.5 591.5,317.5Z"/>
<path android:fillColor="#132c33" android:pathData="M591.5,317.5C599,318.8 599.5,321.8 593,326.5C592.5,338.5 592.3,350.5 592.5,362.5C625.8,362.5 659.2,362.5 692.5,362.5C692.5,373.2 692.5,383.8 692.5,394.5C658.8,394.5 625.2,394.5 591.5,394.5C591.2,406.7 591.5,418.7 592.5,430.5C618,431.5 643.7,431.8 669.5,431.5C669.5,441.8 669.5,452.2 669.5,462.5C608.8,462.5 548.2,462.5 487.5,462.5C487.5,451.8 487.5,441.2 487.5,430.5C510.2,430.5 532.8,430.5 555.5,430.5C556.2,430.5 556.8,430.5 557.5,430.5C558.5,418.7 558.8,406.7 558.5,394.5C528.5,394.5 498.5,394.5 468.5,394.5C468.5,383.8 468.5,373.2 468.5,362.5C498.2,362.5 527.8,362.5 557.5,362.5C558.5,347.7 558.8,332.7 558.5,317.5C569.5,317.5 580.5,317.5 591.5,317.5Z"/>
<path android:fillColor="#cfdade" android:pathData="M331.5,324.5C366.9,324 402.2,324.5 437.5,326C439.5,327.3 440.7,329.2 441,331.5C441.9,342.2 441.6,352.9 440,363.5C439,365.3 437.5,366.4 435.5,367C399.8,368 364.1,367.6 328.5,366C327.3,365.1 326.5,363.9 326,362.5C325.3,351.5 325.3,340.5 326,329.5C327.4,327.3 329.2,325.6 331.5,324.5Z"/>
<path android:fillColor="#b8c1c4" android:pathData="M330.5,331.5C330.2,341 330.5,350.3 331.5,359.5C331.6,360.1 331.9,360.6 332.5,361C349.3,361.2 366.2,361.3 383,361.5C400.2,361.5 417,360.8 433.5,359.5C434.2,359.5 434.8,359.5 435.5,359.5C435.8,359.5 436.2,359.5 436.5,359.5C436.4,361.4 435.4,362.6 433.5,363C399.2,363.7 364.8,363.7 330.5,363C329.9,362.3 329.4,361.4 329,360.5C328.3,351.2 328.3,341.8 329,332.5C329.4,331.9 329.9,331.6 330.5,331.5Z"/>
<path android:fillColor="#a3abaf" android:pathData="M331.5,359.5C365.5,359.5 399.5,359.5 433.5,359.5C417,360.8 400.2,361.5 383,361.5C366.2,361.3 349.3,361.2 332.5,361C331.9,360.6 331.6,360.1 331.5,359.5Z"/>
<path android:fillColor="#142d34" android:pathData="M330.5,327.5C365.5,327.5 400.5,327.5 435.5,327.5C435.5,338.2 435.5,348.8 435.5,359.5C434.8,359.5 434.2,359.5 433.5,359.5C399.5,359.5 365.5,359.5 331.5,359.5C330.5,350.3 330.2,341 330.5,331.5C330.5,330.2 330.5,328.8 330.5,327.5Z"/>
<path android:fillColor="#79868a" android:pathData="M330.5,327.5C365.7,326.5 401,326.2 436.5,326.5C436.5,337.5 436.5,348.5 436.5,359.5C436.2,359.5 435.8,359.5 435.5,359.5C435.5,348.8 435.5,338.2 435.5,327.5C400.5,327.5 365.5,327.5 330.5,327.5Z"/>
<path android:fillColor="#0f5a6b" android:pathData="M870.5,325.5C884.8,352.1 895.8,380.1 903.5,409.5C901.5,408.4 900,406.7 899,404.5C897.4,395.3 895.1,386.3 892,377.5C891.1,375.2 889.7,373.2 888,371.5C882.7,355.9 876.8,340.6 870.5,325.5Z"/>
<path android:fillColor="#8e979a" android:pathData="M468.5,394.5C498.5,394.5 528.5,394.5 558.5,394.5C558.8,406.7 558.5,418.7 557.5,430.5C558.1,419.1 557.5,407.9 555.5,397C528.2,396.7 500.8,396.3 473.5,396C471.6,395.8 470,395.3 468.5,394.5Z"/>
<path android:fillColor="#a6aeb1" android:pathData="M690.5,395.5C690.4,396.1 690.1,396.6 689.5,397C658.2,397.3 626.8,397.7 595.5,398C594.6,399 593.6,399.9 592.5,400.5C592.5,398.8 592.5,397.2 592.5,395.5C625.2,395.5 657.8,395.5 690.5,395.5Z"/>
<path android:fillColor="#b4bec1" android:pathData="M468.5,362.5C468.5,373.2 468.5,383.8 468.5,394.5C470,395.3 471.6,395.8 473.5,396C500.8,396.3 528.2,396.7 555.5,397C557.5,407.9 558.1,419.1 557.5,430.5C556.8,430.5 556.2,430.5 555.5,430.5C556.6,420.9 556.8,411.2 556,401.5C555.5,401 555,400.5 554.5,400C526.5,399.7 498.5,399.3 470.5,399C469.7,398.2 468.8,397.3 468,396.5C467.2,385 467.3,373.7 468.5,362.5Z"/>
<path android:fillColor="#b9c3c6" android:pathData="M690.5,395.5C691.2,395.5 691.8,395.5 692.5,395.5C691.9,396.5 691.2,397.5 690.5,398.5C658.8,399.3 627.2,399.8 595.5,400C595,400.5 594.5,401 594,401.5C593.2,411.2 593.4,420.9 594.5,430.5C593.8,430.5 593.2,430.5 592.5,430.5C592.5,420.5 592.5,410.5 592.5,400.5C593.6,399.9 594.6,399 595.5,398C626.8,397.7 658.2,397.3 689.5,397C690.1,396.6 690.4,396.1 690.5,395.5Z"/>
<path android:fillColor="#515f63" android:pathData="M692.5,394.5C692.5,394.8 692.5,395.2 692.5,395.5C691.8,395.5 691.2,395.5 690.5,395.5C657.8,395.5 625.2,395.5 592.5,395.5C592.5,397.2 592.5,398.8 592.5,400.5C592.5,410.5 592.5,420.5 592.5,430.5C591.5,418.7 591.2,406.7 591.5,394.5C625.2,394.5 658.8,394.5 692.5,394.5Z"/>
<path android:fillColor="#ced9dd" android:pathData="M191.5,401.5C182.5,428.8 176.7,456.8 174,485.5C167.8,567.8 188.1,642.5 235,709.5C235.4,712.7 236.2,715.7 237.5,718.5C214.2,688.9 196.9,655.9 185.5,619.5C182.6,606.1 179.5,592.8 176,579.5C174,566 171.8,552.6 169.5,539.5C167.9,501.8 171.9,464.8 181.5,428.5C184.4,419.3 187.7,410.3 191.5,401.5Z"/>
<path android:fillColor="#ced8dc" android:pathData="M313.5,384.5C359.2,384.3 404.8,384.5 450.5,385C452,386.2 453.2,387.7 454,389.5C454.7,400.5 454.7,411.5 454,422.5C451.8,425.8 448.6,427.3 444.5,427C400.5,428 356.5,427.6 312.5,426C311,424.8 309.8,423.3 309,421.5C308.3,410.8 308.3,400.2 309,389.5C310,387.3 311.5,385.6 313.5,384.5Z"/>
<path android:fillColor="#bbc4c8" android:pathData="M313.5,389.5C313.5,399.2 313.5,408.8 313.5,418.5C314.4,419.3 315.4,420.1 316.5,421C360.2,421.7 403.8,421.7 447.5,421C448.4,420.3 449,419.5 449.5,418.5C449.5,408.8 449.5,399.2 449.5,389.5C450.7,399.7 450.8,410 450,420.5C449.1,421.7 447.9,422.5 446.5,423C402.5,423.7 358.5,423.7 314.5,423C313.8,421.7 313,420.6 312,419.5C311.3,409.8 311.3,400.2 312,390.5C312.4,389.9 312.9,389.6 313.5,389.5Z"/>
<path android:fillColor="#9da6a9" android:pathData="M313.5,418.5C358.8,418.5 404.2,418.5 449.5,418.5C449,419.5 448.4,420.3 447.5,421C403.8,421.7 360.2,421.7 316.5,421C315.4,420.1 314.4,419.3 313.5,418.5Z"/>
<path android:fillColor="#132c33" android:pathData="M449.5,389.5C449.5,399.2 449.5,408.8 449.5,418.5C404.2,418.5 358.8,418.5 313.5,418.5C313.5,408.8 313.5,399.2 313.5,389.5C313.5,388.5 313.5,387.5 313.5,386.5C358.8,386.5 404.2,386.5 449.5,386.5C449.5,387.5 449.5,388.5 449.5,389.5Z"/>
<path android:fillColor="#273b41" android:pathData="M592.5,430.5C593.2,430.5 593.8,430.5 594.5,430.5C620.2,429.8 645.5,430.5 670.5,432.5C670.5,432.8 670.5,433.2 670.5,433.5C670.5,443.2 670.5,452.8 670.5,462.5C670.2,462.5 669.8,462.5 669.5,462.5C669.5,452.2 669.5,441.8 669.5,431.5C643.7,431.8 618,431.5 592.5,430.5Z"/>
<path android:fillColor="#b8c4c8" android:pathData="M198.5,369.5C197.5,374.1 196.7,378.7 196,383.5C188.1,397.5 183.2,412.5 181.5,428.5C171.9,464.8 167.9,501.8 169.5,539.5C163.8,480.3 173.5,423.6 198.5,369.5Z"/>
<path android:fillColor="#cfdadd" android:pathData="M330.5,445.5C365.8,445.3 401.2,445.5 436.5,446C439.1,447.3 440.6,449.5 441,452.5C441.9,463.2 441.6,473.9 440,484.5C439.2,485.3 438.3,486.2 437.5,487C401.5,487.7 365.5,487.7 329.5,487C327.3,486.3 326.1,484.8 326,482.5C325.3,471.8 325.3,461.2 326,450.5C327,448.3 328.5,446.6 330.5,445.5Z"/>
<path android:fillColor="#132c32" android:pathData="M435.5,448.5C435.5,458.8 435.5,469.2 435.5,479.5C400.8,479.5 366.2,479.5 331.5,479.5C331.5,469.2 331.5,458.8 331.5,448.5C366.2,448.5 400.8,448.5 435.5,448.5Z"/>
<path android:fillColor="#273b41" android:pathData="M435.5,447.5C435.5,447.8 435.5,448.2 435.5,448.5C400.8,448.5 366.2,448.5 331.5,448.5C331.5,458.8 331.5,469.2 331.5,479.5C330.5,470.7 330.2,461.7 330.5,452.5C330.5,450.8 330.5,449.2 330.5,447.5C365.5,447.5 400.5,447.5 435.5,447.5Z"/>
<path android:fillColor="#6b7679" android:pathData="M487.5,430.5C487.5,441.2 487.5,451.8 487.5,462.5C548.2,462.5 608.8,462.5 669.5,462.5C669.8,462.5 670.2,462.5 670.5,462.5C670.2,463.3 669.5,463.7 668.5,463.5C608.5,463.5 548.5,463.5 488.5,463.5C488.2,463.5 487.8,463.5 487.5,463.5C486.5,453 486.2,442.3 486.5,431.5C486.5,430.8 486.8,430.5 487.5,430.5Z"/>
<path android:fillColor="#c8d3d6" android:pathData="M486.5,431.5C486.2,442.3 486.5,453 487.5,463.5C487.8,465.8 489.1,466.9 491.5,467C550.8,467.7 610.2,467.7 669.5,467C670.9,465.8 671.7,464.3 672,462.5C672.7,453.2 672.7,443.8 672,434.5C671.6,433.9 671.1,433.6 670.5,433.5C670.5,433.2 670.5,432.8 670.5,432.5C672.4,432.6 673.6,433.6 674,435.5C674.7,445.2 674.7,454.8 674,464.5C672.6,466.1 671.1,467.6 669.5,469C608.8,469.7 548.2,469.7 487.5,469C486.2,467.6 485,466.1 484,464.5C483.3,454.2 483.3,443.8 484,433.5C484.7,432.6 485.5,432 486.5,431.5Z"/>
<path android:fillColor="#bac3c7" android:pathData="M670.5,433.5C671.1,433.6 671.6,433.9 672,434.5C672.7,443.8 672.7,453.2 672,462.5C671.7,464.3 670.9,465.8 669.5,467C610.2,467.7 550.8,467.7 491.5,467C489.1,466.9 487.8,465.8 487.5,463.5C487.8,463.5 488.2,463.5 488.5,463.5C488.6,464.1 488.9,464.6 489.5,465C548.8,465.7 608.2,465.7 667.5,465C668.1,464.6 668.4,464.1 668.5,463.5C669.5,463.7 670.2,463.3 670.5,462.5C670.5,452.8 670.5,443.2 670.5,433.5Z"/>
<path android:fillColor="#a6aeb1" android:pathData="M488.5,463.5C548.5,463.5 608.5,463.5 668.5,463.5C668.4,464.1 668.1,464.6 667.5,465C608.2,465.7 548.8,465.7 489.5,465C488.9,464.6 488.6,464.1 488.5,463.5Z"/>
<path android:fillColor="#0f5b6c" android:pathData="M903.5,411.5C914.1,450.5 918.1,490.2 915.5,530.5C915.7,515.5 915.4,500.5 914.5,485.5C910.8,460.8 907.2,436.2 903.5,411.5Z"/>
<path android:fillColor="#9fa8ab" android:pathData="M435.5,447.5C436.4,448.6 436.8,449.9 436.5,451.5C436.8,461.5 436.5,471.5 435.5,481.5C434.7,481.8 434,482.3 433.5,483C399.5,483.7 365.5,483.7 331.5,483C331,482.3 330.3,481.8 329.5,481.5C329.2,471.7 329.5,462 330.5,452.5C330.2,461.7 330.5,470.7 331.5,479.5C366.2,479.5 400.8,479.5 435.5,479.5C435.5,469.2 435.5,458.8 435.5,448.5C435.5,448.2 435.5,447.8 435.5,447.5Z"/>
<path android:fillColor="#c2ccd0" android:pathData="M436.5,451.5C437.1,451.6 437.6,451.9 438,452.5C438.7,462.2 438.7,471.8 438,481.5C437.6,482.4 437.1,483.3 436.5,484C418.2,485.1 399.9,485.6 381.5,485.5C364.5,485.2 347.5,484.8 330.5,484.5C329.9,483.6 329.5,482.6 329.5,481.5C330.3,481.8 331,482.3 331.5,483C365.5,483.7 399.5,483.7 433.5,483C434,482.3 434.7,481.8 435.5,481.5C436.5,471.5 436.8,461.5 436.5,451.5Z"/>
<path android:fillColor="#142c33" android:pathData="M459.5,485.5C538.8,485.5 618.2,485.5 697.5,485.5C697.5,509.5 697.5,533.5 697.5,557.5C686.2,557.5 674.8,557.5 663.5,557.5C663.5,545.8 663.5,534.2 663.5,522.5C663.5,521.8 663.5,521.2 663.5,520.5C663.5,519.5 663.5,518.5 663.5,517.5C606.8,517.5 550.2,517.5 493.5,517.5C493.5,531.5 493.5,545.5 493.5,559.5C482.2,559.5 470.8,559.5 459.5,559.5C459.5,536.5 459.5,513.5 459.5,490.5C459.5,489.2 459.5,487.8 459.5,486.5C459.5,486.2 459.5,485.8 459.5,485.5Z"/>
<path android:fillColor="#8f9a9e" android:pathData="M459.5,485.5C499.2,484.8 539,484.5 579,484.5C619.2,484.5 659,485.2 698.5,486.5C698.8,510.7 698.5,534.7 697.5,558.5C686,558.8 674.7,558.5 663.5,557.5C674.8,557.5 686.2,557.5 697.5,557.5C697.5,533.5 697.5,509.5 697.5,485.5C618.2,485.5 538.8,485.5 459.5,485.5Z"/>
<path android:fillColor="#bac6ca" android:pathData="M838.5,412.5C841.3,419.5 843.4,426.9 845,434.5C854.7,478.4 855.8,522.4 848.5,566.5C851.3,530.5 851.1,494.5 848,458.5C846.2,447.9 843.5,437.6 840,427.5C839.5,422.5 839,417.5 838.5,412.5Z"/>
<path android:fillColor="#132c32" android:pathData="M331.5,507.5C366.2,507.5 400.8,507.5 435.5,507.5C435.5,517.8 435.5,528.2 435.5,538.5C400.8,538.5 366.2,538.5 331.5,538.5C331.5,528.2 331.5,517.8 331.5,507.5Z"/>
<path android:fillColor="#cbd6d9" android:pathData="M459.5,486.5C459.5,487.8 459.5,489.2 459.5,490.5C458.3,514.3 458.2,538.3 459,562.5C459.5,563 460,563.5 460.5,564C471.2,564.7 481.8,564.7 492.5,564C493.3,563.7 494,563.2 494.5,562.5C495.3,549.9 495.8,537.2 496,524.5C496.9,523.3 498.1,522.5 499.5,522C553.2,521.3 606.8,521.3 660.5,522C661,522.5 661.5,523 662,523.5C662.3,535.5 662.7,547.5 663,559.5C663.8,560.3 664.7,561.2 665.5,562C675.2,562.7 684.8,562.7 694.5,562C695.7,561 696.7,559.8 697.5,558.5C698.5,534.7 698.8,510.7 698.5,486.5C699.5,487 700.3,487.6 701,488.5C701.7,511.8 701.7,535.2 701,558.5C700.3,561.3 698.4,562.8 695.5,563C684.5,563.7 673.5,563.7 662.5,563C662,562.5 661.5,562 661,561.5C659.9,549.4 659,537.2 658.5,525C605.2,524.5 551.8,524.3 498.5,524.5C498.7,537.5 498.5,550.5 498,563.5C496.2,566.3 493.7,567.8 490.5,568C479.8,568.9 469.1,568.5 458.5,567C456.3,565.4 454.9,563.2 454.5,560.5C455.3,537.2 455.8,513.9 456,490.5C456.7,488.7 457.8,487.3 459.5,486.5Z"/>
<path android:fillColor="#aab3b7" android:pathData="M663.5,520.5C663.5,521.2 663.5,521.8 663.5,522.5C662.7,522.2 662,521.7 661.5,521C605.8,520.2 550.1,520.3 494.5,521.5C494.9,520.1 495.9,519.2 497.5,519C552.2,518.3 606.8,518.3 661.5,519C662,519.7 662.7,520.2 663.5,520.5Z"/>
<path android:fillColor="#9ca4a7" android:pathData="M663.5,520.5C662.7,520.2 662,519.7 661.5,519C606.8,518.3 552.2,518.3 497.5,519C495.9,519.2 494.9,520.1 494.5,521.5C494.8,534.3 494.5,547 493.5,559.5C493.5,545.5 493.5,531.5 493.5,517.5C550.2,517.5 606.8,517.5 663.5,517.5C663.5,518.5 663.5,519.5 663.5,520.5Z"/>
<path android:fillColor="#132c33" android:pathData="M629.5,538.5C629.9,541.8 628.5,544.1 625.5,545.5C624.2,582 624.2,618.3 625.5,654.5C625.7,658.6 627.7,661.4 631.5,663C641.5,663.7 651.5,663.7 661.5,663C665.1,661.1 667.6,658.3 669,654.5C671.2,646.6 673,638.6 674.5,630.5C675,629.1 675.6,627.8 676.5,626.5C686.4,629.9 696.4,632.9 706.5,635.5C707.1,636.4 707.4,637.4 707.5,638.5C704.9,648.7 702.4,659 700,669.5C698.8,673.6 697.3,677.6 695.5,681.5C691.1,688.5 684.8,692.9 676.5,694.5C655.5,695.5 634.5,695.8 613.5,695.5C600.6,694.3 593.1,687.3 591,674.5C590.5,630.5 590.3,586.5 590.5,542.5C590.5,541.2 590.5,539.8 590.5,538.5C596.1,537.2 602.1,536.5 608.5,536.5C614.5,536.7 620.5,536.8 626.5,537C627.7,537.2 628.7,537.7 629.5,538.5Z"/>
<path android:fillColor="#2b3e44" android:pathData="M331.5,507.5C331.5,517.8 331.5,528.2 331.5,538.5C366.2,538.5 400.8,538.5 435.5,538.5C435.2,539.3 434.5,539.7 433.5,539.5C399.8,539.5 366.2,539.5 332.5,539.5C330.5,531.2 329.9,522.6 330.5,513.5C330.5,512.2 330.5,510.8 330.5,509.5C330.3,508.5 330.7,507.8 331.5,507.5Z"/>
<path android:fillColor="#b3bcbf" android:pathData="M435.5,507.5C436.2,508.8 437.1,510.1 438,511.5C438.7,521.2 438.7,530.8 438,540.5C437.6,542.4 436.4,543.4 434.5,543.5C400.5,543.7 366.5,543.5 332.5,543C331.7,542.2 330.8,541.3 330,540.5C329.2,531.3 329.3,522.3 330.5,513.5C329.9,522.6 330.5,531.2 332.5,539.5C332.8,540.2 333.1,540.8 333.5,541.5C366.5,542.7 399.5,542.8 432.5,542C433.3,541.3 433.6,540.5 433.5,539.5C434.5,539.7 435.2,539.3 435.5,538.5C435.5,528.2 435.5,517.8 435.5,507.5Z"/>
<path android:fillColor="#a5aeb1" android:pathData="M332.5,539.5C366.2,539.5 399.8,539.5 433.5,539.5C433.6,540.5 433.3,541.3 432.5,542C399.5,542.8 366.5,542.7 333.5,541.5C333.1,540.8 332.8,540.2 332.5,539.5Z"/>
<path android:fillColor="#c7d1d5" android:pathData="M330.5,509.5C330.5,510.8 330.5,512.2 330.5,513.5C329.3,522.3 329.2,531.3 330,540.5C330.8,541.3 331.7,542.2 332.5,543C366.5,543.5 400.5,543.7 434.5,543.5C417.5,544.8 400.2,545.5 382.5,545.5C365.2,545.3 347.8,545.2 330.5,545C330,544.1 329.3,543.2 328.5,542.5C327.3,532.2 327.2,521.8 328,511.5C328.7,510.6 329.5,510 330.5,509.5Z"/>
<path android:fillColor="#cdd8dc" android:pathData="M794.5,326.5C797.6,329.6 799.9,333.3 801.5,337.5C802.7,338 803.9,338.7 805,339.5C812.4,356.2 820.3,372.7 828.5,389C831,396.2 833.5,403.3 836,410.5C836.7,411.4 837.5,412 838.5,412.5C839,417.5 839.5,422.5 840,427.5C843.5,437.6 846.2,447.9 848,458.5C851.1,494.5 851.3,530.5 848.5,566.5C842.5,607.2 829.2,645.2 808.5,680.5C810.4,673.7 812.6,667.1 815,660.5C820.3,649.7 824.9,638.7 829,627.5C862.9,523.2 851.6,424.2 795,330.5C794.5,329.2 794.3,327.9 794.5,326.5Z"/>
<path android:fillColor="#b4bdc0" android:pathData="M459.5,490.5C459.5,513.5 459.5,536.5 459.5,559.5C470.8,559.5 482.2,559.5 493.5,559.5C494.5,547 494.8,534.3 494.5,521.5C550.1,520.3 605.8,520.2 661.5,521C662,521.7 662.7,522.2 663.5,522.5C663.5,534.2 663.5,545.8 663.5,557.5C674.7,558.5 686,558.8 697.5,558.5C696.7,559.8 695.7,561 694.5,562C684.8,562.7 675.2,562.7 665.5,562C664.7,561.2 663.8,560.3 663,559.5C662.7,547.5 662.3,535.5 662,523.5C661.5,523 661,522.5 660.5,522C606.8,521.3 553.2,521.3 499.5,522C498.1,522.5 496.9,523.3 496,524.5C495.8,537.2 495.3,549.9 494.5,562.5C494,563.2 493.3,563.7 492.5,564C481.8,564.7 471.2,564.7 460.5,564C460,563.5 459.5,563 459,562.5C458.2,538.3 458.3,514.3 459.5,490.5Z"/>
<path android:fillColor="#becacd" android:pathData="M169.5,539.5C171.8,552.6 174,566 176,579.5C179.5,592.8 182.6,606.1 185.5,619.5C176.5,593.7 171.1,567 169.5,539.5Z"/>
<path android:fillColor="#ccd7db" android:pathData="M674.5,630.5C673,638.6 671.2,646.6 669,654.5C667.6,658.3 665.1,661.1 661.5,663C651.5,663.7 641.5,663.7 631.5,663C627.7,661.4 625.7,658.6 625.5,654.5C625.3,619.5 625.5,584.5 626,549.5C630.1,546.6 631.3,542.9 629.5,538.5C628.7,537.7 627.7,537.2 626.5,537C620.5,536.8 614.5,536.7 608.5,536.5C602.1,536.5 596.1,537.2 590.5,538.5C589.8,538.8 589.2,539.1 588.5,539.5C587.3,586.2 587.2,632.8 588,679.5C588.6,683.6 590.1,687.2 592.5,690.5C593.3,691.4 594.3,691.7 595.5,691.5C599.4,695.2 604.1,697.7 609.5,699C631.2,699.7 652.8,699.7 674.5,699C678.4,697.5 682.4,696.4 686.5,695.5C691,693.2 694.3,689.7 696.5,685C697.8,684.7 698.9,684.2 700,683.5C702.3,679.1 703.9,674.4 705,669.5C705.5,659.1 706.3,648.7 707.5,638.5C707.4,637.4 707.1,636.4 706.5,635.5C708,636.2 708.9,637.6 709,639.5C710.4,656.8 707.1,673.1 699,688.5C693.4,696.1 685.9,701 676.5,703C653.8,703.7 631.2,703.7 608.5,703C595.4,699.9 587.9,691.8 586,678.5C585.3,631.8 585.3,585.2 586,538.5C587.1,536.2 588.9,534.7 591.5,534C602.5,533.3 613.5,533.3 624.5,534C630.5,534.3 633.8,537.3 634.5,543C633.9,546.9 632.4,550.4 630,553.5C629,588.5 629.4,623.5 631,658.5C632.5,660 634.3,660.8 636.5,661C644.6,661.9 652.6,661.6 660.5,660C667.7,652.6 671.7,643.8 672.5,633.5C672.6,632 673.3,631 674.5,630.5Z"/>
<path android:fillColor="#aeb7ba" android:pathData="M590.5,542.5C590.3,586.5 590.5,630.5 591,674.5C593.1,687.3 600.6,694.3 613.5,695.5C633.7,696.7 654,696.8 674.5,696C675.4,695.7 676.1,695.2 676.5,694.5C684.8,692.9 691.1,688.5 695.5,681.5C694,687.1 691,691.8 686.5,695.5C682.4,696.4 678.4,697.5 674.5,699C652.8,699.7 631.2,699.7 609.5,699C604.1,697.7 599.4,695.2 595.5,691.5C594.5,687.4 592.7,683.8 590,680.5C589.2,634.3 589.3,588.3 590.5,542.5Z"/>
<path android:fillColor="#838d90" android:pathData="M676.5,694.5C676.1,695.2 675.4,695.7 674.5,696C654,696.8 633.7,696.7 613.5,695.5C634.5,695.8 655.5,695.5 676.5,694.5Z"/>
<path android:fillColor="#bdc7ca" android:pathData="M707.5,638.5C706.3,648.7 705.5,659.1 705,669.5C703.9,674.4 702.3,679.1 700,683.5C698.9,684.2 697.8,684.7 696.5,685C694.3,689.7 691,693.2 686.5,695.5C691,691.8 694,687.1 695.5,681.5C697.3,677.6 698.8,673.6 700,669.5C702.4,659 704.9,648.7 707.5,638.5Z"/>
<path android:fillColor="#aeb7bb" android:pathData="M629.5,538.5C631.3,542.9 630.1,546.6 626,549.5C625.5,584.5 625.3,619.5 625.5,654.5C624.2,618.3 624.2,582 625.5,545.5C628.5,544.1 629.9,541.8 629.5,538.5Z"/>
<path android:fillColor="#c3cdd1" android:pathData="M590.5,538.5C590.5,539.8 590.5,541.2 590.5,542.5C589.3,588.3 589.2,634.3 590,680.5C592.7,683.8 594.5,687.4 595.5,691.5C594.3,691.7 593.3,691.4 592.5,690.5C590.1,687.2 588.6,683.6 588,679.5C587.2,632.8 587.3,586.2 588.5,539.5C589.2,539.1 589.8,538.8 590.5,538.5Z"/>
<path android:fillColor="#cad6db" android:pathData="M808.5,680.5C796.6,702.3 782.3,722.3 765.5,740.5C765.5,738.8 765.5,737.2 765.5,735.5C778.8,720.7 790.6,704.3 801,686.5C803.9,684.7 806.4,682.7 808.5,680.5Z"/>
<path android:fillColor="#d0dbdf" android:pathData="M474.5,666.5C466.2,669.7 457.6,671.4 448.5,671.5C447.8,671.9 447.3,672.6 447,673.5C446.7,673.2 446.3,672.8 446,672.5C445.7,640.8 445.3,609.2 445,577.5C444.8,576.3 444.3,575.3 443.5,574.5C443.5,572.8 443.5,571.2 443.5,569.5C403.3,569.2 363.3,569.5 323.5,570.5C323.5,571.5 323.5,572.5 323.5,573.5C322.9,573.6 322.4,573.9 322,574.5C321.3,619.5 321.3,664.5 322,709.5C322.8,710.3 323.7,711.2 324.5,712C333.8,714 343.2,714 352.5,712C353.9,710.8 354.7,709.3 355,707.5C355.3,703.8 355.7,700.2 356,696.5C356.4,695.6 356.9,694.8 357.5,694C374.2,693.3 390.8,693.3 407.5,694C408.4,694.4 409.3,694.9 410,695.5C409.7,700.1 411.2,703.9 414.5,707C423.2,707.7 431.8,707.7 440.5,707C442.3,706.2 443.8,705 445,703.5C446,697.9 446.9,692.2 447.5,686.5C448.7,687.3 449.7,688.3 450.5,689.5C452.8,693.9 454.6,698.5 456,703.5C459.1,710.8 464.6,713.3 472.5,711C481.2,708.8 489.2,705.3 496.5,700.5C502.5,698 507.8,694.5 512.5,690C513.8,689.5 515.1,689.3 516.5,689.5C518.3,688.9 519.9,688.1 521.5,687C527.7,679.8 534.2,673 541,666.5C552.5,646.5 558.9,624.8 560,601.5C560.3,584.5 560.7,567.5 561,550.5C563.7,548.3 564.8,545.5 564.5,542C564.4,540 563.8,538.5 562.5,537.5C560.9,537 559.2,536.6 557.5,536.5C546.3,535.5 535,535.2 523.5,535.5C523.5,536.5 523.5,537.5 523.5,538.5C522.2,539.1 521.4,540.1 521,541.5C520.8,565.5 520.2,589.5 519,613.5C517.2,619.1 515.7,624.8 514.5,630.5C508.7,644 499.4,654.3 486.5,661.5C486.6,660.5 486.3,659.7 485.5,659C499.8,650.4 509.3,637.9 514,621.5C515.8,614.9 517.2,608.3 518,601.5C518.3,582.2 518.7,562.8 519,543.5C518.8,538.6 521,535.5 525.5,534C536.5,533.3 547.5,533.3 558.5,534C569.5,537.3 571.3,543.5 564,552.5C563.9,572.5 563.2,592.5 562,612.5C554.2,670.8 521.4,705.1 463.5,715.5C461,715.2 458.8,714.2 457,712.5C454.1,706.2 451.5,699.9 449,693.5C448.2,697.1 447.6,700.8 447,704.5C445.2,707.3 442.7,708.8 439.5,709C430.4,709.8 421.4,709.5 412.5,708C411.7,707.2 410.8,706.3 410,705.5C409.8,702.2 409.3,699.1 408.5,696C391.8,695.3 375.2,695.3 358.5,696C357.7,701.1 357.2,706.3 357,711.5C356.5,713.3 355.3,714.5 353.5,715C343.5,715.7 333.5,715.7 323.5,715C322.1,714.5 320.9,713.7 320,712.5C318.3,666.3 318,619.9 319,573.5C319.9,570.2 322.1,568.4 325.5,568C365.2,567 404.9,567.4 444.5,569C445.7,569.9 446.5,571.1 447,572.5C447.5,605.2 447.7,637.8 447.5,670.5C456.5,669.2 465.5,667.8 474.5,666.5Z"/>
<path android:fillColor="#c5d0d3" android:pathData="M323.5,573.5C323.5,574.2 323.5,574.8 323.5,575.5C322.5,619.7 322.2,664 322.5,708.5C331.6,710.5 341,711 350.5,710C351.4,709.3 352,708.5 352.5,707.5C353.5,703 353.8,698.4 353.5,693.5C354.3,693.2 355,692.7 355.5,692C373.5,691.3 391.5,691.3 409.5,692C410,692.7 410.7,693.2 411.5,693.5C412.2,696.3 412.8,699 413.5,701.5C413.8,701.5 414.2,701.5 414.5,701.5C414.4,702.5 414.7,703.3 415.5,704C424.9,704.9 434.2,704.6 443.5,703C444.1,693.9 444.9,684.7 446,675.5C446.9,677.7 448,679.7 449.5,681.5C450.5,684.1 450.8,686.8 450.5,689.5C449.7,688.3 448.7,687.3 447.5,686.5C446.9,692.2 446,697.9 445,703.5C443.8,705 442.3,706.2 440.5,707C431.8,707.7 423.2,707.7 414.5,707C411.2,703.9 409.7,700.1 410,695.5C409.3,694.9 408.4,694.4 407.5,694C390.8,693.3 374.2,693.3 357.5,694C356.9,694.8 356.4,695.6 356,696.5C355.7,700.2 355.3,703.8 355,707.5C354.7,709.3 353.9,710.8 352.5,712C343.2,714 333.8,714 324.5,712C323.7,711.2 322.8,710.3 322,709.5C321.3,664.5 321.3,619.5 322,574.5C322.4,573.9 322.9,573.6 323.5,573.5Z"/>
<path android:fillColor="#737e82" android:pathData="M413.5,687.5C414.5,691.6 414.8,696 414.5,700.5C423.8,700.5 433.2,700.5 442.5,700.5C433.3,701.5 424,701.8 414.5,701.5C414.2,701.5 413.8,701.5 413.5,701.5C413.5,698.5 413.5,695.5 413.5,692.5C413.5,690.8 413.5,689.2 413.5,687.5Z"/>
<path android:fillColor="#b6bfc2" android:pathData="M413.5,692.5C413.5,695.5 413.5,698.5 413.5,701.5C412.8,699 412.2,696.3 411.5,693.5C410.7,693.2 410,692.7 409.5,692C391.5,691.3 373.5,691.3 355.5,692C355,692.7 354.3,693.2 353.5,693.5C353.5,692.2 353.5,690.8 353.5,689.5C372.2,689.3 390.8,689.5 409.5,690C411,690.7 412.3,691.5 413.5,692.5Z"/>
<path android:fillColor="#a2aaae" android:pathData="M413.5,687.5C413.5,689.2 413.5,690.8 413.5,692.5C412.3,691.5 411,690.7 409.5,690C390.8,689.5 372.2,689.3 353.5,689.5C353.5,690.8 353.5,692.2 353.5,693.5C353.8,698.4 353.5,703 352.5,707.5C352.5,700.8 352.5,694.2 352.5,687.5C372.8,687.5 393.2,687.5 413.5,687.5Z"/>
<path android:fillColor="#a5aeb1" android:pathData="M547.5,645.5C546.6,651.6 544.5,657.3 541,662.5C533.3,672 525.1,681 516.5,689.5C515.1,689.3 513.8,689.5 512.5,690C507.8,694.5 502.5,698 496.5,700.5C486.3,703.7 475.9,706.7 465.5,709.5C461.2,709.8 459.2,707.8 459.5,703.5C460.6,704.6 461.4,705.9 462,707.5C490.8,703.1 514.8,690.1 534,668.5C539.2,661.2 543.7,653.5 547.5,645.5Z"/>
<path android:fillColor="#b8c2c6" android:pathData="M562.5,537.5C563.8,538.5 564.4,540 564.5,542C564.8,545.5 563.7,548.3 561,550.5C560.7,567.5 560.3,584.5 560,601.5C558.9,624.8 552.5,646.5 541,666.5C534.2,673 527.7,679.8 521.5,687C519.9,688.1 518.3,688.9 516.5,689.5C525.1,681 533.3,672 541,662.5C544.5,657.3 546.6,651.6 547.5,645.5C553,630.3 556.5,614.6 558,598.5C558.3,580.8 558.7,563.2 559,545.5C562.5,543.6 563.7,540.9 562.5,537.5Z"/>
<path android:fillColor="#b4bdc0" android:pathData="M323.5,575.5C323.5,619.5 323.5,663.5 323.5,707.5C333.2,707.5 342.8,707.5 352.5,707.5C352,708.5 351.4,709.3 350.5,710C341,711 331.6,710.5 322.5,708.5C322.2,664 322.5,619.7 323.5,575.5Z"/>
<path android:fillColor="#bcc5c9" android:pathData="M443.5,574.5C444.3,575.3 444.8,576.3 445,577.5C445.3,609.2 445.7,640.8 446,672.5C446.3,672.8 446.7,673.2 447,673.5C447.3,672.6 447.8,671.9 448.5,671.5C452.2,682.2 455.8,692.8 459.5,703.5C459.2,707.8 461.2,709.8 465.5,709.5C475.9,706.7 486.3,703.7 496.5,700.5C489.2,705.3 481.2,708.8 472.5,711C464.6,713.3 459.1,710.8 456,703.5C454.6,698.5 452.8,693.9 450.5,689.5C450.8,686.8 450.5,684.1 449.5,681.5C448,679.7 446.9,677.7 446,675.5C444.9,684.7 444.1,693.9 443.5,703C434.2,704.6 424.9,704.9 415.5,704C414.7,703.3 414.4,702.5 414.5,701.5C424,701.8 433.3,701.5 442.5,700.5C443.5,658.7 443.8,616.7 443.5,574.5Z"/>
<path android:fillColor="#8e999d" android:pathData="M323.5,570.5C363.3,569.5 403.3,569.2 443.5,569.5C443.5,571.2 443.5,572.8 443.5,574.5C443.8,616.7 443.5,658.7 442.5,700.5C442.5,657.2 442.5,613.8 442.5,570.5C402.8,570.5 363.2,570.5 323.5,570.5Z"/>
<path android:fillColor="#142c33" android:pathData="M323.5,570.5C363.2,570.5 402.8,570.5 442.5,570.5C442.5,613.8 442.5,657.2 442.5,700.5C433.2,700.5 423.8,700.5 414.5,700.5C414.8,696 414.5,691.6 413.5,687.5C393.2,687.5 372.8,687.5 352.5,687.5C352.5,694.2 352.5,700.8 352.5,707.5C342.8,707.5 333.2,707.5 323.5,707.5C323.5,663.5 323.5,619.5 323.5,575.5C323.5,574.8 323.5,574.2 323.5,573.5C323.5,572.5 323.5,571.5 323.5,570.5Z"/>
<path android:fillColor="#728084" android:pathData="M413.5,597.5C414.5,618.3 414.8,639.3 414.5,660.5C393.7,660.8 373,660.5 352.5,659.5C352.8,659.5 353.2,659.5 353.5,659.5C354.2,659.5 354.8,659.5 355.5,659.5C374.5,659.5 393.5,659.5 412.5,659.5C413.5,640 413.8,620.3 413.5,600.5C413.5,599.5 413.5,598.5 413.5,597.5Z"/>
<path android:fillColor="#bcc6ca" android:pathData="M413.5,600.5C413.8,620.3 413.5,640 412.5,659.5C412.8,640.8 412.5,622.2 411.5,603.5C393.2,602.3 374.9,602.2 356.5,603C356,603.5 355.5,604 355,604.5C354.2,622.9 354.3,641.2 355.5,659.5C354.8,659.5 354.2,659.5 353.5,659.5C353.2,639.9 353.5,620.4 354.5,601C364.4,599.9 374.4,599.4 384.5,599.5C394.2,599.5 403.9,599.9 413.5,600.5Z"/>
<path android:fillColor="#cfdade" android:pathData="M412.5,659.5C393.5,659.5 374.5,659.5 355.5,659.5C354.3,641.2 354.2,622.9 355,604.5C355.5,604 356,603.5 356.5,603C374.9,602.2 393.2,602.3 411.5,603.5C412.5,622.2 412.8,640.8 412.5,659.5Z"/>
<path android:fillColor="#d7e2e6" android:pathData="M358.5,605.5C375.2,605.5 391.8,605.5 408.5,605.5C408.5,622.8 408.5,640.2 408.5,657.5C391.6,657.8 374.8,657.5 358,656.5C357.2,639.4 357.3,622.4 358.5,605.5Z"/>
<path android:fillColor="#a0a8ab" android:pathData="M413.5,597.5C413.5,598.5 413.5,599.5 413.5,600.5C403.9,599.9 394.2,599.5 384.5,599.5C374.4,599.4 364.4,599.9 354.5,601C353.5,620.4 353.2,639.9 353.5,659.5C353.2,659.5 352.8,659.5 352.5,659.5C352.5,638.8 352.5,618.2 352.5,597.5C372.8,597.5 393.2,597.5 413.5,597.5Z"/>
<path android:fillColor="#8e999d" android:pathData="M557.5,536.5C546.5,536.5 535.5,536.5 524.5,536.5C524.8,555 524.5,573.3 523.5,591.5C523.5,573.8 523.5,556.2 523.5,538.5C523.5,537.5 523.5,536.5 523.5,535.5C535,535.2 546.3,535.5 557.5,536.5Z"/>
<path android:fillColor="#c4ced2" android:pathData="M523.5,538.5C523.5,556.2 523.5,573.8 523.5,591.5C523.6,605.4 520.6,618.4 514.5,630.5C515.7,624.8 517.2,619.1 519,613.5C520.2,589.5 520.8,565.5 521,541.5C521.4,540.1 522.2,539.1 523.5,538.5Z"/>
<path android:fillColor="#142c33" android:pathData="M557.5,536.5C559.2,536.6 560.9,537 562.5,537.5C563.7,540.9 562.5,543.6 559,545.5C558.7,563.2 558.3,580.8 558,598.5C556.5,614.6 553,630.3 547.5,645.5C543.7,653.5 539.2,661.2 534,668.5C514.8,690.1 490.8,703.1 462,707.5C461.4,705.9 460.6,704.6 459.5,703.5C455.8,692.8 452.2,682.2 448.5,671.5C457.6,671.4 466.2,669.7 474.5,666.5C478.9,665.6 482.9,663.9 486.5,661.5C499.4,654.3 508.7,644 514.5,630.5C520.6,618.4 523.6,605.4 523.5,591.5C524.5,573.3 524.8,555 524.5,536.5C535.5,536.5 546.5,536.5 557.5,536.5Z"/>
<path android:fillColor="#cad8dc" android:pathData="M601.5,841.5C553.1,855.4 504.1,858.1 454.5,849.5C488.9,853.4 523.2,853.2 557.5,849C572.2,846.4 586.8,843.9 601.5,841.5Z"/>
</vector>
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file was created by '@expo/configure-splash-screen' and some of it's content shouldn't be modified by hand
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
</layer-list>
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/splashscreen_background"
android:gravity="center"
android:orientation="vertical"
android:scaleType="centerCrop"
android:navigationBarColor="#333333"
tools:context=".MainActivity">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="130dp"
app:lottie_rawRes="@raw/loading"
app:lottie_autoPlay="true"
app:lottie_loop="false"
/>
</LinearLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<color name="iconBackground">#FFFFFF</color>
<color name="splashscreen_background">#1F2024</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#202125</color>
<color name="navigationBarColor">#202125</color>
<color name="notification_icon_color">#00adb5</color>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<string name="app_name">LNReader</string>
</resources>
@@ -1,33 +0,0 @@
<?xml
version="1.0"
encoding="UTF-8"
standalone="yes"
?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
<item name="colorControlActivated">#2596be</item>
</style>
<style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<!-- Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually -->
<!-- Customize your splash screen theme here -->
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/colorPrimaryDark</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s" >@drawable/invisible</item>
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="s">1000</item>
<!-- <item name="icon">@drawable/no_icon</item> -->
<item name="android:windowBackground">@drawable/splashscreen</item>
</style>
</resources>
-21
View File
@@ -1,21 +0,0 @@
buildscript {
ext {
buildToolsVersion = "36.0.0"
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
kotlinVersion = "2.1.20"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
}
}
apply plugin: "com.facebook.react.rootproject"
-56
View File
@@ -1,56 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=false
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=false
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn't support animated webp
expo.webp.animated=false
# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=false
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-251
View File
@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
-99
View File
@@ -1,99 +0,0 @@
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
@REM
@REM This source code is licensed under the MIT license found in the
@REM LICENSE file in the root directory of this source tree.
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-11
View File
@@ -1,11 +0,0 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'LNReader'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
useExpoModules()
+82 -7
View File
@@ -1,19 +1,94 @@
{
"expo": {
"version": "2.1.4",
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"icon": "./assets/native/ic_launcher.png",
"plugins": [
"expo-localization",
"react-native-edge-to-edge",
"./plugins/withAndroidCustomizations",
[
"expo-sqlite",
"expo-splash-screen",
{
"enableFTS": false,
"useSQLCipher": false
"image": "./assets/native/ic_launcher_round.png",
"backgroundColor": "#1D1B20",
"resizeMode": "contain",
"dark": {
"image": "./assets/native/ic_launcher_round.png",
"backgroundColor": "#000000"
},
"android": {
"image": "./assets/native/splash_icon.png",
"backgroundColor": "#132C33",
"imageWidth": 288,
"resizeMode": "native",
"dark": {
"image": "./assets/native/splash_icon.png",
"backgroundColor": "#132C33"
}
}
}
],
"expo-web-browser"
]
"expo-localization",
[
"react-native-edge-to-edge",
{
"android": {
"enforceNavigationBarContrast": false
}
}
],
"expo-web-browser",
"./plugins/withReaderAssets",
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true,
"manifestApplicationAttributes": {
"android:largeHeap": "true",
"android:allowBackup": "true"
},
"enableMinifyInReleaseBuilds": true,
"enableShrinkResourcesInReleaseBuilds": true
}
}
]
],
"android": {
"versionCode": 4196356,
"package": "com.rajarsheechatterjee.LNReader",
"adaptiveIcon": {
"foregroundImage": "./assets/native/ic_launcher_foreground.png",
"backgroundImage": "./assets/native/ic_launcher_background.png",
"monochromeImage": "./assets/native/ic_launcher_monochrome.png"
},
"permissions": [
"android.permission.DOWNLOAD_WITHOUT_NOTIFICATION",
"android.permission.WAKE_LOCK",
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_DATA_SYNC",
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
"android.permission.POST_NOTIFICATIONS"
],
"intentFilters": [
{
"action": "SEND",
"category": ["DEFAULT"],
"data": [{ "mimeType": "text/plain" }]
}
]
},
"extra": {
"eas": {
"projectId": "3a63596f-e80c-4342-96b7-a8ac48910489"
}
},
"ios": {
"bundleIdentifier": "com.rajarsheechatterjee.LNReader",
"infoPlist": {
"UIBackgroundModes": ["audio"]
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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