Compare commits

...

275 Commits

Author SHA1 Message Date
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
Palloxin 41ea7eb7b7 chore: Add onScrollEnd Polyfill (#1609) 2025-11-14 19:01:27 +05:30
Rajarshee Chatterjee e31eb9d457 fix: Move WebView Button to Right Actions
[skip ci]
2025-11-14 18:55:30 +05:30
Rajarshee Chatterjee 7842e860cb chore: Update Translations (#1346)
[skip ci]
2025-11-14 15:46:29 +05:30
Rajarshee Chatterjee daf7bf2ccb refactor: Move to Pagination Component and Replace Drawer with BottomSheet 2025-11-14 14:14:41 +05:30
Rajarshee Chatterjee a712100cfc feat: TTS Improvements
Closes #1500
Closes #1210
Closes #1244
Closes #1110
2025-11-14 13:07:37 +05:30
Rajarshee Chatterjee 92e1210fc8 feat: Add In-App Language Selector in Appearance Settings
Closes #1577
2025-11-13 17:19:20 +05:30
Rajarshee Chatterjee 5b4058c093 feat: Add “Has Result” Filter for Global Search
Closes #1512
2025-11-13 16:48:56 +05:30
Rajarshee Chatterjee 77ebdebcca fix: Center Align Button Labels 2025-11-12 21:25:23 +05:30
Rajarshee Chatterjee ea72de0113 fix: Ensure 'Download All' and 'Unread' Download Entire List, Not Just Batch 2025-11-12 21:17:18 +05:30
Rajarshee Chatterjee a1c0820da8 fix: BottomSheets After React Native 0.81 Upgrade 2025-11-12 20:11:29 +05:30
Rajarshee Chatterjee c9eb42ff4d chore: Upgrade React Native to 0.81
This is a manual cherry-pick from @CD-Z’s PR that updates React Native to 0.81. Thanks for originally working on it
2025-10-23 16:39:11 +05:30
Rajarshee Chatterjee 8f0c39ab41 chore: Update Kotlin Version to 2.1.20 2025-10-23 15:35:14 +05:30
Rajarshee Chatterjee 4de059d5bb chore: Add Missing Gradle Plugin 2025-10-23 15:29:32 +05:30
Rajarshee Chatterjee 969572b5a0 chore: Remove Unnecessary PNPM Setup Option 2025-10-23 14:55:02 +05:30
Rajarshee Chatterjee 4236037b6b chore: Switch to PNPM and Rename Scripts 2025-10-23 14:48:12 +05:30
Rajarshee Chatterjee a800c98d02 feat: Revamp Reader Settings UI (#1612)
[skip ci]
2025-10-23 14:10:55 +05:30
Rajarshee Chatterjee f1c825f826 feat: Implement Structured Database Migration System (#1621) 2025-10-23 13:00:10 +05:30
Rider21 fba8974699 fix: Pass imageRequestInit at Missing Places
Closes #1771
2025-10-23 00:49:23 +05:30
Rajarshee Chatterjee 10b90de171 feat: Replace FlashList with LegendList (Closes #1589) 2025-10-22 22:17:31 +05:30
Rajarshee Chatterjee 3785dddebc feat: Add Drag and Drop Support in Categories Screen
Closes #1592
2025-10-22 19:49:45 +05:30
Rajarshee Chatterjee 259dd6acc7 feat: Fetch All Chapters for EPUB Export and Add Range Selection
Closes #1605
Closes #1606
2025-10-22 18:45:08 +05:30
Rajarshee Chatterjee 90160f7f40 fix: Miscellaneous Issues
[skip ci]
2025-10-22 18:22:20 +05:30
Rajarshee Chatterjee 8bdc86c974 feat: Check for New Release Once Per Day
[skip ci]
2025-10-22 17:31:33 +05:30
Rajarshee Chatterjee 5b441562f8 feat: Add Toasts for Creating and Restoring Backups
[skip ci]
2025-10-22 17:24:59 +05:30
Rajarshee Chatterjee 4e1a67efc8 fix: Refactor and Fix EPUB Export (Closes #1611) 2025-10-22 17:03:55 +05:30
Rajarshee Chatterjee cc217e2b04 feat: Add Scroll-to-Top Functionality in Chapter List (Closes #1470)
[skip ci]
2025-10-22 16:19:27 +05:30
Rajarshee Chatterjee f8d4f50307 feat: Add Pinned Plugins and Refactor Plugin List Components
[skip ci]
2025-10-22 15:56:37 +05:30
Rajarshee Chatterjee d0e14d518e feat: Implement Multi-Tracker Support (Closes #1381) 2025-10-22 12:54:27 +05:30
Rajarshee Chatterjee c06d6f9326 feat: Add MangaUpdates Tracker (#1613)
* feat: Add MangaUpdates Tracker

* refactor: Migrate Tracker Components to TypeScript
2025-10-22 12:46:50 +05:30
Rajarshee Chatterjee 1200b7f75b fix: Make Advanced Section Scrollable (Closes #1513)
[skip ci]
2025-10-21 20:33:52 +05:30
Rajarshee Chatterjee ee7d96daf9 feat: Add System Theme Support, Yin & Yang Theme, and Adaptive Plugin Badge Color
Closes #437
Closes #1374
Closes #1578
2025-10-21 19:05:00 +05:30
Rajarshee Chatterjee 5a6f324638 feat: Add Sort by Unread Chapters (Closes #1524) 2025-10-21 16:36:53 +05:30
Rajarshee Chatterjee 4b1ff0efc1 fix: Skip Updating Imported Entries During Library Update (Closes #1404)
[skip ci]
2025-10-21 15:26:09 +05:30
Rajarshee Chatterjee 45174af3c2 fix: Include Imported Entries in “Downloaded Only” Mode (Closes #1261) 2025-10-21 15:20:34 +05:30
Rajarshee Chatterjee baabc073f0 fix: Search Functionality on History Screen (Closes #1418) 2025-10-21 14:11:35 +05:30
Rajarshee Chatterjee 37969da3e9 fix: Prevent Crash When App Is Closed During Onboarding (Closes #1517) 2025-10-21 14:02:56 +05:30
Rajarshee Chatterjee a81b32ae7f chore: Update README 2025-10-18 23:55:38 +05:30
Rajarshee Chatterjee 389fee8ecb chore: Update CI Workflows 2025-10-18 23:55:12 +05:30
Rajarshee Chatterjee 8fcfd22767 chore: Update CI Workflows 2025-10-18 22:20:50 +05:30
Rajarshee Chatterjee b216d3739a chore: Update CI Workflows 2025-10-18 22:17:33 +05:30
yammyy dcceea064a fix: Update Only the Selected Category During Category Update (#1594)
For this:
1) Select all novelIds that are in chosen category
2) Select novels by chosen Ids
2025-10-18 21:56:08 +05:30
Rajarshee Chatterjee 64c027c695 chore: Temporarily Disable Type Check 2025-10-18 21:39:54 +05:30
ilona 0dd9f2bd94 feat: Reimplement Local Backup Based on Self-Hosted Backup (#1603)
Closes #1276
2025-10-18 21:33:59 +05:30
ilona 80228ada1e chore: Improve General Plugin DX (#1604)
* enable webview debugging in dev builds only

* Fixed error downloading empty files (#1601)

* Allow data attributes in chapter HTML #1602
2025-10-18 09:51:10 +05:30
Palloxin 00c180927e Fix broken Bionic Reading (#1588)
fix broken Bionic Reading
2025-09-21 21:11:21 +07:00
Palloxin 7f61219bb0 Improve ‘remove extra spacing’ feature (#1579)
* Improve ‘remove extra paragraphs’ feature

* fix lint

* fix little mistake

* lil edit of code comment

* a little bit faster regex

* forgot to add final \s*
2025-09-21 21:10:11 +07:00
Palloxin 00d3a441e3 Improve regex of import and bionic reading (#1485)
* Improve regex of import

* Update import.ts

* Update import.ts

* lint

* lint

* bionic reading

* lint

* resolve conflict

* fix lint

* Update import.ts

* [^]

* lil fix
2025-09-07 22:07:43 +07:00
Palloxin f6b82939de chore: Update Readme to Display 2.0 Prereleases (#1572)
* Update readme to display 2.0 prereleases

* Change "preview" to "beta"

* last
2025-09-01 23:51:46 +05:30
Patrick Loser 3f2cfc253b added nix flake for a reproducible environment (#1565)
* added flake.nix

* added documentation

* added reference to CONTRIBUTING.md
2025-08-16 13:44:47 +02:00
Palloxin b107d3ca54 fix: Default Line Height ofsup Tag in Reader Mode (#1546) 2025-08-14 21:02:56 +05:30
rvx05 6bda33efd4 feat: Add Catppuccin Theme (#1561)
* Added Catppuccin Theme

* catppuccin theme fixes
2025-08-14 20:52:48 +05:30
Palloxin 1baa3e914c feat: Allow "details" And "summary" HTML Tags In The Sanitizer (#1551) 2025-08-14 20:50:19 +05:30
Patrick Loser ccbbe36ce6 Readd commits from before force push
Fixing mistake
2025-06-06 18:51:32 +02:00
nyagami 9e81f805c0 fix: epub parsing (#1518)
* fix: decode path before save

* fix: absolute path to label
2025-05-25 16:39:18 +07:00
nyagami 47428ec484 feat: import epub cpp (android) (#1514) 2025-05-25 08:15:58 +07:00
Patrick Loser 58f9427008 Small fixes & rendering time improvements (#1511)
* fix not being able to change from chapter drawer

* add UpdatesContext

* fix library search

* Add Switch for plugin settings

* improved BrowseScreen rendering time

* improved rendering time further

* fix download button
2025-05-24 18:18:16 +02:00
Patrick Loser 5e0f375f7f chore: fix all lint warnings (#1510)
* fix all lint warnings

* fix types
2025-05-20 00:02:24 +02:00
Patrick Loser 21611de3aa feat: Added chapter caching & page reader improvements
* update dependencies

* added stopPropagation

* change setInterval to onscrollend

* update progress save logic

* update chapter progress on reader exit

* chapter preloading

* reduced rerender on chapter change

* organize index.css

* added styles

* context restructure

* working page reader upgrade

* reduced unnecessary rerenders

* preserve reader settings during chapter change
2025-05-19 22:33:48 +02:00
nyagami 70520223cd feat: ios native modules (unstable) 2025-05-18 13:49:45 +07:00
nyagami 2c8880c9af [skip ci] splash screen ios (unusable) 2025-05-18 01:47:10 +07:00
nyagami ee864d3e81 [skip ci] feat: IOS (unusable) (#1504)
* feat: IOS (unusable)

* rename action

* remove Gem.lock
2025-05-11 15:46:16 +07:00
nyagami 6730c773f3 [skip ci]: new action for types checking 2025-05-11 10:14:24 +07:00
Patrick Loser 241cb0a664 Improvement: change bottom tabs implementation
* change bottom tabs implementation

* show navbar outside of chapter
2025-05-09 23:13:51 +02:00
Patrick Loser 3d59b8f774 fix: categories and update library on novel import (#1498)
* update epub

* fix categorization of novels

* fix: update library when importing novel

* update categories on change
2025-05-09 20:39:41 +02:00
Patrick Loser 188700112a fix library update crashing the app (#1496) 2025-05-09 19:11:50 +02:00
Patrick Loser 03f582da98 fix backup modal positioning (#1495) 2025-05-09 10:34:21 +02:00
Patrick Loser ed215a1d54 fix focusmode and readerscreen animations (#1494)
* update addToLibrary logic

* replace GlobalSearchNovelItem with NovelCover

* fix NovelCover and related type issues

* fix addToLibrary logic

* installed react-native-edge-to-edge

* fix Footer behaviour in Reader

* fix type issue

* fix Header behaviour in Reader

* fix ToolWrapper animation

* fix
2025-05-09 10:29:31 +02:00
Patrick Loser c487bd2ca6 fix: type errors
* fix app.tsx

* fix InfoItem

* fix helpers

* fix webview ref

* fix MoreScreen

* fix NovelScreenButtonGroup

* fix ReaderScreen

* fix readerTextAlignSelector

* fix ReaderValueChange

* fix UpdateNovelCard

* fix myAnimeList

* fixed legacy backup

* fix migrateNovel
2025-05-08 22:39:02 +02:00
Patrick Loser f90a4d1103 fix: logic related to LibraryContext
* update addToLibrary logic

* replace GlobalSearchNovelItem with NovelCover

* fix replace chaptersRead with chaptersUnread

* remove LegendList from browseTabs

* remove Legendlist from LibraryBottomSheet

* remove LegendList from JumpToChapterModal

* remove LegendList from NovelDrawer

* remove LegendList from ChapterDrawer

* fix NovelCover and related type issues

* added check for pages

* fix addToLibrary logic

* fix lint
2025-05-08 21:38:41 +02:00
nyagami 8306296736 added typescript checking 2025-05-04 21:06:12 +07:00
nyagami ae022052d5 [skip ci] more strict linter 2025-05-04 20:58:09 +07:00
Patrick Loser 7299e53df6 [skip ci] fix: correct save novel cover alignment
fix: fix alignment of save novel cover button
2025-05-04 15:22:03 +02:00
Neel Narayan Shetty c513361a5e fix: fix alignment of save novel cover button 2025-05-04 18:36:27 +05:30
Neel Shetty 429829b80b feat: Save Novel Cover (#1481)
* feat: Save novel cover (#1454)

* fix: handle storage permission for saving novel cover image

* fix: resolve conflicts with new arch
2025-05-04 17:35:16 +05:30
CD-Z ad1ccd815a lint + prettier 2025-05-04 10:48:50 +02:00
CD-Z 036608f7a8 back to Flashlist 2025-05-04 10:41:17 +02:00
CD-Z 8747e8ad39 fix themepicker style 2025-05-04 10:10:12 +02:00
CD-Z 537cf48fb4 fix chapter insert 2025-05-04 09:49:55 +02:00
CD-Z 9f51adb196 update packages 2025-05-04 09:49:55 +02:00
nyagami f2d3de4470 [skip ci] fix: eslint (except insert chapters) (#1487) 2025-05-03 22:53:04 +07:00
Soopyboo32 5f4507cfd5 Better chapter download notification (#1401)
* Better chapter download notification

* fix

* Update starting tasks list when new tasks are added to queue

* Give tasks an id

* Requested changes
2025-05-03 22:28:33 +07:00
Rajarshee Chatterjee ecc7a7a6e7 fix: Database Schema 2025-05-03 18:46:07 +05:30
nyagami 8d5b92bc9e Feat: React Native new architecture (#1420)
* update depedencies (buidable)

* fix: init db once

* upgrade react navigation v7

* better deeplink handler

* upgrade react naitve vector icons

* updated sql querries (may need further adjustments)

* fix: added type for icon name

* update react-native-reanimated, react-native-gesture-handler

* update google signin

* update tabivew & bottom sheet

* make all repository queries synchroniuously

* update categoryqueries

* import db in categoryqueries

* update chapter queries

* update import novel

* updated LibraryUpdateQueries

* fix download chapter

* fixed stats

* update history queries

* improved updates tab logic

* update LibraryQueries

* update NovelQueries

* improved import novel

* fixed notifications for android 14 and up

* fix Cant parse on first opening

* fixed loss of precision error

* Partial Loading of NovelScreen

* NovelScreenButtonGroup animation

* NovelScreen appbar animations

* update migrate novel

* feat: rewrite VolumeButtonListener to turbor module

* minor: clean up

* fixed type errors

* Novelscreen updates

* fix: better material-design-icons for react-native-paper

* migrate react-native to 0.77, good bye metro console streaming

* replace elevation with boxShadow

* rewrite FileManager -> NativeFile in TurborModule

* rewrite ZipArchive -> NativeZipArchive

* rewrite EpubUtil -> NativeEpubUtil

* dev improvement: active arch only for ndk and run launch debug id

* experimental React compiler (works but no significant improvements)

* ReaderBotttomSheet rewrite

* remove db from all query helper functions

* simplified db helper types

* better helper functions

* switch to native-stack

* removed console.log

* Change loading in UpdatesScreen

* pass novelcover on opening novel

* cleanup UpdateNovelCard

* fixed lint errors

* changed novelscreen animation behaviour

* fix UpdatesScreen

* fix lint errors

* fix unwanted downloadbutton behaviour

* fix: wrong key for multi

* fix: react-native-screens override

* remove abi filters for debug

* fix selecting chapters not working correctly

* adjust novelappbar animation

* preserve Layout when opening novel image

* added SafeAreaViews

* set Modal shadow to transparent

* replace rn paper modal with @components modal

* fix go prev chapter goes to first chapter

* fix open drawer with button

* fix SafeAreaView

* fix LibraryScreen

* improve BottomSheet performance

* SafeAreaView for global search

* fix NovelScreen SafeArea

* restructure useNovel

* remove useNovel from NovelScreenList due to possible db lock

* fix NovelCover not showing on first opening

* fix library not updating  changes to novels

* fix menu covering taskbar

* added FadeIn transition to appbar elements

* added SafeSnapPoints to keep bottomsheet inside of the safe area

* upgrade packages

* maybe round download button for jacubb

* fix space after autoscroll

* fix ReaderBottomSheet switch

* fix Jump to Chapter style

* fix Jump to chapter behaviour

* add Loading state to novelcover

* insertChapters performance improvement

* getChapter and getNovel db optimization

* updated db indexes

* New Skeleton for NovelScreen

* smoother FAB button

* add fetching state to useNovel

* fix paged novel chapter navigation

* fix type issues

* fix expanded novelCover on rotation

* add batched loading for chapters

* fix download button

* fix novelScreen loading state

* upgrade react native 0.78

* update to LegendList and fixed type errors

* fix chapter drawer

* added novel context + moved chapter & novel into ReaderStack

* fix lint

* improved chapterlist performance

* improved ChapterListSkeleton performance

* improved updates skeleton

* reworked onboading screen

* fix chapter drawer render error

* add NovelContext [unstable]

* fix filter compatibility

* upgrade eslint + add tanstack query

* eslint auto fix

* fix eslint errors

* upgrade expo 53, react native 0.79.1

* remove console

* remove unused package

* fix: exact vesion for core packages

* update contributing versions

---------

Co-authored-by: CD-Z <p_loser_Austria@gmx.at>
2025-05-01 23:05:30 +07:00
Batorian 0f5eee1bdb Improvement: Updated Sanitizer (#1129)
* update: GitHub workflow-dispatch

* fix: separate app for testing

* fix: separate app for testing

* fix: separate app for testing

* fix: new css for chapter content

* fix: separate app for testing

* branch: new test branch

* Enhancement: added more CSS

* build

* Enhancement: git build

* Fix: git build

* merge

* Enhancement: added more CSS

* dev: update build.yml

* dev: update sanitizer

* sanitizer: updated sanitizer

* dev: update index css

* dev: update sanitizer

* dev: update sanitizer

* sanitizer: update sanitizer

* sanitizer: added more allowed tags

* filter: fix filters

* progress-tracking: initial commit

* progress-tracking: implemented tracking

* progress-tracking: update tracker

* progress-tracking: complete rework

* progress-tracking: improvements

* sanitizer: update sanitizer

* sanitizer: block text-align

* sanitizer: update img

* sanitizer: reworked

* sanitizer: reworked

* sanitizer: fixed lazyloaded images

* sanitizer: revert sanitizer

* sanitizer: remove alt and title from images

* sanitizer: revert other changes

* sanitizer: fix

* sanitizer: update
2025-03-01 11:23:21 +07:00
Adam Kemish 4e3f817792 Update global search result if added to library (#1414) 2025-02-25 21:56:34 +07:00
Adam Kemish c61c4d0105 Fix app crash when downloading novel entry if it is on the recent updates page (#1395) 2025-01-20 23:23:07 +07:00
Adam Kemish 659988330c Fix some bugs with task queue (#1384)
* Fix novel chapter downloading spinner not showing in chapter list

* Fix novel chapter download notification staying on 'Prepairing'
2025-01-18 15:43:04 +07:00
jacksin125 430a067105 Fixed last read not updating and bookmark button in reader mode. (#1368)
* Fixed "Continue Reading" button

The lastRead chapter object that the "Continue Reading" button uses for everything is now updated on entering and leaving the chapter, instead of never updating when information changes.

* Fixed bookmark button not saving state

Done by passing the bookmark state down from the parent.

* Fixed bug caused by my own code

More thorough testing revealed that lastRead did not update properly on chapter switch

* Fixed issue that rendered previous fix ineffective

The original fix caused a new bug when changing chapters, and fixing that caused some of the old bugs to return. It should now be completely fixed.

---------

Co-authored-by: orionduffy <45501204+orionduffy@users.noreply.github.com>
2025-01-13 09:06:35 +07:00
MOAYAD c2c8935068 fix : ERROR $ open failed: EPERM (Operation not permitted) (#1371)
When you try to create a backup file on Android 11+ device you get this error 
/storage/emulated/0/h/inreader_backup_YYYY-MM-DD_HH_mm.json: open failed: EPERM (Operation not permitted)
2025-01-04 16:16:28 +07:00
Adam Kemish 9636aab53f fix: Resolve Service Manager Issue Caused by Unbound ‘this’ (#1375)
* Fix servicemanager broken due to unbound this

* Can use  here now!
2025-01-01 17:27:32 +05:30
Palloxin 22915914fb feat: Improve ‘Remove Extra Paragraph Spacing’ Feature (#1203)
* Improve "Remove extra paragraph spacing" feature

* Adjust in case of p with classes

* Lint

* Lint - part 2

* Lint - part 3

* forgot \s*

* \u00a exception

found here https://readnovelfull.com/the-wife-of-a-powerful-family-huo-shao-how-dare-you-flirt-with-me/chapter-1.html

* Better

* Fix

* fix lint

* forgot ``?:``

* Remove zero width

Removed until a concrete source example has them.

* fix lint

* lint

* better v2

* better 2.1

* Lint

* \s

* Exception: double br are used to simulate paragraphs

NovelUpdates - The Extra of The Bad Ending Game, chapter 159

* double br exception correction

* -

* better exception

* Lint

* flaw correction

doest match ``ahahah<br><br><br><br><br>ahahahaha``

* fix little flaw

* Shortening regex

* faster regex

* comment + faster regex

* lint

* lint

* Improve double br exception

* Correct flaw

* forgot

* \b for p

* Revert correct flaw

* Minimal faster regex

* remove useless piece

* forgot

* Faster regex

* very minor change

* Max level fast

* forgot a thing

* missing \s

* plus ultra

* suppefast

* ops

* u200b
2024-12-29 20:20:14 +05:30
Adam Kemish d39dabef68 Improve Task Queue (#1363)
* Improve Task Queue

* Seperate setMeta function
2024-12-29 13:00:24 +07:00
MOAYAD e01cbefdae fix: legacy backup file name (#1365)
in android file system you can't use the char  ' : ' 
so lnreader file name:
"Inreader_backup_YYYY-MM-DD_HH:mm.json"
you can't write it as a file. 
so i edit it to looks like
"Inreader_backup_YYYY-MM-DD_HH_mm.json"
2024-12-27 22:17:18 +07:00
Brian Fernandez 73fcba007a fix: "Add tag" input does not work correctly (#1357)
* fix: "Add tag" entry does not add tags correctly if there is at least 1 tag

* fix: "Add tag" entry allows to add empty tags when there is at least 1 tag
2024-12-20 22:34:53 +07:00
Brian Fernandez 54a3feb08c fix: "Edit cover" function is not called (#1354) 2024-12-15 12:52:31 +07:00
Rajarshee Chatterjee ecb3875fe2 chore: Update Translations (#1317)
* 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 (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 (Russian)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations strings.json (German)
[ci skip]
2024-12-01 21:11:44 +05:30
nyagami 2c9df1d93f more useful empty chapter message 2024-11-27 20:46:58 +07:00
nyagami 5db79a09eb fix: missing history navigation 2024-11-26 23:39:10 +07:00
Adam Kemish ff771fdd68 Better bottom navigation (#1335)
* Better bottom navigation

* Add double press actions to the rest of the tabs

* Fix history screen double press

* Fix lint error

* Fix more lint errors 😢

* 😢

* memo
2024-11-26 23:09:49 +07:00
Nil Silva 169c4b9de1 Add 'multi' language option (#1314)
* Added plugin settings

* Get plugin settings from plugin.

* Added multi language option

* Translate multi string

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
2024-11-26 23:08:04 +07:00
nyagami 4bcd22b9de fix: margin for flatlist container not content container 2024-11-26 22:44:54 +07:00
nyagami 8a2dc15da6 fix: novel status 2024-11-26 22:39:56 +07:00
nyagami 3f3a407820 closes #1263; fix: invoke old refreshChapters 2024-11-26 22:06:20 +07:00
nyagami c72808d72f closes #1322; fix: tracking sheet position 2024-11-26 21:21:29 +07:00
nyagami dabe4710bb Fix: reader page padding & back press (#1339)
* fix: remove bottom padding in page reader

* fix: disable drag transition when holding

* reader bottom sheet backpress handler

* close drawer on backpress

* fix: undefined index

* chores: added onboading strings
2024-11-26 20:47:58 +07:00
Adam Kemish cdc4ddcc97 perf: Small changes following faster global search pr (#1320)
* Small changes following faster global search pr

* Fix lint error
2024-11-25 22:17:18 +07:00
Rajarshee Chatterjee 8f783fd4c2 refactor: Miscellaneous Cleanup (Closes #1307) 2024-11-10 11:12:08 +05:30
Rajarshee Chatterjee 048119e530 chore: Update Translations (#1308)
* New translations strings.json (Russian)
[ci skip]

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

* New translations strings.json (Russian)
[ci skip]
2024-11-09 08:14:52 +05:30
nyagami b68d44d500 refactor: Remove Unused Translations (#1316) 2024-11-09 08:13:00 +05:30
Rider21 4bc3638b38 link fixing (#1302)
* test

* Update downloadChapter.ts

* react-native-url-polyfill

* -urlencode

* oops

* +urlencode

* fix prettier

* wrong
2024-11-06 21:07:22 +07:00
Rajarshee Chatterjee bc5e2f1fd1 fix: Use onSurface Color for Chapter Names (Closes #1259) 2024-11-05 09:23:41 +05:30
Rajarshee Chatterjee e8e7519c40 fix: Show Empty Search State When No Results in Available Tab 2024-11-05 08:39:19 +05:30
Rajarshee Chatterjee 5460047e41 fix: Use Safe Area Insets for WebView Screen (Closes #1301) 2024-11-05 08:16:53 +05:30
Rajarshee Chatterjee bdd00cd87d perf: Add Lazy Loading for All Screens 2024-11-05 08:12:35 +05:30
Rajarshee Chatterjee f83c017e09 fix: fix: Display Loading Animation on Novel Screen 2024-11-05 08:07:31 +05:30
Rajarshee Chatterjee e38cb9972f chore: Update Translations (#1305)
* 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 (Indonesian)
[ci skip]

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

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

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

* Update crowdin.yml
2024-11-04 20:21:22 +05:30
Rajarshee Chatterjee ac9c789a89 chore: Update Translations 2024-11-04 20:09:48 +05:30
Adam Kemish 445187cf11 perf: Optimize Global Search Performance (#1255)
* Faster and less laggy global search

- Memoise each row in the search results page
- Limit number of concurrent searches (has setting, defaults to 5 sources at once)
- Load sources in the same order as they show in menu so from the user's pov it loads in faster
- Cancel search when u close global search
- Made the order of sources that are still loading follow the sorted order so theres less popin
- Added setting to remove animations from the loading placeholders
- Pause global search if u open a sub window (eg opening a novel)
- Change no results color to look less active

* Cancel previous global search when starting another

* Dont clear global search on unfocus
2024-11-04 18:30:11 +05:30
Rajarshee Chatterjee 52ec11a33d refactor: Prevent Multiple Database Connections 2024-11-04 17:30:36 +05:30
Palloxin c498d7e9cd feat: Increase Transparency of Android Navigation Bar (#1267)
Makes it more transparent in the reader mode.
2024-10-30 21:36:48 +05:30
Nil Silva 0fad9b907f feat: Add User Input Support for Plugin Configurations (#1273)
* Added plugin settings

* Get plugin settings from plugin.
2024-10-30 21:36:00 +05:30
Adam Kemish 0a170f14bc fix: Adjust Cover Height for Screen Width Changes (#1295) 2024-10-30 21:32:21 +05:30
Adam Kemish 8245e77672 feat: Add Switch and TextInput Support to Plugin Filters (#1271)
* Add FilterTypes.Switch support to plugin filters

* Add FilterTypes.TextInput support to plugin filters
2024-10-12 15:39:49 +05:30
nguyd1 9c68260592 fix: Resolve 'Can't Parse Page' SQL Syntax Error (#1266)
fix cant parse page sql syntax error
2024-10-04 17:24:37 +05:30
Palloxin b1778c5802 chore: Genericize Preview Novel Details (#1239) 2024-09-05 10:54:10 +05:30
Justin COLLON be4ed6de79 setEnvFile: fix error when no .env file (#1237) 2024-09-01 17:33:14 +07:00
Skeltons 3597b542d3 Fix: novel info not saving (#1232) 2024-08-27 20:44:56 +07:00
Palloxin fd6d912391 Disable user-select on next-button and info-text (#1225)
Disable user-select on next-button and info-text
2024-08-21 23:01:54 +07:00
Rider21 d9dc18da43 fix: download files with relative url (#1216)
* hard fix

* test

* Revert "test"

This reverts commit 1fe9247752fcbadf348b6017f2d1fbf05165cfce.

* error handler

[URL](https://github.com/facebook/react-native/blob/v0.72.10/packages/react-native/Libraries/Blob/URL.js)
2024-08-21 21:25:39 +07:00
Skeltons a22d754f95 feat: Reduce Sensitivity of Swiping for Plugins/Sources (#1220)
* Enhancement: Make swiping of plugins/sources less sensitive

* Update swiping offset to be less aggressive
2024-08-17 22:15:52 +05:30
Skeltons d9d7d3aaf0 feat: Reduce Sensitivity of Swiping for Plugins/Sources (#1219) 2024-08-17 15:15:09 +05:30
S.M.A Sayem Prodhan a399927264 fix: Resolve Search Error with Query Containing ' (#1133)
* fix search error when query contains '

* change search to prepared statement

* added missing semicolons
2024-08-17 15:12:12 +05:30
Palloxin b4b00a5c66 fix: Improve Progress Bar Proportions (#1197)
Progress bar better proportions
2024-08-17 15:10:04 +05:30
Justin COLLON 9861f81589 fix: Infinite Plugin Updates in Development Mode (#1217)
* re fix setEnvFile.cjs

* infinit plugin update if in dev mode
2024-08-17 15:09:08 +05:30
Skeltons d06aecb5f8 feat: Sort Source List Alphabetically (#1218) 2024-08-17 15:07:30 +05:30
nyagami 86c82c1a61 Fix reader bugs (#1212)
* fix: tts algorithm

* update: turn off tts by default

* update default padding

* change tts transition to 500ms

* update reader body padding

* cleanup

* fix: throw Exception
2024-08-15 23:07:01 +07:00
nyagami 1131446ddc Improvemed bottomsheets (#1195)
* smooth scroll behavior for reader bottom sheet

* fix: BottomSheet flatlist

* add bottom inset for reader bottom sheet
2024-08-11 16:15:15 +07:00
nyagami f6b2516643 Feat: rewrite Reader in vanjs (#1194)
* setup van.js

* scrollbar

* Reader Footer

* move state to reader object

* scroll to previous progress

* image modal

* TTS bubble

* swipe gestures

* binoic reading & remove extra spacing

* tap to scroll

* tap & scroll to move page

* page reader move chapter

* display page in footer

* hide progress bar in page reader mode

* remember postion when switch reader mode

* refactor: move initial scroll out of scrollbar

* scroll bar in page reader mode

* chapter ending content

* fix: tts controller

* fix: tts highlight & stopping

* optional tts

* custom js

* update preview reader

* cleanup

* fix: css
2024-08-11 10:37:28 +07:00
Palloxin 7cf8692e77 fix: Reduce Font Size of Reader Progress Bar (#1153)
From 16px to 14px.
It's better looking.
2024-07-19 11:12:46 +05:30
nyagami d6290294f7 Improvement: not using replace screen to navigate chpater 2024-07-17 21:58:38 +07:00
nyagami d5cb0a234f improvement: simplify shared chapter states with react context 2024-07-16 23:11:41 +07:00
nyagami 306d8d1d42 Improvement: move chapter logic to useChapter (almost) 2024-07-16 22:04:48 +07:00
nyagami 7b1eb7dd9d Improvement: Optimise available plugins by flashlist 2024-07-15 23:42:51 +07:00
nyagami 1ad2c98473 Feat: search plugin with id 2024-07-15 22:39:03 +07:00
nyagami f0b8df93b4 Fix: better download chapter error handling
at least
2024-07-15 22:30:11 +07:00
nyagami 19ce7da32e Feat: voice rate/pitch settings 2024-07-15 21:47:33 +07:00
nyagami 9d35f6d303 minor: allow title tag 2024-07-15 20:44:13 +07:00
Patrick Loser 31d0aa1787 feat: Add Horizontal Reading Mode (#1021)
* Horizontal Reading Mode (#733)

Closes #29
Closes #149

* fixed first few compatibility issues

* added WebView debugging help

* fixed pages logic + open next chapter at end

* hopefully fixed infinity glitch

* removed logs

* fixed small issues

* working page numbering

* double L shape navigation

* changed let to const

* fixed scrollTo function

* cleanup and fixed css issues

* fixed display bug

* fixed settings bug

* added change padding setting

* added strings

* save + scroll on opening chapter logic

* resolved totalpages detection issue

* simplified getInt function

* css cleanup

* removed useless

* fixed padding scaling bug

* deleted debug items

* additional top padding

* smaller chapter height

* fixed contextMenu

* replaced name: readerPages » pageReader

* fixed ReaderSettings Preview Reader behaviour

* cleanup

* ReaderValueChange improvements

* moved page state to js

* fixed type issues

* functionality test

* fix: NaN page on scrollbar

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
2024-07-14 18:10:38 +05:30
nyagami ea5fde4156 Fix: add check multiplicable tasks & repace download queue by task queue 2024-07-14 00:42:52 +07:00
Rajarshee Chatterjee 03e3816ab8 fix: Optimize Marking Chapters as Read or Unread 2024-07-13 20:10:50 +05:30
Justin COLLON 9d6e407c40 fix: Retain Existing Data in .env File (#1130)
* setEnvFile: keeps data from .env file

* prettier for scripts
2024-07-13 18:37:32 +05:30
Batorian 6781b5d6a4 fix: Display Filter Label if Present (#1134)
filter: fix filters
2024-07-13 18:34:30 +05:30
Palloxin 352913f00e fix: Improve Image Placement in Preview (#1094)
* Better place the image in preview

The preview has to.. preview decently:
1. the image is too big (screen space taken)
2. in raw-chapter-text mode the img link is HUGE and one has to scroll a lot.

* Better place the image preview V2
2024-07-13 18:20:32 +05:30
Rajarshee Chatterjee 73f7ebf7b5 fix: Reset Chapter Progress When Marked as Unread (Closes #678) 2024-07-13 13:16:24 +05:30
Rajarshee Chatterjee 045d6419d8 fix: Fix Default Chapter Sorting (Closes #1060) 2024-07-13 12:59:35 +05:30
Rajarshee Chatterjee acc9d8e7ed feat: Pre-fill Edit Information with Existing Data and Add a Reset Option
Closes #1081
Closes #960
2024-07-13 12:52:47 +05:30
Rajarshee Chatterjee a29cce9eba feat: Add Description for "Disable haptic feedback" (Closes #1057) 2024-07-13 12:25:40 +05:30
Rajarshee Chatterjee 1292f520bb feat: Export as EPUB (Closes #1137) 2024-07-13 12:19:02 +05:30
Rajarshee Chatterjee 8bd6131862 feat: Add Option for "Keep screen on" (Closes #1137) 2024-07-13 11:06:29 +05:30
Rajarshee Chatterjee ed6c2b7ba2 feat: Add Empty State in Available Plugins Tab 2024-07-13 10:53:25 +05:30
nyagami f68a7a4e6a Fix: legacy restore 2024-07-12 10:07:50 +07:00
nyagami c9d2ce382f [skip ci] Dev improvement: Different configs for debug app 2024-07-12 09:29:53 +07:00
nyagami 5262567d61 Minor: hide reload button for custom page novels 2024-07-07 07:30:11 +07:00
nyagami b45a30f0cc Fix: avoid re-render in novel screen 2024-07-07 07:20:47 +07:00
nyagami 867b63af22 Feat: Plugin Custom JS/CSS 2024-07-06 11:42:18 +07:00
nyagami b3a5e237f4 added default cover 2024-07-06 11:41:29 +07:00
nyagami 7aae3a81c3 Fix: inlibrary badge 2024-07-06 11:32:25 +07:00
nyagami 078796784b Feat: Service Manager (#1124)
* feat: ServiceManager & import multiple epub files

* Fix: merge chapter based on index of toc in file list

* notifcation when jobs get done

* Fix epub: only read table of content in navmap

* add update library to service manager

* add drive backup/restore to service manager

* add self host backup/restore to service manager

* add migrate novel to service manager

* add download chapter to service manager

* clean up
2024-07-05 23:32:10 +07:00
Patrick Loser 15aed61aa6 Feat: tailing skeletons (#1064)
* tailing loading covers

* fix type issues

* fixed compatibility issues

* show skeleton even if row complete

* cleanup

* replaced !! with Boolean()

* Update type names

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* updated type occurances

* fixed key issue

* deleted unused vars and comments

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
2024-07-05 23:31:53 +07:00
Justin COLLON b67d6f8088 fix: update defaultCover URL (#1122) 2024-06-30 02:14:04 +05:30
nyagami 9b4bdfea89 Merge epub chapters (#1117)
* Fix: ignore non existed files

* merge chapters & scan cover

* remove unused tocMap

* clean code
2024-06-27 16:04:50 +07:00
r1di 70220a5a87 Fix: Volume scrolling (#1114)
* Volume scrolling fix

the scrolling by volume button right now scrolls 100% of the screen, which hides text when scrolled so, the commit is just changing the the scrolling to 50%.

* typo

* 50% to 75% scroll

---------

Co-authored-by: Ridi <ridi.culous.progger@gmail.com>
2024-06-25 20:48:10 +07:00
Patrick Loser aa95743d1c Fix: hide progressbar on opening drawer (#1102) 2024-06-25 20:20:47 +07:00
nyagami eb818ef55b add website to about page 2024-06-23 17:37:35 +07:00
nyagami 0bda5fd055 No default repo 2024-06-23 17:37:29 +07:00
Patrick Loser ea336902f2 Fix: Scroll to current Chapter (#1101)
* cleanup + fixed scroll to current Chapter

* removed unused memoization

* removed unused import

* ChapterDrawer.tsx => index.tsx

* added correct types

* applied suggestion

* changed chapter.position to scrollIndex + 2

* refactored index.ts
2024-06-14 23:17:22 +07:00
nyagami 58a633e5b9 [skip ci] ignore prettier for android folder 2024-06-13 00:39:19 +07:00
nyagami 74ed5a446d Fix: background color for plugin icon 2024-06-13 00:17:41 +07:00
nyagami a5d073a1ec Fix: Adding Category/Repo buttons position 2024-06-12 23:45:51 +07:00
nyagami f4d3d9d4eb Fix: navigation bar content color after closing reader 2024-06-12 23:40:51 +07:00
nyagami 772a9ef3a2 Feat: Plugin lazy init(#1104)
* move plugins to single folder

* lazy initial

* clean up

* fix: restore novels
2024-06-12 16:26:55 +07:00
Patrick Loser 6f3278f6ff Fix: Allow id and class attributes (#1066)
added exceptions to sanitizeHtml

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
2024-06-10 20:26:34 +07:00
Adam Kemish f11a10ba22 Feat: Add open random entry button (#1098)
* Add open random entry button

* Move menu logic to searchbarv2
2024-06-10 20:20:54 +07:00
Justin COLLON 02304fd394 [skip ci] version increase for default plugins repo(#1074) 2024-06-09 12:18:05 +07:00
nyagami 94d11a12ff Fix: detect gzip images 2024-06-09 10:42:08 +07:00
nyagami fca37868f1 Feat: NOVEL and CHAPTER reader context 2024-06-08 23:01:07 +07:00
nyagami 574f54a9bc Fix: update novel logic for custom page 2024-06-08 22:12:38 +07:00
nyagami fb0649f07f Minor fix: update novel toast 2024-06-08 22:06:12 +07:00
nyagami e3def3821e Fix: Repository add button position 2024-06-08 21:44:44 +07:00
nyagami 334c981013 Fix: add imageRequestInit for Reader instead of fetching base64 2024-06-08 21:42:07 +07:00
nyagami b62c8e3aea Download file with cookie persisted from RN (#1100)
* download file

* clean up
2024-06-08 20:15:41 +07:00
nyagami 09a482e4b7 Fix: do not modify available plugins 2024-06-08 01:57:51 +07:00
nyagami b7bbbbe962 Fix: revert to app storage 2024-06-08 01:42:47 +07:00
Elfing 30265471b5 [skip ci] Adding support for French localization (#1092)
synchro Text translate
2024-06-04 21:35:02 +07:00
nyagami b4b038dd42 fix: use back button instead of previous 2024-06-04 19:37:45 +07:00
nyagami 6517028cd4 minor fix: more header & error on first time launching 2024-06-04 19:28:21 +07:00
nyagami 3f62f6f225 Feat: Permissions requesting (#1096)
fix: app permissions & remove restore errors
2024-06-04 18:52:51 +07:00
nyagami 822256229f Refactor: convert Java to Kotlin (#1095)
* kotlin migrating

* fix: kotlin generations

* fix: unzip & cleanup
2024-06-04 14:14:03 +07:00
nyagami 4f23e1814e Feat: Onboarding screen & FileManager (#1093)
* base onboard screen

* pick theme step

* cleanup

* refactor: TextFile -> FileManager

* pick storage folder

* default root storage

* refactor: fixed storage -> custom storage

* replace rnfs by native module FileManager

* clean up

* refactor: corresponding theme list
2024-06-03 21:21:46 +07:00
Justin COLLON aec6c34b66 Update: Version utils & Github actions (#1072)
* github action:
 - custom apk:
   - packet name with git commit id
   - app name with git commit id
 - cached `npm install`
 - update actions versions

* About page:
 - Version based on package.json
 - added git hash to the version if not Stable
 - RELEASE_DATE, GIT_HASH, BUILD_TYPE created at build time from setEnvFile.cjs
 - special BUILD_TYPE for github actions
 - Version modal copy on click
 - removed versionUtils

* expo-clipboard instead of react-native clipboard

* buildRelease Release instead of Debug ^^'
Gihtub Actions => Github Action
2024-05-31 00:00:07 +07:00
Rider21 e50b6c4a47 Feat: Plugin storages (#986)
* test

* fix

* Update pluginManager.ts

* test

* fix lint

* test

* test webview

* sync

* /

* Update WebviewScreen.tsx

* CookieManager

* Update storage.ts

* Update сookie.ts

* fix title

* MarqueeText...

* Slow

* Even slower

* Squashed commit of the following:

commit da4d7e878aced6567b98fc802c34715b27dfbe00
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Fri Mar 8 13:21:19 2024 +0300

    /

commit 592c4dbfca2d04931ffffba19d1eec20db3ac183
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Fri Mar 8 13:20:34 2024 +0300

    test

commit 63240502d67cda6e851d602728b873e0b6d2aed0
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Fri Mar 8 12:38:13 2024 +0300

    /

commit 591d2592a7f64e65da4f2c61216b564f3eafa2e6
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Fri Mar 8 11:42:03 2024 +0300

    fix progressbar

commit 3d34cfb72827fdcf61267eafbb128b9e893ef40f
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 21:13:49 2024 +0300

    forgot

commit c36d090a60eadfcc4bee1f0cae04bf9213a694a4
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 20:51:31 2024 +0300

    /

commit 3c4e43b7bb6f845d94737aa8cba96b0dc1714b4b
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 20:15:38 2024 +0300

    /

commit 0a20a483968fa42d24996a1d74f4c8e91f86d49a
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 19:46:31 2024 +0300

    add zIndex

commit 7d4490110457f1ae189890268731fc1726a87825
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 19:27:32 2024 +0300

    test

commit b585d5ee0a6d36707ddc323547fdd1976451ee5a
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 18:28:49 2024 +0300

    /

commit cb7241c33d6cea3c1493f41b46fdd67f70455731
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 18:06:30 2024 +0300

    Update Appbar.tsx

commit 5fab766e17708a1be6a9f53029918cad122110bb
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 17:55:24 2024 +0300

    Update WebviewScreen.tsx

commit aec9b92d033156936b97a8455f6198fa20570906
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 17:15:16 2024 +0300

    xd

commit 1fa385b4bd85bea4da34ff9c23021fb92bc622d9
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 17:11:23 2024 +0300

    2

commit 589870de27cee2fcf29c894bd055a936bb0e7a5d
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 16:28:24 2024 +0300

    1

commit 343d2dd5b64404a3ea54d8e0366d8c856c9ea429
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 16:11:08 2024 +0300

    test

commit f2dbf34a7962b04cee46fdb37a9d0ea83ca541fe
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Thu Mar 7 14:36:48 2024 +0300

    test

commit 04c04d97a9dd87815c36145b3e0bd63e1bb3d118
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Wed Mar 6 22:37:45 2024 +0300

    test zoom

commit 6b80559e053a0d1a31f4c4762570b894bb5f70e5
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Wed Mar 6 22:33:55 2024 +0300

    fix

commit 287ce94efc18036ff9cca3054e9ca182137ffe8c
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Wed Mar 6 19:00:01 2024 +0300

    test

commit 67bdddc4d10920b559122f3d7df0225cea65497b
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Wed Mar 6 11:10:22 2024 +0300

    Test newArch

commit 762353788716341474832815f332ad6ff9ebe5be
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Tue Mar 5 20:49:29 2024 +0300

    Update Appbar.tsx

commit 2a876cb942847fb4c67085a6736b2d9e6e9a3012
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Tue Mar 5 20:42:28 2024 +0300

    modal

commit ba4cadec3f87d565fe045e3c977f604444a06571
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Tue Mar 5 16:50:38 2024 +0300

    1

commit 6c14a18b7083fc3292f796408e2defdad2728b10
Author: Rider21 <58046032+Rider21@users.noreply.github.com>
Date:   Tue Mar 5 14:54:27 2024 +0300

    test

* fix

* fix

* Update pluginManager.ts

* test

* fix

* fix lint

* -marquee

* fix defaultCover

* test

* Update src/plugins/helpers/storage.ts

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* test

* Typescript fixes

* private pluginID

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
2024-05-28 15:50:24 +07:00
nyagami 1ac6d116a2 Fix: Reader footer position & system bar (#1076)
* replace native Navigation bar by expo-navigation-bar

* fix fullscreen logic

* fix: navigation button color

* fix: navigation color when theme changed

* fix: status bar color on full screen mode

* 40 is too large

* no 40 is acceptable :)

* refactor setBarColor

* refactor: rename setStatusBarColor

* no padding for landscape mode
2024-05-28 15:13:46 +07:00
K1ngfish3r 6864d07dd9 Remove html entity replace function when sanitizing text (#1049)
* add spacing between <>

* remove check entirely

* fix ol tag
2024-05-28 11:21:36 +05:30
Lukas Richter 671e4f4bfd Fix: updateNovelTotalPages query (#1069) 2024-05-16 23:29:06 +07:00
nyagami e2d22a5b05 enhance TTS (#1054)
* fix: read by paragraph and empty string

* add voice picker modal

* add tts voice picker for preview

* fix: tts for bionic reading

* stop speak when get out of reader

* search voice

* temporary fix: original font

* fix: start & resume

* fix: close context menu on copy
2024-05-01 00:29:53 +07:00
Rajarshee Chatterjee 3cb2eb6b11 Make Reader Appbar and Footer Animation Smoother 2024-04-30 21:09:03 +05:30
Rajarshee Chatterjee 4597a95e9c Prevent Crash on Promise Rejection 2024-04-30 20:19:28 +05:30
Rajarshee Chatterjee cf4d97e7f9 Use Promise.allSettled and Update Plugin Repository Regex 2024-04-30 20:13:57 +05:30
Rajarshee Chatterjee 09f7319f2f feat: Repositories (#1051)
* feat: Repositories

* feat: Repositories

* Cleanup

* Cleanup

* Cleanup
2024-04-28 18:38:52 +05:30
Rajarshee Chatterjee c77dc52f29 Tweak UX (#1052)
* Tweak UX

* Prevent Crash in Migrate Screen

* Cleanup
2024-04-28 18:38:30 +05:30
Rajarshee Chatterjee 3cd5b40c25 Show Chapter Titles By Default 2024-04-28 09:37:49 +05:30
Rajarshee Chatterjee 42570d29f3 Move Update Icon Outside of Swipe Actions 2024-04-28 09:29:42 +05:30
nyagami 7e7a8866ca Fix: update novel pages (#1048)
* refreshPage button

* no latestChapter

* no hasUpdate in novel page
2024-04-27 09:44:54 +07:00
K1ngfish3r ed5e599edb add table style (#1020) 2024-04-26 00:16:46 +07:00
Patrick Loser addcadeba2 Feature: dynamic novel appbar (#998)
* update depd

* dynamic novel appbar

* fixed merge problems

* changed color from background to surface2

* added gitignore
2024-04-26 00:15:06 +07:00
Palloxin 484c2b7e33 Fix arabic alignment and language list order (#1024)
* Fix arabic alignment and language list order

- Left-to-Right for arabic language with u200e
- Fix alpabethic order

* Cleanup

---------

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2024-04-13 10:59:23 +05:30
nyagami b6fea9111c Fix: insert/update novel/chapter logic (#1019)
* fix: always create novelDir

* fix: download new chapters

* update novel totalPages if refreshNovelMetadata is off

* fix: updateChapter logic

* fix: insertId could be -1
2024-03-30 23:49:48 +07:00
470 changed files with 75333 additions and 36960 deletions
+9 -5
View File
@@ -1,18 +1,22 @@
module.exports = {
root: true,
extends: '@react-native',
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
overrides: [
{
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
rules: {
'@typescript-eslint/no-shadow': ['warn'],
'no-shadow': 'off',
'no-undef': 'off',
'react-native/no-inline-styles': 'warn',
'no-console': 'warn',
'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',
},
},
],
+3 -3
View File
@@ -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.0.3"
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.0.3](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+2 -2
View File
@@ -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.0.3](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

+66 -29
View File
@@ -3,55 +3,92 @@ on:
push:
branches:
- master
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-android:
name: Build App
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Clone Repository (Latest)
uses: actions/checkout@v3
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install Dependencies
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm Store Directory
shell: bash
run: |
npm install
- name: Create .env
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm Cache
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: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
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
- 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: Cache Gradle Wrapper
uses: actions/cache@v3
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache Gradle Dependencies
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-caches-
- name: Make Gradlew Executable
run: cd android && chmod +x ./gradlew
- name: Build Android Release
run: |
cd android && ./gradlew assembleRelease --no-daemon
- name: Set Env
- 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: Upload Artifact
uses: actions/upload-artifact@v1
- name: Build Android Release
env:
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
COMMIT_ID: ${{ env.COMMIT_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
- name: Upload Release Artifact
env:
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
COMMIT_ID: ${{ env.COMMIT_ID }}
uses: actions/upload-artifact@v4
with:
name: LNReader-r${{ env.COMMIT_COUNT }}
path: android/app/build/outputs/apk/release/app-release.apk
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
+33 -4
View File
@@ -7,13 +7,42 @@ on:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: ESLint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: Install modules
run: npm install
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- 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
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: Run ESLint
run: npm run lint
run: pnpm run lint
+45
View File
@@ -0,0 +1,45 @@
# Temporarily Disabled
name: Type Check
on:
workflow_dispatch:
# push:
# branches:
# - master
# pull_request:
# branches:
# - master
jobs:
build:
name: Type Checking
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- 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
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: Check Types
run: pnpm run type-check
+19 -2
View File
@@ -38,7 +38,7 @@ DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
**/.xcode.env.local
# Android/IntelliJ
#
@@ -48,6 +48,10 @@ build/
local.properties
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore
.kotlin/
# node.js
#
@@ -66,6 +70,7 @@ buck-out/
# CocoaPods
/ios/Pods/
Gemfile.lock
# Expo
.expo/
@@ -74,4 +79,16 @@ web-build/
# @end expo-cli
.vscode/
src/sources/en/generators/
src/sources/en/generators/
*.lockb
*.lock
!pnpm-lock.yaml
.tool-versions
flake.lock
# pnpm
.pnpm-store
.cursor/
.agents/
.claude/
+3 -1
View File
@@ -1 +1,3 @@
**/node_modules
**/node_modules
android/**/js/van.js
android/**/js/text-vibe.js
+1
View File
@@ -7,4 +7,5 @@ module.exports = {
trailingComma: 'all',
arrowParens: 'avoid',
quoteProps: 'preserve',
endOfLine: 'auto', // stop prettier from getting mad on windows
};
+83 -18
View File
@@ -1,45 +1,104 @@
import 'react-native-gesture-handler';
import 'react-native-url-polyfill/auto';
import { enableFreeze } from 'react-native-screens';
enableFreeze(true);
import React from 'react';
import { StatusBar } from 'react-native';
import React, { useEffect } from 'react';
import { StatusBar, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import LottieSplashScreen from 'react-native-lottie-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 BackgroundService from 'react-native-background-actions';
import { createTables } from '@database/db';
import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';
import { deserializePlugins } from '@plugins/pluginManager';
import AppErrorBoundary, {
ErrorFallback,
} from '@components/AppErrorBoundary/AppErrorBoundary';
import { useDatabaseInitialization } from '@hooks';
import Main from './src/navigators/Main';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { MMKVStorage } from '@utils/mmkv/mmkv';
import { BACKGROUND_ACTION } from '@services/constants';
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldPlaySound: false,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true,
};
},
});
createTables();
deserializePlugins().then(() => LottieSplashScreen.hide());
if (!BackgroundService.isRunning()) {
MMKVStorage.delete(BACKGROUND_ACTION);
}
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);
}
},
);
return () => {
subscription.remove();
};
}, []);
if (dbError) {
return <ErrorFallback error={dbError} resetError={retryInitialization} />;
}
if (!isDbReady) {
return null;
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<GestureHandlerRootView style={styles.flex}>
<AppErrorBoundary>
<SafeAreaProvider>
<PaperProvider>
@@ -55,3 +114,9 @@ const App = () => {
};
export default App;
const styles = StyleSheet.create({
flex: {
flex: 1,
},
});
+108
View File
@@ -0,0 +1,108 @@
# React Native Development Nix Environment Setup
This document outlines how to set up your React Native development environment using Nix flakes for reproducibility and isolation.
## Disclaimer
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.
## Prerequisites
- **Nix installed**: [https://nixos.org/download.html](https://nixos.org/download.html)
- **Git**: Required for cloning the repository.
- **KVM (for emulator, if enabled)**: Ensure KVM is enabled and your user is in the `kvm` group for optimal emulator performance:
```bash
sudo usermod -aG kvm $USER && newgrp kvm # (or log out/in)
kvm-ok # Verify KVM acceleration
```
- **GPU Drivers (for emulator, if enabled)**: For hardware-accelerated emulator (AMD/Intel), ensure host Vulkan drivers are working:
```bash
vulkaninfo | grep -i "device name" # Should show your GPU
```
If not, install `vulkan-radeon` (AMD) or `vulkan-intel` (Intel) on your host system and reboot.
## Getting Started
1. **Clone the Project:**
```bash
git clone <your-fork-url>
cd <your-fork-name>
```
2. **Enter the Development Shell:**
If you have `pnpm` installed, use the defined scripts:
- **For Physical Device Only (default):**
```bash
pnpm run nix:shell
```
- **For Emulator Development (explicitly enable):**
```bash
pnpm run nix:emulator
```
If you do not have `pnpm` installed, execute the commands manually:
- **For Physical Device Only (default):**
```bash
nix develop --extra-experimental-features nix-command --extra-experimental-features flakes
```
- **For Emulator Development (explicitly enable):**
```
nix develop .#emulator --extra-experimental-features nix-command --extra-experimental-features flakes
```
The first time, this will download and set up all required tools, which can take considerable time. Subsequent entries will be much faster. You will also see a message about "Setting up writable Android SDK..." which only happens once.
## Development Workflow
Once inside the `nix develop` shell, all tools are available.
### 1. Start Metro Bundler
Open a new terminal or run in the background:
```bash
pnpm run dev:start
```
### 2. Physical Device Development
1. **Connect Device**: Enable USB debugging on your Android device and connect it via USB.
2. **Verify Device**:
```bash
adb devices
```
3. **Mirror Screen (Optional)**:
```bash
scrcpy # Mirrors screen, provides control
scrcpy -s <device_serial> # If multiple devices connected
```
4. **Run App**:
```bash
npx run android
```
### 3. Android Emulator Development (if enabled)
1. **Launch Emulator**:
```bash
emulator -avd ReactNative_API35 -gpu host -no-metrics -no-audio
```
- Use `-gpu swiftshader_indirect` if hardware acceleration (`-gpu host`) fails.
2. **Verify Emulator**:
```bash
adb devices
```
3. **Run App**:
Launches the the emulater automatically, if it is not already running.
```bash
npx run android
```
+15 -53
View File
@@ -2,14 +2,19 @@
Contributions are welcome and are greatly appreciated!
## 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)
## Setting up your environment
After forking to your own github org or account, do the following steps to get started:
```bash
# prerequisites
node --version <= 16.13.1 (for version management, get nvm [recommended])
java sdk --version <= 11 (for version management, get jenv [optional])
node --version >= 20 (for version management, get nvm [recommended])
java sdk --version >= 17 (for version management, get jenv [optional])
android sdk (https://developer.android.com/studio)
# clone your fork to your local machine
@@ -18,11 +23,14 @@ git clone https://github.com/<your-account-name>/lnreader.git
# step into local repo
cd lnreader
# install pnpm (if not already installed)
npm install -g pnpm
# install dependencies
npm install
pnpm install
# build the apk (the built apk will be found in ~/lnreader/android/app/build/outputs/apk/release/)
npm run buildRelease
pnpm run build:release:android
```
### Developing on Android
@@ -38,15 +46,12 @@ IDE
adb devices
# run metro for development
npm start
pnpm run dev:start
# then to view on your android device (new terminal)
npm run android
pnpm run dev:android
```
To view any changes to the app with new code, save your code and press "r" on the metro terminal to
reload it. The app on the android device/emulator will reload shortly.
### Style & Linting
This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
@@ -55,48 +60,5 @@ It is recommended that you install an eslint plugin for your editor of choice wh
codebase, however you can always check to see if the source code is compliant by running:
```bash
npm run lint
```
# Database
```mermaid
erDiagram
Category ||--|{ NovelCategory : contains
Category{
INTEGER id PK
TEXT name
INTEGER sort
}
NovelCategory{
INTEGER id PK
INTEGER novelId FK
INTEGER categoryId FK "default: whose sort = 1"
}
Novel ||--|{ NovelCategory : contains
Novel{
INTEGER id PK
TEXT url
TEXT pluginId
TEXT name
TEXT cover
TEXT summary
TEXT author
TEXT artist
TEXT status
TEXT genres
INTEGER inLibrary "default: 0"
}
Novel ||--o| Chapter : has
Chapter{
INTEGER id PK
INTEGER novelId FK
text url
TEXT name
TEXT releaseTime
INTEGER bookmark "default: 0"
INTEGER unread "default: 1"
TEXT readTime
INTEGER isDownloaded "default: 0"
TEXT updatedTime
TEXT chapterNumber
}
pnpm run lint
```
+16
View File
@@ -0,0 +1,16 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
# Ruby 3.4.0 has removed some libraries from the standard library.
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
+50 -29
View File
@@ -1,47 +1,68 @@
<p align="center">
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
<p>
<h1 align="center">
LNReader
</h1>
<p align="center">
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
<a href="https://lnreader.app">
<img src="./.github/readme-images/icon_new.png" align="center" width="128" />
</a>
</p>
<h1 align="center">LNReader</h1>
<p align="center">
LNReader is a free and open source light novel reader for Android, inspired by Tachiyomi.
</p>
<div align="center">
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LNReader/lnreader/build.yml?branch=main">
</a>
<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=7289DA&label=Discord&color=17cf48">
<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/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
</a>
<a href="https://github.com/LNReader/lnreader-preview/releases/latest">
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader-preview?color=red&label=preview">
</a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader"><img src="https://badges.crowdin.net/lnreader/localized.svg"></a>
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
<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>
<br>
<br>
<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">
</a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader">
<img src="https://badges.crowdin.net/lnreader/localized.svg">
</a>
</div>
<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>
<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>
</p>
<p align="center">
Get the app from our <a href="https://github.com/lnreader/lnreader/releases">releases page</a>.
</p>
<p align="center">
<em>Android 7.0 or higher.</em>
</p>
<h2 align="center">Screenshots</h2>
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" />
</p>
## Download
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
## Sources
## Plugins
LNReader does not have any affiliation with the content providers available.
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
Plugin requests should be created at [lnreader-plugins](https://github.com/lnreader/lnreader-plugins).
## Translation
Help translate LNReader into your language on [Crowdin](https://crowdin.com/project/lnreader).
## Building & Contributing
@@ -49,4 +70,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md)
## License
[MIT](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
[MIT](https://github.com/lnreader/lnreader/blob/main/LICENSE)
-55
View File
@@ -1,55 +0,0 @@
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
lib_deps = []
create_aar_targets(glob(["libs/*.aar"]))
create_jar_targets(glob(["libs/*.jar"]))
android_library(
name = "all-libs",
exported_deps = lib_deps,
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
)
android_build_config(
name = "build_config",
package = "com.rajarsheechatterjee.LNReader",
)
android_resource(
name = "res",
package = "com.rajarsheechatterjee.LNReader",
res = "src/main/res",
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
)
+30 -41
View File
@@ -1,5 +1,6 @@
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.
@@ -7,14 +8,14 @@ apply plugin: "com.facebook.react"
*/
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")
// 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
@@ -48,6 +49,9 @@ react {
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
@@ -59,28 +63,28 @@ def enableProguardInReleaseBuilds = false
* The preferred build flavor of JavaScriptCore (JSC).
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
* `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 = 'org.webkit:android-jsc:+'
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
ext {
versionMajor = 2
versionMinor = 0
versionPatch = 0
versionPatch = 3
}
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.rajarsheechatterjee.LNReader"
defaultConfig {
applicationId 'com.rajarsheechatterjee.LNReader'
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
@@ -98,6 +102,8 @@ android {
buildTypes {
debug {
signingConfig signingConfigs.debug
applicationIdSuffix 'debug'
versionNameSuffix '-debug'
}
release {
// Caution! In production, you need to generate your own keystore file.
@@ -107,41 +113,24 @@ android {
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 'com.facebook.soloader:soloader:0.10.5+'
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
if (isGifEnabled || isWebpEnabled) {
implementation 'com.facebook.fresco:fresco:2.0.0'
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.0.0'
}
if(isGifEnabled){
// For animated gif support
implementation 'com.facebook.fresco:animated-gif:2.0.0'
}
if(isWebpEnabled){
// For webp support
implementation 'com.facebook.fresco:webpsupport:2.0.0'
if(isWebpAnimatedEnabled){
// Animated webp support
implementation 'com.facebook.fresco:animated-webp:2.0.0'
}
}
implementation 'androidx.core:core-ktx:1.15.0'
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
}
+3 -6
View File
@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
</application>
</manifest>
tools:ignore="GoogleAppIndexingWarning"/>
</manifest>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">LNReader debug</string>
</resources>
+5 -6
View File
@@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.rajarsheechatterjee.LNReader">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET" />
@@ -29,7 +29,8 @@
android:roundIcon="@mipmap/ic_launcher"
android:allowBackup="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
android:supportsRtl="true">
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
@@ -41,7 +42,6 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
@@ -59,13 +59,12 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lnreader" />
<data android:scheme="com.rajarsheechatterjee.LNReader" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" android:foregroundServiceType="shortService" />
</application>
</manifest>
</manifest>
+123 -359
View File
@@ -1,419 +1,183 @@
html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: var(--StatusBar-currentHeight);
word-wrap: break-word;
scroll-behavior: smooth;
overflow-x: hidden;
word-wrap: break-word;
}
::selection {
color: var(--theme-onSecondary);
background-color: var(--theme-secondary);
color: var(--theme-onSecondary);
background-color: var(--theme-secondary);
}
::-moz-selection {
color: var(--theme-onSecondary);
background-color: var(--theme-secondary);
color: var(--theme-onSecondary);
background-color: var(--theme-secondary);
}
body {
padding-left: var(--readerSettings-padding);
padding-right: var(--readerSettings-padding);
padding-bottom: 40px;
margin-left: 0;
margin-right: 0;
padding-bottom: 40px;
padding-top: var(--StatusBar-currentHeight);
font-size: var(--readerSettings-textSize);
background-color: var(--readerSettings-theme);
color: var(--readerSettings-textColor);
text-align: var(--readerSettings-textAlign);
line-height: var(--readerSettings-lineHeight);
font-family: var(--readerSettings-fontFamily);
font-size: var(--readerSettings-textSize);
background-color: var(--readerSettings-theme);
color: var(--readerSettings-textColor);
text-align: var(--readerSettings-textAlign);
line-height: var(--readerSettings-lineHeight);
font-family: var(--readerSettings-fontFamily);
}
chapter {
display: block;
#LNReader-chapter {
padding-left: var(--readerSettings-padding);
padding-right: var(--readerSettings-padding);
}
hr {
margin-top: 20px;
margin-bottom: 20px;
margin-top: 20px;
margin-bottom: 20px;
}
a {
color: var(--theme-primary);
color: var(--theme-primary);
}
sup {
line-height: 0.1em;
}
img {
display: block;
width: auto;
height: auto;
max-width: 100%;
display: block;
width: auto;
height: auto;
max-width: 100%;
}
table {
background-color: var(--theme-onPrimary);
border-collapse: collapse;
color: var(--theme-primary);
}
th {
font-weight: bold;
}
td {
padding: 10px;
text-align: center;
}
table,
th,
td {
border: 1px solid var(--theme-outline);
}
.hidden {
visibility: hidden;
visibility: hidden;
}
.nextButton,
.infoText {
width: 100%;
border-radius: 50px;
border-width: 1;
color: var(--theme-onPrimary);
background-color: var(--theme-primary);
font-family: var(--readerSettings-fontFamily);
font-size: 16px;
border-width: 0;
user-select: none;
.next-button,
.info-text {
margin-left: var(--readerSettings-padding);
margin-right: var(--readerSettings-padding);
border-radius: 50px;
border-width: 1;
color: var(--theme-onPrimary);
background-color: var(--theme-primary);
font-family: var(--readerSettings-fontFamily);
font-size: 16px;
border-width: 0;
user-select: none;
}
.nextButton {
min-height: 40px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 0 16px;
.next-button {
min-height: 40px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 0 16px;
width: calc(100% - var(--readerSettings-padding) * 2);
}
.infoText {
background-color: transparent;
text-align: center;
border: none;
margin: 0px;
color: inherit;
padding-top: 16px;
padding-bottom: 16px;
}
.chapterCtn {
min-height: var(--chapterCtn-height);
margin-bottom: auto;
.info-text {
background-color: transparent;
text-align: center;
border: none;
margin: 0px;
color: inherit;
padding-top: 16px;
padding-bottom: 16px;
}
.d-none {
display: none;
}
#ToolWrapper {
position: fixed;
top: 50%;
transform: translateY(-50%);
right: 5vw;
}
#ToolWrapper.horizontal {
top: unset;
right: unset;
left: 50%;
transform: translateX(-50%);
bottom: 120px;
display: flex;
flex-direction: row-reverse;
align-items: center;
}
@media only screen and (min-width: 500px) {
#ToolWrapper.horizontal {
bottom: 80px;
}
}
#ToolWrapper.hidden {
opacity: 0;
right: 0;
}
#ToolWrapper.hidden.horizontal {
opacity: 0;
bottom: 0;
right: unset;
}
#TTS-Controller {
display: block;
}
#ToolWrapper.horizontal > #TTS-Controller {
margin-left: 4px;
}
#TTS-Controller button{
background: var(--theme-surface-0-9);
border: 0;
}
#TTS-Controller svg {
fill: var(--theme-onSurface);
}
#ToolWrapper > #ScrollBar {
margin-top: 8px;
width: 2.4rem;
height: 45vh;
min-height: 200px;
border-radius: 1.2rem;
background-color: var(--theme-surface-0-9);
touch-action: none;
font-size: 16px;
user-select: none;
}
#ScrollBar > .scrollbar-item {
display: flex;
justify-content: center;
align-items: center;
color: var(--theme-onSurface);
}
#ScrollBar > .scrollbar-item.scrollbar-text {
height: 12.5%;
}
#ScrollBar > .scrollbar-item#scrollbar-slider {
height: 75%;
align-items: unset;
}
#ScrollBar #scrollbar-track {
width: 0.1rem;
height: 100%;
background-color: var(--theme-outline);
}
#ScrollBar #scrollbar-progress {
width: 100%;
height: 0;
background-color: var(--theme-primary);
}
#ScrollBar #scrollbar-thumb-wrapper {
position: relative;
top: 100%;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-0.95rem, -1rem);
}
#ScrollBar #scrollbar-thumb {
top: 100%;
height: 1rem;
width: 1rem;
border-radius: 100%;
background-color: var(--theme-primary);
}
#ToolWrapper.horizontal > #ScrollBar{
display: flex;
width: 80vw;
height: 2.4rem;
min-height: unset;
align-items: center;
margin-top: unset;
}
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item {
display: flex;
justify-content: center;
color: var(--theme-onSurface);
}
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item.scrollbar-text {
width: 12.5%;
}
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item#scrollbar-slider {
width: 75%;
align-items: center;
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-track {
height: 0.1rem;
width: 100%;
background-color: var(--theme-outline);
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-progress {
height: 100%;
width: 0;
background-color: var(--theme-primary);
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb-wrapper {
position: relative;
left: 100%;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-0.95rem, -1rem);
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb {
top: 100%;
height: 1rem;
width: 1rem;
border-radius: 100%;
background-color: var(--theme-primary);
display: none;
}
#reader-footer-wrapper {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 9999999;
}
#reader-footer {
display: flex;
flex: 3;
padding: 0.2rem 2rem;
background-color: var(--readerSettings-theme);
color: var(--readerSettings-textColor);
font-size: 1rem;
text-align: center;
user-select: none;
display: flex;
flex: 3;
padding: 0.2rem 2rem;
background-color: var(--readerSettings-theme);
color: var(--readerSettings-textColor);
font-size: 1rem;
text-align: center;
user-select: none;
}
.reader-footer-item {
flex: 1;
flex: 1;
}
.reader-footer-item:first-child {
text-align: left;
text-align: left;
}
.reader-footer-item:last-child {
text-align: right;
text-align: right;
}
tts.highlight {
color: var(--theme-onSecondary);
background-color: var(--theme-secondary);
.highlight {
color: var(--readerSettings-textColor) !important;
background-color: color-mix(
in srgb,
var(--readerSettings-textColor) 20%,
var(--readerSettings-theme)
);
}
.contextMenu {
position: absolute;
height: 0;
overflow: hidden;
background: var(--theme-surface-0-9);
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
position: fixed;
top: var(--top);
left: var(--left);
-webkit-animation: menuAnimation 0.4s 0s both;
animation: menuAnimation 0.4s 0s both;
transform-origin: left;
list-style: none;
margin: 4px;
padding: 0;
display: flex;
flex-direction: column;
z-index: 999999999;
}
.contextMenu-item {
padding: 4px,
}
.contextMenu-button {
color: var(--theme-onSurface);
background: 0;
border: 0;
white-space: nowrap;
width: 100%;
border-radius: 4px;
padding: 6px 24px 6px 7px;
text-align: left;
display: flex;
align-items: center;
font-size: 14px;
width: 100%;
-webkit-animation: menuItemAnimation 0.2s 0s both;
animation: menuItemAnimation 0.2s 0s both;
font-family: var(--readerSettings-fontFamily);
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: var(--theme-rippleColor);
}
.contextMenu-button svg {
fill: var(--theme-onSurface);
}
.contextMenu-button span {
margin-left: 4px;
}
@-webkit-keyframes menuAnimation {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
height: var(--height);
opacity: 1;
border-radius: 8px;
transform: scale(1);
}
}
@keyframes menuAnimation {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
height: var(--height);
opacity: 1;
border-radius: 8px;
transform: scale(1);
}
}
@-webkit-keyframes menuItemAnimation {
0% {
opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes menuItemAnimation {
0% {
opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
.highlight * {
color: var(--readerSettings-textColor) !important;
background-color: transparent;
}
#Image-Modal {
position: fixed;
left: 0;
top: 0;
user-select: none;
position: fixed;
left: 0;
top: 0;
user-select: none;
}
#Image-Modal.show {
display: flex;
height: 100%;
width: 100%;
background-color: var(--theme-surface-0-9);
align-items: center;
justify-content: center;
overflow: scroll;
display: flex;
height: 100%;
width: 100%;
background-color: var(--theme-surface-0-9);
align-items: center;
justify-content: center;
overflow: scroll;
}
#Image img {
display: block;
width: 100%;
}
display: block;
width: 100%;
}
@@ -0,0 +1,29 @@
body.page-reader {
overflow: hidden;
padding-bottom: unset;
}
body.page-reader > #LNReader-chapter {
height: calc(90vh);
column-width: calc(100vw - var(--readerSettings-padding) * 2);
column-gap: calc(var(--readerSettings-padding) * 2);
transition: 200ms;
}
.transition-chapter {
height: 100vh;
width: 100vw;
max-width: 100vw;
position: fixed;
left: 100vw;
top: 0;
box-sizing: border-box;
padding: var(--readerSettings-padding);
padding-top: 30vh;
text-align: center;
font-size: 2rem;
z-index: 888888;
background-color: var(--readerSettings-theme);
}
@@ -0,0 +1,151 @@
#ToolWrapper {
position: fixed;
top: 50%;
transform: translateY(-50%);
right: 5vw;
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: 9999999;
}
#ToolWrapper.horizontal {
top: unset;
right: unset;
left: 50%;
transform: translateX(-50%);
bottom: 120px;
display: flex;
flex-direction: row-reverse;
align-items: center;
}
@media only screen and (min-width: 500px) {
#ToolWrapper.horizontal {
bottom: 80px;
}
}
#ToolWrapper.hidden {
opacity: 0;
right: 0;
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
}
#ToolWrapper.hidden.horizontal {
opacity: 0;
bottom: 80px;
right: unset;
}
#ToolWrapper > #ScrollBar {
margin-top: 8px;
width: 2.4rem;
height: 50vh;
min-height: 200px;
border-radius: 1.2rem;
background-color: var(--theme-surface-0-9);
touch-action: none;
font-size: 14px;
user-select: none;
}
#ScrollBar > .scrollbar-item {
display: flex;
justify-content: center;
align-items: center;
color: var(--theme-onSurface);
}
#ScrollBar > .scrollbar-item.scrollbar-text {
height: 12.5%;
}
#ScrollBar > .scrollbar-item#scrollbar-slider {
height: 75%;
align-items: unset;
}
#ScrollBar #scrollbar-track {
width: 0.1rem;
height: 100%;
background-color: var(--theme-outline);
}
#ScrollBar #scrollbar-progress {
width: 100%;
height: 0;
background-color: var(--theme-primary);
}
#ScrollBar #scrollbar-thumb-wrapper {
position: relative;
top: 100%;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-0.95rem, -1rem);
}
#ScrollBar #scrollbar-thumb {
top: 100%;
height: 1rem;
width: 1rem;
border-radius: 100%;
background-color: var(--theme-primary);
}
#ToolWrapper.horizontal > #ScrollBar {
display: flex;
width: 95.5vw;
height: 2.4rem;
min-height: unset;
align-items: center;
margin-top: unset;
}
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item {
display: flex;
justify-content: center;
color: var(--theme-onSurface);
}
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item.scrollbar-text {
width: 12.5%;
}
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item#scrollbar-slider {
width: 75%;
align-items: center;
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-track {
height: 0.1rem;
width: 100%;
background-color: var(--theme-outline);
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-progress {
height: 100%;
width: 0;
background-color: var(--theme-primary);
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb-wrapper {
position: relative;
left: 100%;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-0.95rem, -1rem);
}
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb {
top: 100%;
height: 1rem;
width: 1rem;
border-radius: 100%;
background-color: var(--theme-primary);
}
+48
View File
@@ -0,0 +1,48 @@
#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;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+772
View File
@@ -0,0 +1,772 @@
/* 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.elementsRead = 0;
this.totalElements = 0;
this.allReadableElements = []; // Store all readable elements at start
this.textQueue = []; // Flat list of normalized text for native fallback
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
// FIXED: Added proper boundary checks to prevent stack overflow
this.findNextTextNode = (depth = 0) => {
// Prevent deep recursion
if (depth > 500) {
console.warn('TTS: findNextTextNode max depth reached');
return false;
}
if (this.currentElement.isSameNode(reader.chapterElement) && this.started) {
return false;
} else {
this.started = true;
}
// Safety check: ensure currentElement is valid
if (!this.currentElement || !this.currentElement.nodeName) {
return false;
}
// 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;
return this.findNextTextNode(depth + 1);
} else if (
this.currentElement.parentElement &&
!this.currentElement.parentElement.isSameNode(document.body) &&
!this.currentElement.parentElement.isSameNode(document.documentElement)
) {
this.currentElement = this.currentElement.parentElement;
return this.findNextTextNode(depth + 1);
} else {
return false;
}
} 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;
return this.findNextTextNode(depth + 1);
} else if (this.currentElement.nextElementSibling) {
this.prevElement = this.currentElement;
this.currentElement = this.currentElement.nextElementSibling;
return this.findNextTextNode(depth + 1);
} else if (
this.currentElement.parentElement &&
!this.currentElement.parentElement.isSameNode(document.body) &&
!this.currentElement.parentElement.isSameNode(document.documentElement)
) {
this.prevElement = this.currentElement;
this.currentElement = this.currentElement.parentElement;
return this.findNextTextNode(depth + 1);
} else {
return false;
}
}
};
this.next = () => {
try {
this.currentElement?.classList?.remove('highlight');
// Use array-based approach instead of DOM traversal (no recursion!)
while (this.elementsRead < this.totalElements) {
const nextElement = this.allReadableElements[this.elementsRead];
if (!nextElement) break;
const text = this.normalizeText(nextElement.innerText);
if (text) {
// Found valid text - speak it
this.currentElement = nextElement;
this.reading = true;
this.elementsRead++;
this.speak();
return;
} else {
// Empty text, skip to next in array (no recursion!)
this.elementsRead++;
}
}
// Reached the end (elementsRead >= totalElements or no more valid elements)
this.reading = false;
const autoPageAdvance =
reader.readerSettings.val.tts?.autoPageAdvance === true;
const hasNextChapter = !!reader.nextChapter;
if (autoPageAdvance && hasNextChapter) {
reader.post({ type: 'next', autoStartTTS: true });
} else {
this.stop();
const controller = document.getElementById('TTS-Controller');
if (controller?.firstElementChild) {
controller.firstElementChild.innerHTML = volumnIcon;
}
}
} catch (e) {
this.stop();
alert('TTS Error: ' + e.message);
}
};
this.start = element => {
this.stop();
const startElement = element ?? reader.chapterElement;
this.currentElement = startElement;
// Get all readable elements from the chapter
this.allReadableElements = this.getAllReadableElements(
reader.chapterElement,
);
this.totalElements = this.allReadableElements.length;
this.textQueue = this.allReadableElements
.map(el => this.normalizeText(el.innerText))
.filter(text => !!text);
reader.post({
type: 'tts-queue',
data: {
queue: this.textQueue,
startIndex: this.elementsRead,
},
});
// If starting from a specific element, count how many are before it
if (element && element !== reader.chapterElement) {
const startIndex = this.allReadableElements.indexOf(element);
this.elementsRead = startIndex >= 0 ? startIndex : 0;
} else {
this.elementsRead = 0;
}
this.next();
};
// Get all readable elements in order
this.getAllReadableElements = element => {
const elements = [];
const traverse = el => {
if (!el) return;
if (this.readable(el)) {
elements.push(el);
}
for (let i = 0; i < el.children.length; i++) {
traverse(el.children[i]);
}
};
traverse(element);
return elements;
};
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;
this.elementsRead = 0;
this.totalElements = 0;
this.allReadableElements = [];
this.textQueue = [];
reader.post({ type: 'tts-state', data: { isReading: false } });
// Ensure icon updates to stopped state
const controller = document.getElementById('TTS-Controller');
if (controller?.firstElementChild) {
controller.firstElementChild.innerHTML = volumnIcon;
}
};
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
);
};
// UPDATED: Scroll to top or center based on settings with padding for notch/camera
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) {
// Check scrollToTop setting (default to true for better reading experience)
const scrollToTop = reader.readerSettings.val.tts?.scrollToTop !== false;
if (scrollToTop) {
// Scroll to top with padding for notch/camera (80px from top)
const elementTop =
element.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementTop - 80; // 80px padding for notch/camera
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
});
} else {
// Center scroll (original behavior)
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,
index: this.elementsRead - 1,
});
reader.post({ type: 'tts-state', data: { isReading: true } });
} else {
this.next();
}
};
})();
// Watch for TTSEnable changes and stop TTS when disabled
van.derive(() => {
if (!reader.generalSettings.val.TTSEnable && window.tts) {
if (tts.reading || tts.started) {
tts.stop();
}
}
});
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
@@ -0,0 +1,6 @@
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>';
+62
View File
@@ -0,0 +1,62 @@
import { ChapterInfo, NovelInfo } from '@database/types';
import {
ChapterGeneralSettings,
ChapterReaderSettings,
} from '@hooks/persisted/useSettings';
import { State } from './van';
export interface Reader {
// element
chapterElement: HTMLElement;
viewport: HTMLMetaElement;
selection: Selection;
// state
hidden: State<boolean>;
generalSettings: State<ChapterGeneralSettings>;
readerSettings: State<ChapterReaderSettings>;
batteryLevel: State<number>;
novel: NovelInfo;
chapter: ChapterInfo;
nextChapter?: ChapterInfo;
autoSaveInterval: number;
rawHTML: string;
strings: {
finished: string;
nextChapter: string;
noNextChapter: string;
};
//layout props
paddingTop: number;
layoutHeight: number;
layoutWidth: number;
chapterHeight: number;
chapterWidth: number;
post: (obj: Record<string, any>) => void;
refresh: () => void;
}
interface PageReader {
page: State<number>;
totalPages: State<number>;
movePage: (page: number) => void;
}
interface TTS {
started: boolean;
reading: boolean;
start: (element?: HTMLElement) => void;
resume: () => void;
stop: () => void;
pause: () => void;
readable: (element?: HTMLElement) => void;
}
declare global {
const reader: Reader;
const tts: TTS;
const pageReader: PageReader;
}
+327 -634
View File
@@ -1,658 +1,351 @@
/**
* https://fonts.google.com/icons
* Filled
* other settings are default
*/
const copyIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Z"/></svg>';
const volumeIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320Z"/></svg>';
const selectAllIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M280-280v-400h400v400H280Zm80-80h240v-240H360v240ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm80-160h-80q0-33 23.5-56.5T200-840v80Zm80 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80q0 33-23.5 56.5T760-120Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80q33 0 56.5 23.5T840-760h-80Z"/></svg>';
const translateIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>';
const resumeIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M240-240v-480h80v480h-80Zm160 0 400-240-400-240v480Z"/></svg>';
const pauseIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/></svg>';
const stopIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M320-320h320v-320H320v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>';
const selectVolumeIcon =
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131Zm-80-29L280-360H120v-240h160l200-200v640Zm80-160v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM40-680v-240h240v80H120v160H40ZM680-40v-80h160v-160h80v240H680Z"/></svg>';
/**
* @type {import("./type").Reader}
*/
class Reader {
constructor() {
this.selection = window.getSelection();
this.viewport = document.querySelector('meta[name=viewport]');
this.footerWrapper = document.getElementById('reader-footer-wrapper');
this.percentage = document.getElementById('reader-percentage');
this.battery = document.getElementById('reader-battery');
this.time = document.getElementById('reader-time');
this.paddingTop = parseInt(
getComputedStyle(document.querySelector('html')).getPropertyValue(
'padding-top',
),
);
this.chapter = document.querySelector('chapter');
this.rawHTML = this.chapter.innerHTML;
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
this.layoutHeight = window.innerHeight;
this.pluginId = this.chapter.getAttribute('data-plugin-id');
this.novelId = this.chapter.getAttribute('data-novel-id');
this.chapterId = this.chapter.getAttribute('data-chapter-id');
this.saveProgressInterval = setInterval(
() =>
this.post({
type: 'save',
data: parseInt(
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
),
}),
autoSaveInterval,
);
this.time.innerText = new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
this.timeInterval = setInterval(() => {
this.time.innerText = new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}, 60000);
this.updateBatteryLevel(batteryLevel);
this.updateGeneralSettings(initSettings);
}
const { div, p, img, button } = van.tags;
refresh = () => {
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
};
post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
updateReaderSettings = settings => {
document.documentElement.style.setProperty(
'--readerSettings-theme',
settings.theme,
);
document.documentElement.style.setProperty(
'--readerSettings-padding',
settings.padding + '%',
);
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,
);
new FontFace(
settings.fontFamily,
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
)
.load()
.then(function (loadedFont) {
document.fonts.add(loadedFont);
});
};
updateGeneralSettings = settings => {
this.showScrollPercentage = settings.showScrollPercentage;
this.showBatteryAndTime = settings.showBatteryAndTime;
this.verticalSeekbar = settings.verticalSeekbar;
this.bionicReading = settings.bionicReading;
if (this.bionicReading) {
this.chapter.innerHTML = textVide.textVide(this.rawHTML);
} else {
this.chapter.innerHTML = this.rawHTML;
}
if (settings.swipeGestures) {
swipeHandler.enable();
} else {
swipeHandler.disable();
}
if (!this.showScrollPercentage && !this.showBatteryAndTime) {
this.footerWrapper.classList.add('d-none');
} else {
this.footerWrapper.classList.remove('d-none');
if (this.showScrollPercentage) {
this.percentage.classList.remove('hidden');
} else {
this.percentage.classList.add('hidden');
}
if (this.showBatteryAndTime) {
this.battery.classList.remove('hidden');
this.time.classList.remove('hidden');
} else {
this.battery.classList.add('hidden');
this.time.classList.add('hidden');
}
}
if (!this.verticalSeekbar) {
toolWrapper.$.classList.add('horizontal');
} else {
toolWrapper.$.classList.remove('horizontal');
}
if (scrollHandler) {
scrollHandler.refresh();
scrollHandler.update();
}
};
updateBatteryLevel = level => {
this.battery.innerText = Math.ceil(level * 100) + '%';
};
}
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),
);
};
class ToolWrapper {
constructor() {
this.$ = document.getElementById('ToolWrapper');
this.$.onclick = e => {
e.stopPropagation();
};
this.tools = [];
}
hide = () => {
this.$.classList.add('hidden');
this.visible = false;
};
show = () => {
this.$.classList.remove('hidden');
this.visible = true;
for (const tool of this.tools) {
tool.onShow?.();
}
};
}
class ScrollHandler {
constructor(reader, toolWrapper) {
this.reader = reader;
this.toolWrapper = toolWrapper;
this.$ = document.getElementById('ScrollBar');
this.$.innerHTML = `
<div class="scrollbar-item scrollbar-text" id="scrollbar-percentage">
0
</div>
<div class="scrollbar-item" id="scrollbar-slider">
<div id="scrollbar-track">
<div id="scrollbar-progress">
<div id="scrollbar-thumb-wrapper">
<div id="scrollbar-thumb"></div>
</div>
</div>
</div>
</div>
<div class="scrollbar-item scrollbar-text">
100
</div>
`;
this.percentage = this.$.querySelector('#scrollbar-percentage');
this.progress = this.$.querySelector('#scrollbar-progress');
this.thumb = this.$.querySelector('#scrollbar-thumb-wrapper');
this.slider = this.$.querySelector('#scrollbar-slider');
this.sliderHeight = this.slider.clientHeight;
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
this.lock = false;
window.onscroll = () => !this.lock && this.update();
this.thumb.ontouchstart = () => (this.lock = true);
this.thumb.ontouchend = () => (this.lock = false);
this.thumb.ontouchmove = e => {
const ratio =
((this.reader.verticalSeekbar
? e.changedTouches[0].clientY
: e.changedTouches[0].clientX) -
this.sliderOffsetY) /
this.sliderHeight;
this.update(ratio < 0 ? 0 : ratio);
};
}
update = ratio => {
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 + this.reader.layoutHeight) / this.reader.chapterHeight;
ratio = (window.scrollY + reader.layoutHeight) / reader.chapterHeight;
}
if (ratio > 1) {
ratio = 1;
}
const percentage = parseInt(ratio * 100);
if (this.toolWrapper.visible) {
if (this.reader.verticalSeekbar) {
this.progress.style.height = percentage + '%';
this.progress.style.width = '100%';
} else {
this.progress.style.height = '100%';
this.progress.style.width = percentage + '%';
}
this.percentage.innerText = percentage;
if (reader.generalSettings.val.pageReader) {
pageReader.movePage(
parseInt(pageReader.totalPages.val * Math.min(0.99, ratio)),
);
return;
}
if (this.lock) {
percentage.val = parseInt(ratio * 100);
if (lock) {
window.scrollTo({
top: this.reader.chapterHeight * ratio - this.reader.layoutHeight,
top: reader.chapterHeight * ratio - reader.layoutHeight,
behavior: 'instant',
});
}
if (this.reader.percentage) {
this.reader.percentage.innerText = percentage + '%';
}
};
refresh = () => {
if (this.reader.verticalSeekbar) {
this.sliderHeight = this.slider.clientHeight;
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
} else {
this.sliderHeight = this.slider.clientWidth;
this.sliderOffsetY = this.slider.getBoundingClientRect().left;
}
};
onShow = () => {
this.reader.refresh();
this.refresh();
this.update();
};
}
class SwipeHandler {
constructor() {
this.initialX = null;
this.initialY = null;
}
touchStartHandler = e => {
this.initialX = e.changedTouches[0].screenX;
this.initialY = e.changedTouches[0].screenY;
};
/**
* @param {TouchEvent} e
*/
touchEndHandler = e => {
let diffX = e.changedTouches[0].screenX - this.initialX;
let diffY = e.changedTouches[0].screenY - this.initialY;
if (
e.target.id?.startsWith('scrollbar') ||
e.target.id === 'Image-Modal-img'
) {
return;
}
if (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' });
}
}
};
enable = () => {
document.addEventListener('touchstart', this.touchStartHandler);
document.addEventListener('touchend', this.touchEndHandler);
};
disable = () => {
document.removeEventListener('touchstart', this.touchStartHandler);
document.removeEventListener('touchend', this.touchEndHandler);
};
}
/**
* @type {import('./type').TextToSpeech}
*/
class TextToSpeech {
constructor(reader) {
this.reader = reader;
(this.leaf = this.reader.chapter), (this.TTSWrapper = null); // swap these 2 elements
this.TTSEle = null;
this.reading = false;
this.$ = document.getElementById('TTS-Controller');
this.$.classList.add('d-none');
this.$.onclick = () => {
if (this.reading) {
this.pause();
} else {
this.resume();
}
};
}
start = () => {
this.$.innerHTML = `<button>${pauseIcon}</button>`;
this.$.classList.remove('d-none');
this.stop();
this.next();
};
startHere = () => {
this.$.innerHTML = `<button>${pauseIcon}</button>`;
this.$.classList.remove('d-none');
this.stop();
if (this.reader.selection.type === 'Range') {
if (this.leaf && this.TTSWrapper) {
this.TTSWrapper.replaceWith(this.leaf);
}
this.leaf = this.reader.selection.anchorNode;
this.makeLeafSpeakable();
this.speak();
} else {
this.next();
}
};
resume = () => {
this.$.innerHTML = `<button>${pauseIcon}</button>`;
this.next();
};
findLeaf() {
while (this.leaf.firstChild) {
this.leaf = this.leaf.firstChild;
}
}
findNextLeaf() {
if (this.reader.chapter.isSameNode(this.leaf)) {
this.findLeaf();
} else if (this.leaf.nextSibling) {
this.leaf = this.leaf.nextSibling;
this.findLeaf();
} else {
this.leaf = this.leaf.parentNode;
if (this.reader.chapter.isSameNode(this.leaf)) {
return;
}
this.findNextLeaf();
}
}
readable() {
return (
this.leaf.nodeName === '#text' && /[^\s\n,.?!;":“”]/.test(this.leaf.data)
);
}
findNextTextNode() {
if (this.leaf && this.TTSWrapper) {
this.TTSWrapper.replaceWith(this.leaf);
this.TTSWrapper = null;
}
do {
this.findNextLeaf();
} while (!this.readable() && !this.reader.chapter.isSameNode(this.leaf));
if (this.reader.chapter.isSameNode(this.leaf)) {
return;
}
this.makeLeafSpeakable();
}
makeLeafSpeakable() {
this.TTSWrapper = document.createElement('tts-wrapper');
this.TTSWrapper.innerHTML = this.leaf.data.replace(
/([^\n,.?!;":“”]+)/g,
matched => {
if (matched.trim().length) {
return `<tts>${matched}</tts>`;
}
return matched;
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',
},
);
this.TTSEle = this.TTSWrapper.firstElementChild;
this.leaf.replaceWith(this.TTSWrapper);
}
() =>
reader.generalSettings.val.pageReader ? pageReader.totalPages.val : 100,
),
);
};
next = () => {
try {
if (this.TTSEle) {
this.TTSEle.classList.remove('highlight');
}
if (this.reading) {
if (this.TTSEle && this.TTSEle.nextElementSibling) {
this.TTSEle = this.TTSEle.nextElementSibling;
} else {
this.findNextTextNode();
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 = '';
}
} else {
if (!this.TTSEle) {
this.findNextTextNode();
}
}
this.speak();
} catch (e) {
alert(e);
}
};
},
},
img({
id: 'Image-Modal-img',
src: src,
alt: () => (src.val ? `Cant not render image from ${src.val}` : ''),
}),
);
};
stop = () => {
this.$.classList.add('d-none');
this.reading = false;
if (this.leaf && this.TTSWrapper) {
this.TTSWrapper.replaceWith(this.leaf);
this.TTSWrapper = null;
this.TTSEle = null;
this.leaf = this.reader.chapter;
}
this.reader.post({ type: 'stop-speak' });
};
pause = () => {
this.$.innerHTML = `<button>${resumeIcon}</button>`;
this.reading = false;
this.reader.post({ type: 'stop-speak' });
};
started = () => this.TTSWrapper !== null;
speak = () => {
this.reading = true;
if (this.reader.chapter.isSameNode(this.leaf)) {
return;
}
this.TTSEle.classList.add('highlight');
this.reader.post({ type: 'speak', data: this.TTSEle?.innerText.trim() });
};
}
/**
* @type {import("./type").ContextMenu}
*/
class ContextMenu {
/**
* @param {import("./type").Reader} reader
*/
constructor(reader) {
this.reader = reader;
this.items = {
START_READING: this.renderItem({
name: 'Start Reading',
icon: volumeIcon,
action: () => {
tts.start();
this.closeMenu();
},
}),
START_HERE: this.renderItem({
name: 'Start Here',
icon: selectVolumeIcon,
action: () => {
tts.startHere();
this.closeMenu();
},
}),
COPY: this.renderItem({
name: 'Copy',
icon: copyIcon,
action: () => {
this.reader.post({
type: 'copy',
data: this.reader.selection.toString(),
});
},
}),
SELECT_ALL: this.renderItem({
name: 'Select All',
icon: selectAllIcon,
action: () => {
const range = document.createRange();
range.selectNodeContents(this.reader.chapter);
this.reader.selection.removeAllRanges();
this.reader.selection.addRange(range);
},
}),
};
this.commonItems = [this.items.COPY, this.items.SELECT_ALL];
this.contextMenu = document.createElement('ul');
this.contextMenu.classList.add('contextMenu');
this.isOpened = false;
}
/**
* @param {{name: string, icon: string, action: () => void}} data
* @returns
*/
renderItem(data) {
const item = document.createElement('li');
const button = document.createElement('button');
button.innerHTML = `${data.icon} <span>${data.name}</span>`;
button.classList.add('contextMenu-button');
item.classList.add('contextMenu-item');
item.appendChild(button);
button.addEventListener('click', event => {
event.stopPropagation();
data.action();
});
return item;
}
renderMenu(items) {
this.contextMenu.innerHTML = '';
items.concat(this.commonItems).forEach((item, index) => {
item.firstChild.setAttribute('style', `animation-delay: ${index * 0.1}s`);
this.contextMenu.appendChild(item);
});
}
closeMenu() {
if (this.isOpened) {
this.isOpened = false;
this.contextMenu.remove();
}
}
init() {
this.reader.chapter.addEventListener('click', () => {
this.closeMenu(this.contextMenu);
});
document.addEventListener('contextmenu', e => {
e.preventDefault();
if (e.target instanceof HTMLImageElement) {
if (imageModal.showing) {
imageModal.hide();
} else {
imageModal.show(e.target);
}
return;
}
this.isOpened = true;
if (this.reader.selection.type === 'Range') {
this.renderMenu([this.items.START_HERE, this.items.START_READING]);
} else {
this.renderMenu([this.items.START_READING]);
}
const { clientX, clientY } = e;
e.target.appendChild(this.contextMenu);
const positionY =
clientY + this.contextMenu.scrollHeight >= window.innerHeight
? window.innerHeight - this.contextMenu.scrollHeight - 20
: clientY;
const positionX =
clientX + this.contextMenu.scrollWidth >= window.innerWidth
? window.innerWidth - this.contextMenu.scrollWidth - 20
: clientX;
this.contextMenu.setAttribute(
'style',
`--width: ${this.contextMenu.scrollWidth}px;
--height: ${this.contextMenu.scrollHeight}px;
--top: ${positionY}px;
--left: ${positionX}px;`,
);
});
}
}
/**
* @type {import("./type").ImageModal}
*/
class ImageModal {
/**
* @param {import('./type').Reader} reader
*/
constructor(reader) {
this.reader = reader;
this.$ = document.getElementById('Image-Modal');
this.img = this.$.querySelector('img');
this.showing = false;
this.$.onclick = e => {
if (e.target.id !== 'Image-Modal-img') {
e.stopPropagation();
this.hide();
}
};
this.$.ontouchmove = e => {
e.stopImmediatePropagation();
};
}
/**
*
* @param {HTMLImageElement} image
*/
show(image) {
this.img.src = image.src;
this.img.alt = `Can not render image from ${image.src}`;
this.reader.viewport.setAttribute(
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',
);
this.$.classList.add('show');
this.showing = true;
}
hide() {
this.$.classList.remove('show');
this.showing = false;
this.img.src = '';
this.img.alt = '';
this.reader.viewport.setAttribute(
};
const hideImage = () => {
imgSrc.val = '';
reader.viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0',
);
}
}
};
try {
var swipeHandler = new SwipeHandler();
var toolWrapper = new ToolWrapper();
var reader = new Reader();
var scrollHandler = new ScrollHandler(reader, toolWrapper);
var tts = new TextToSpeech(reader);
toolWrapper.tools = [scrollHandler, tts];
imageModal = new ImageModal(reader);
const contextMenu = new ContextMenu(reader);
contextMenu.init();
} catch (e) {
alert(e);
}
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'}`,
style: () =>
reader.generalSettings.val.TTSEnable
? 'pointer-events: auto;'
: 'pointer-events: none; display: none !important; opacity: 0; transition: none;',
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`;
// Check if TTS is still enabled before starting
if (hoverElement && reader.generalSettings.val.TTSEnable) {
tts.start(hoverElement);
controllerElement.firstElementChild.innerHTML = pauseIcon;
}
}
clientX = null;
clientY = null;
},
onclick: e => {
e.stopPropagation();
// Don't allow interaction if TTS is disabled
if (!reader.generalSettings.val.TTSEnable) {
return;
}
if (tts.reading) {
tts.pause();
controllerElement.firstElementChild.innerHTML = resumeIcon;
} else if (tts.started) {
tts.resume();
controllerElement.firstElementChild.innerHTML = pauseIcon;
} else {
tts.start();
controllerElement.firstElementChild.innerHTML = pauseIcon;
}
},
},
button({ innerHTML: volumnIcon }),
);
};
const ReaderUI = () => {
return div(
ToolWrapper(),
TTSController(),
ModalWrapper(),
Footer(),
ChapterEnding(),
);
};
van.add(document.getElementById('reader-ui'), ReaderUI());
@@ -0,0 +1,91 @@
/* eslint-disable eslint-comments/no-unlimited-disable */
/* eslint-disable */
'use strict';
(() => {
function f(i, o, r, s = !1) {
if (!(o in i) || typeof r != 'function') return !1;
let t = i[o];
return (
(i[o] = function (...e) {
return (
s || t.apply(this, e), (t = t.bind(this)), r.apply(this, [t, ...e])
);
}),
!0
);
}
if (typeof window == 'undefined') throw 'Not in a browser.';
var p = 'onscrollend' in window;
if (!p) {
let s = function (e, u, l) {
if (u !== 'scrollend') {
e.apply(this, [u, l]);
return;
}
let c = this,
n = i.get(c);
if (!n) {
let d;
(n = {
scrollFn: () => {
clearTimeout(d),
(d = setTimeout(() => {
l();
}, 100));
},
handlers: [l],
}),
e.apply(c, ['scroll', n.scrollFn, !1]),
i.set(c, n);
}
},
t = function (e, u, l) {
if (u !== 'scrollend') {
e.apply(this, [u, l]);
return;
}
let c = this,
n = i.get(c);
if (n) {
if (
(typeof n.scrollFn == 'function' &&
e.apply(this, ['scroll', n.scrollFn]),
typeof l == 'undefined')
)
n.handlers = [];
else {
let d = n.handlers.indexOf(l);
d > -1 && n.handlers.splice(d, 1);
}
n.handlers.length === 0 && i.delete(c);
}
};
(a = s), (w = t);
let i = new WeakMap(),
o = null,
r = null;
f(window, 'addEventListener', s, !0),
f(window, 'removeEventListener', t, !0),
Object.defineProperty(window, 'onscrollend', {
set: e => {
(typeof e != 'function' || o) &&
t.call(window, window.removeEventListener, 'scrollend', o),
(o = e),
e && s.call(window, window.addEventListener, 'scrollend', e);
},
get: () => o,
}),
f(document, 'addEventListener', s, !0),
f(document, 'removeEventListener', t, !0),
Object.defineProperty(document, 'onscrollend', {
set: e => {
(typeof e != 'function' || r) &&
t.call(document, document.removeEventListener, 'scrollend', r),
(r = e),
e && s.call(document, document.addEventListener, 'scrollend', e);
},
get: () => r,
});
}
var a, w;
})();
+1 -111
View File
@@ -1,111 +1 @@
var textVide = (function (L) {
'use strict';
const f = t => t == null || t === '',
h = (t, n) =>
Object.keys(t).reduce((e, s) => (n(e[s]) && delete e[s], e), t),
I = (t, n) => ({ ...n, ...h(t, f) }),
_ = ['<b>', '</b>'],
y = t =>
I(t, {
sep: _,
fixationPoint: 1,
ignoreHtmlTag: !0,
ignoreHtmlEntity: !0,
}),
u = [
[0, 4, 12, 17, 24, 29, 35, 42, 48],
[1, 2, 7, 10, 13, 14, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49],
[
1, 2, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37,
39, 41, 43, 45, 47, 49,
],
[
0, 2, 4, 5, 6, 8, 9, 11, 14, 15, 17, 18, 20, 0, 21, 23, 24, 26, 27, 29,
30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48,
],
[
0, 2, 3, 5, 6, 7, 8, 10, 11, 12, 14, 15, 17, 19, 20, 21, 23, 24, 25, 26,
28, 29, 30, 32, 33, 34, 35, 37, 38, 39, 41, 42, 43, 44, 46, 47, 48,
],
],
A = (t, n) => {
const { length: i } = t,
e = u[n - 1] ?? u[0],
s = e.findIndex(c => i <= c);
let o = i - s;
return s === -1 && (o = i - e.length), Math.max(o, 0);
},
R = (t, n) =>
typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,
m = t =>
Array.from(t).map(n => {
const i = n.index,
[e] = n,
{ length: s } = e;
return [i, i + s - 1];
}),
H = /(<!--[\s\S]*?-->)|(<[^>]*>)/g,
N = t => {
const n = t.matchAll(H),
e = m(n).reverse();
return s => {
const o = s.index,
c = e.find(([r]) => o > r);
if (!c) {
return !1;
}
const [, a] = c;
return o < a;
};
},
O = /(&[\w#]+;)/g,
M = t => {
const n = t.matchAll(O),
e = m(n).reverse();
return s => {
const o = s.index,
c = e.find(([r]) => o > r);
if (!c) {
return !1;
}
const [, a] = c;
return o < a;
};
},
x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,
F = (t, n = {}) => {
if (!(t != null && t.length)) {
return '';
}
const {
fixationPoint: i,
sep: e,
ignoreHtmlTag: s,
ignoreHtmlEntity: o,
} = y(n),
c = Array.from(t.matchAll(x));
let a = '',
l = 0,
r;
s && (r = N(t));
let d;
o && (d = M(t));
for (const g of c) {
if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {
continue;
}
const [D] = g,
T = g.index,
E = T + A(D, i),
U = t.slice(l, T);
(a += U), T !== E && (a += R(t.slice(T, E), e)), (l = E);
}
const G = t.slice(l);
return a + G;
};
return (
(L.textVide = F),
Object.defineProperty(L, Symbol.toStringTag, { value: 'Module' }),
L
);
})({});
var textVide = function(L){'use strict'; const f = t=>t == null || t === '',h = (t,n)=>Object.keys(t).reduce((e,s)=>(n(e[s]) && delete e[s],e),t),I = (t,n)=>({...n,...h(t,f)}),_ = ['<b>','</b>'],y = t=>I(t,{sep:_,fixationPoint:1,ignoreHtmlTag:!0,ignoreHtmlEntity:!0}),u = [[0,4,12,17,24,29,35,42,48],[1,2,7,10,13,14,19,22,25,28,31,34,37,40,43,46,49],[1,2,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49],[0,2,4,5,6,8,9,11,14,15,17,18,20,0,21,23,24,26,27,29,30,32,33,35,36,38,39,41,42,44,45,47,48],[0,2,3,5,6,7,8,10,11,12,14,15,17,19,20,21,23,24,25,26,28,29,30,32,33,34,35,37,38,39,41,42,43,44,46,47,48]],A = (t,n)=>{const {length:i} = t,e = u[n - 1] ?? u[0],s = e.findIndex(c=>i <= c); let o = i - s; return s === -1 && (o = i - e.length),Math.max(o,0);},R = (t,n)=>typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,m = t=>Array.from(t).map(n=>{const i = n.index,[e] = n,{length:s} = e; return [i,i + s - 1];}),H = /<!--[^]*?-->|(<[^>]+>)/g,N = t=>{const n = t.matchAll(H),e = m(n).reverse(); return s=>{const o = s.index,c = e.find(([r])=>o > r); if (!c) {return !1;} const [,a] = c; return o < a;};},O = /&[\w#]+;/g,M = t=>{const n = t.matchAll(O),e = m(n).reverse(); return s=>{const o = s.index,c = e.find(([r])=>o > r); if (!c) {return !1;} const [,a] = c; return o < a;};},x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,F = (t,n = {})=>{if (!(t != null && t.length)) {return '';} const {fixationPoint:i,sep:e,ignoreHtmlTag:s,ignoreHtmlEntity:o} = y(n),c = Array.from(t.matchAll(x)); let a = '',l = 0,r; s && (r = N(t)); let d; o && (d = M(t)); for (const g of c){if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {continue;} const [D] = g,T = g.index,E = T + A(D,i),U = t.slice(l,T); a += U,T !== E && (a += R(t.slice(T,E),e)),l = E;} const G = t.slice(l); return a + G;}; return L.textVide = F,Object.defineProperty(L,Symbol.toStringTag,{value:'Module'}),L;}({});
-70
View File
@@ -1,70 +0,0 @@
import {
ChapterGeneralSettings,
ChapterReaderSettings,
} from '@hooks/persisted/useSettings';
import 'typescript';
import 'typescript/lib/lib.dom';
interface Reader {
selection: Selection;
viewport: HTMLMetaElement;
footerWrapper: HTMLDivElement;
percentage: HTMLDivElement;
battery: HTMLDivElement;
time: HTMLDivElement;
paddingTop: number;
chapter: HTMLElement;
chapterHeight: number;
layoutHeight: number;
pluginId: string;
novelId: string;
chapterId: string;
saveProgressInterval: NodeJS.Timeout;
timeInterval: NodeJS.Timeout;
refresh: () => void;
post: (message: { type: string; data?: any }) => void;
updateReaderSettings: (settings: ChapterReaderSettings) => void;
updateGeneralSettings: (settings: ChapterGeneralSettings) => void;
updateBatteryLevel: (level: number) => void;
}
interface TextToSpeech {
reader: Reader;
leaf: HTMLElement;
TTSWrapper: HTMLElement;
TTSEle: HTMLElement;
reading: boolean;
start: () => void;
startHere: () => void;
resume: () => void;
pause: () => void;
stop: () => void;
started: () => boolean;
}
export enum ContextMenuItem {
START_READING = 'START_READING',
START_HERE = 'START_HERE',
COPY = 'COPY',
SELECT_ALL = 'SELECT_ALL',
}
interface ContextMenu {
reader: Reader;
contextMenu: HTMLUListElement;
items: Record<ContextMenuItem, HTMLLIElement>;
renderItem: (data: {
name: string;
icon: string;
action: () => void;
}) => HTMLLIElement;
renderMenu: (items: HTMLLIElement[]) => void;
closeMenu: (menu: HTMLUListElement) => void;
init: () => void;
}
interface ImageModal {
reader: Reader;
showing: boolean;
show: (image: HTMLImageElement) => void;
hide: () => void;
}
+70
View File
@@ -0,0 +1,70 @@
import 'typescript';
import 'typescript/lib/lib.dom';
export interface State<T> {
val: T;
readonly oldVal: T;
readonly rawVal: T;
}
// Defining readonly view of State<T> for covariance.
// Basically we want StateView<string> to implement StateView<string | number>
export type StateView<T> = Readonly<State<T>>;
export type Val<T> = State<T> | T;
export type Primitive = string | number | boolean | bigint;
export type PropValue = Primitive | ((e: any) => void) | null;
export type PropValueOrDerived =
| PropValue
| StateView<PropValue>
| (() => PropValue);
export type Props = Record<string, PropValueOrDerived> & {
class?: PropValueOrDerived;
};
export type PropsWithKnownKeys<ElementType> = Partial<{
[K in keyof ElementType]: PropValueOrDerived;
}>;
export type ValidChildDomValue = Primitive | Node | null | undefined;
export type BindingFunc =
| ((dom?: Node) => ValidChildDomValue)
| ((dom?: Element) => Element);
export type ChildDom =
| ValidChildDomValue
| StateView<Primitive | null | undefined>
| BindingFunc
| readonly ChildDom[];
export type TagFunc<Result> = (
first?: (Props & PropsWithKnownKeys<Result>) | ChildDom,
...rest: readonly ChildDom[]
) => Result;
type Tags = Readonly<Record<string, TagFunc<Element>>> & {
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>;
};
declare function state<T>(): State<T>;
declare function state<T>(initVal: T): State<T>;
export interface Van {
readonly state: typeof state;
readonly derive: <T>(f: () => T) => State<T>;
readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element;
readonly tags: Tags &
((namespaceURI: string) => Readonly<Record<string, TagFunc<Element>>>);
readonly hydrate: <T extends Node>(
dom: T,
f: (dom: T) => T | null | undefined,
) => T;
}
declare global {
const van: Van;
}
+1
View File
@@ -0,0 +1 @@
{let e,t,r,o,n,l,s,f,i,h,w,a,d,u,_,c,S,g,y,b,m,v,j,x,O; s = Object.getPrototypeOf,i = {},h = s(f = {isConnected:1}),w = s(s),a = (e,t,r,o)=>(e ?? (setTimeout(r,o),new Set())).add(t),d = (e,t,o)=>{let n = r; r = t; try {return e(o);} catch (e){return console.error(e),o;} finally {r = n;}},u = e=>e.filter(e=>e.t?.isConnected),_ = e=>n = a(n,e,()=>{for (let e of n){e.o = u(e.o),e.l = u(e.l);}n = l;},1e3),c = {get val(){return r?.i?.add(this),this.rawVal;},get oldVal(){return r?.i?.add(this),this.h;},set val(o){r?.u?.add(this),o !== this.rawVal && (this.rawVal = o,this.o.length + this.l.length ? (t?.add(this),e = a(e,this,x)) : this.h = o);}},S = e=>({__proto__:c,rawVal:e,h:e,o:[],l:[]}),g = (e,t)=>{let r = {i:new Set(),u:new Set()},n = {f:e},l = o; o = []; let s = d(e,r,t); s = (s ?? document).nodeType ? s : new Text(s); for (let e of r.i){r.u.has(e) || (_(e),e.o.push(n));} for (let e of o){e.t = s;} return o = l,n.t = s;},y = (e,t = S(),r)=>{let n = {i:new Set(),u:new Set()},l = {f:e,s:t}; l.t = r ?? o?.push(l) ?? f,t.val = d(e,n,t.rawVal); for (let e of n.i){n.u.has(e) || (_(e),e.l.push(l));} return t;},b = (e,...t)=>{for (let r of t.flat(1 / 0)){let t = s(r ?? 0),o = t === c ? g(()=>r.val) : t === w ? g(r) : r; o != l && e.append(o);} return e;},m = (e,t,...r)=>{let [o,...n] = s(r[0] ?? 0) === h ? r : [{},...r],f = e ? document.createElementNS(e,t) : document.createElement(t); for (let [e,r] of Object.entries(o)){let o = t=>t ? Object.getOwnPropertyDescriptor(t,e) ?? o(s(t)) : l,n = t + ',' + e,h = i[n] ?? (i[n] = o(s(f))?.set ?? 0),a = e.startsWith('on') ? (t,r)=>{let o = e.slice(2); f.removeEventListener(o,r),f.addEventListener(o,t);} : h ? h.bind(f) : f.setAttribute.bind(f,e),d = s(r ?? 0); e.startsWith('on') || d === w && (r = y(r),d = c),d === c ? g(()=>(a(r.val,r.h),f)) : a(r);} return b(f,n);},v = e=>({get:(t,r)=>m.bind(l,e,r)}),j = (e,t)=>t ? t !== e && e.replaceWith(t) : e.remove(),x = ()=>{let r = 0,o = [...e].filter(e=>e.rawVal !== e.h); do {t = new Set(); for (let e of new Set(o.flatMap(e=>e.l = u(e.l)))){y(e.f,e.s,e.t),e.t = l;}} while (++r < 100 && (o = [...t]).length); let n = [...e].filter(e=>e.rawVal !== e.h); e = l; for (let e of new Set(n.flatMap(e=>e.o = u(e.o)))){j(e.t,g(e.f,e.t)),e.t = l;} for (let e of n){e.h = e.rawVal;}},O = {tags:new Proxy(e=>new Proxy(m,v(e)),v()),hydrate:(e,t)=>j(e,g(t,e)),add:b,state:S,derive:y},window.van = O;}
@@ -1,163 +0,0 @@
package com.rajarsheechatterjee.EpubUtil;
import android.util.Xml;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.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 org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Objects;
public class EpubUtil extends ReactContextBaseJavaModule {
EpubUtil(ReactApplicationContext context){super(context);}
@NonNull
@Override
public String getName() {
return "EpubUtil";
}
private XmlPullParser initParse(File file) throws XmlPullParserException, IOException {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(new FileInputStream(file), null);
parser.nextTag();
return parser;
}
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
String result = "";
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
}
return result;
}
private String cleanUrl(String url){
if(url != null){
return url.replaceFirst("#[^.]+?$", "");
}
return null;
}
@ReactMethod
public void parseNovelAndChapters(String epubDirPath, Promise promise) {
try {
File containerFile = new File(epubDirPath, "META-INF/container.xml");
File contentMetaFile = new File(epubDirPath, getContentMetaFilePath(containerFile));
String contentDir = contentMetaFile.getParent();
ReadableMap novel = getNovelMetadata(contentMetaFile, contentDir);
promise.resolve(novel);
} catch (XmlPullParserException | IOException e) {
promise.reject(e.getCause());
}
}
private String getContentMetaFilePath(File file) throws XmlPullParserException, IOException {
XmlPullParser parser = initParse(file);
while (parser.next() != XmlPullParser.END_TAG){
@Nullable String tag = parser.getName();
if(tag != null && tag.equals("rootfile")) {
return parser.getAttributeValue(null, "full-path");
}
}
return "OEBPS/content.opf"; // default
}
private ReadableMap getNovelMetadata(File file, String contentDir) throws XmlPullParserException, IOException {
WritableMap novel = new WritableNativeMap();
WritableArray chapters = new WritableNativeArray();
XmlPullParser parser = initParse(file);
HashMap<String, String> refMap = new HashMap<>();
HashMap<String, String> tocMap = new HashMap<>();
File tocFile = new File(contentDir, "toc.ncx");
if(tocFile.exists()){
XmlPullParser tocParser = initParse(tocFile);
String label = null;
while (tocParser.next() != XmlPullParser.END_DOCUMENT){
String tag = tocParser.getName();
if(tag != null){
if(tag.equals("text")){
label = readText(tocParser);
}else if(tag.equals("content")){
String href = cleanUrl(tocParser.getAttributeValue(null, "src"));
if(href != null){
tocMap.put(href, label);
}
}
}
}
}
String cover = null;
while (parser.next() != XmlPullParser.END_DOCUMENT){
@Nullable String tag = parser.getName();
if(tag != null){
switch (tag) {
case "item": {
String id = parser.getAttributeValue(null, "id");
String href = parser.getAttributeValue(null, "href");
if (id != null) {
refMap.put(id, href);
}
break;
}
case "itemref": {
String idRef = parser.getAttributeValue(null, "idref");
String href = refMap.get(idRef);
if (href != null) {
WritableMap chapter = new WritableNativeMap();
chapter.putString("path", contentDir + "/" + href);
String name = tocMap.get(href);
chapter.putString("name", name == null ? href : name);
chapters.pushMap(chapter);
}
break;
}
case "title":
novel.putString("name", readText(parser));
break;
case "creator":
novel.putString("author", readText(parser));
break;
case "contributor":
novel.putString("artist", readText(parser));
break;
case "description":
novel.putString("summary", readText(parser));
break;
case "meta":
String metaName = parser.getAttributeValue(null, "name");
if(metaName != null && metaName.equals("cover")){
cover = parser.getAttributeValue(null, "content");
}
break;
}
parser.next();
}
}
if(cover != null){
String coverPath = contentDir + "/" + refMap.get(cover);
novel.putString("cover", coverPath);
}
novel.putArray("chapters", chapters);
return novel;
}
}
@@ -1,33 +0,0 @@
package com.rajarsheechatterjee.EpubUtil;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.rajarsheechatterjee.ZipArchive.ZipArchive;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class EpubUtilPackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
List<NativeModule> modules = new ArrayList<>();
try {
modules.add(new EpubUtil(reactApplicationContext));
} catch (Exception e) {
throw new RuntimeException(e);
}
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
return Collections.emptyList();
}
}
@@ -1,87 +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.Window;
import android.view.WindowManager;
import expo.modules.ReactActivityDelegateWrapper;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
import org.devio.rn.splashscreen.SplashScreen;
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
Window w = getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
super.onCreate(null);
SplashScreen.show(this,R.style.SplashScreenTheme, R.id.lottie);
SplashScreen.setAnimationFinished(true);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (VolumeButtonListener.isActive()) {
int action = event.getAction();
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (action == KeyEvent.ACTION_DOWN) {
// volup
VolumeButtonListener.up();
if (!VolumeButtonListener.prevent)
return super.dispatchKeyEvent(event);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
// voldown
VolumeButtonListener.down();
if (!VolumeButtonListener.prevent)
return super.dispatchKeyEvent(event);
}
return true;
default:
return super.dispatchKeyEvent(event);
}
}
return super.dispatchKeyEvent(event);
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "main";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(
this,
getMainComponentName(),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled()
));
}
}
@@ -0,0 +1,74 @@
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,82 +0,0 @@
package com.rajarsheechatterjee.LNReader;
import android.app.Application;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import com.rajarsheechatterjee.EpubUtil.EpubUtilPackage;
import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage;
import com.rajarsheechatterjee.TextFile.TextFilePackage;
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
import com.rajarsheechatterjee.ZipArchive.ZipArchivePackage;
import expo.modules.ApplicationLifecycleDispatcher;
import expo.modules.ReactNativeHostWrapper;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new NavigationBarColorPackage());
packages.add(new VolumeButtonListenerPackage());
packages.add(new ZipArchivePackage());
packages.add(new TextFilePackage());
packages.add(new EpubUtilPackage());
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
});
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
ApplicationLifecycleDispatcher.onApplicationCreate(this);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
}
}
@@ -0,0 +1,52 @@
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)
}
}
@@ -0,0 +1,248 @@
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) {
try {
val fw = FileWriter(path)
fw.write(content)
fw.close()
} catch (e: IOException) {
throw Exception("Failed to write file '$path': ${e.message}")
}
}
override fun readFile(path: String): String {
val file = File(path)
if (!file.exists()) {
throw Exception("File not found: '$path'")
}
return file.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,
) {
try {
val inputStream = getInputStream(filepath)
try {
val outputStream = getOutputStream(destPath)
try {
val buffer = ByteArray(BUFFER_SIZE)
var length: Int
while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
}
} finally {
outputStream.close()
}
} finally {
inputStream.close()
}
if (onDone != null) {
onDone()
}
} catch (e: IOException) {
throw Exception("Failed to copy file from '$filepath' to '$destPath': ${e.message}")
}
}
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) {
response.use {
if (!it.isSuccessful || it.body == null) {
promise.reject(Exception("Failed to download: ${it.code}"))
return
}
try {
decompressStream(it.body!!.byteStream()).use { inputStream ->
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
}
}
@@ -0,0 +1,30 @@
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
)
)
}
}
@@ -0,0 +1,32 @@
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)
}
}
}
}
@@ -0,0 +1,30 @@
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
)
)
}
}
@@ -0,0 +1,137 @@
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()
}
}
@@ -0,0 +1,30 @@
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
)
)
}
}
@@ -1,158 +0,0 @@
package com.rajarsheechatterjee.NavigationBarColor;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.graphics.Color;
import android.os.Build;
import android.app.Activity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import java.util.HashMap;
import java.util.Map;
import com.facebook.react.uimanager.IllegalViewOperationException;
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
public class NavigationBarColorModule extends ReactContextBaseJavaModule {
public static final String REACT_CLASS = "NavigationBarColor";
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity";
private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
public NavigationBarColorModule(ReactApplicationContext context) {
super(context);
}
public void setNavigationBarTheme(Activity activity, Boolean light) {
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Window window = activity.getWindow();
int flags = window.getDecorView().getSystemUiVisibility();
if (light) {
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
} else {
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
window.getDecorView().setSystemUiVisibility(flags);
}
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("EXAMPLE_CONSTANT", "example");
return constants;
}
@ReactMethod
public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) {
final WritableMap map = Arguments.createMap();
if (getCurrentActivity() != null) {
try {
final Window window = getCurrentActivity().getWindow();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (color.equals("transparent") || color.equals("translucent")) {
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
if (color.equals("transparent")) {
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
} else {
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
setNavigationBarTheme(getCurrentActivity(), light);
map.putBoolean("success", true);
promise.resolve(map);
return;
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
if (animated) {
Integer colorFrom = window.getNavigationBarColor();
Integer colorTo = Color.parseColor(color);
//window.setNavigationBarColor(colorTo);
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
window.setNavigationBarColor((Integer) animator.getAnimatedValue());
}
});
colorAnimation.start();
} else {
window.setNavigationBarColor(Color.parseColor(color));
}
setNavigationBarTheme(getCurrentActivity(), light);
WritableMap map = Arguments.createMap();
map.putBoolean("success", true);
promise.resolve(map);
}
});
} catch (IllegalViewOperationException e) {
map.putBoolean("success", false);
promise.reject("error", e);
}
} else {
promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE));
}
}
@ReactMethod
public void hideNavigationBar(Promise promise) {
try {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (getCurrentActivity() != null) {
View decorView = getCurrentActivity().getWindow().getDecorView();
decorView.setSystemUiVisibility(UI_FLAG_HIDE_NAV_BAR);
}
}
});
} catch (IllegalViewOperationException e) {
WritableMap map = Arguments.createMap();
map.putBoolean("success", false);
promise.reject("error", e);
}
}
@ReactMethod
public void showNavigationBar(Promise promise) {
try {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (getCurrentActivity() != null) {
Window w = getCurrentActivity().getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
View decorView = w.getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
}
});
} catch (IllegalViewOperationException e) {
WritableMap map = Arguments.createMap();
map.putBoolean("success", false);
promise.reject("error", e);
}
}
}
@@ -1,25 +0,0 @@
package com.rajarsheechatterjee.NavigationBarColor;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class NavigationBarColorPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new NavigationBarColorModule(reactContext));
return modules;
}
}
@@ -1,53 +0,0 @@
package com.rajarsheechatterjee.TextFile;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
public class TextFile extends ReactContextBaseJavaModule {
TextFile(ReactApplicationContext context) {
super(context);
}
@NonNull
@Override
public String getName() {
return "TextFile";
}
@ReactMethod
public void writeFile(String path, String content, Promise promise) {
try {
FileWriter fw = new FileWriter(path);
fw.write(content);
fw.close();
promise.resolve(null);
} catch (Exception e) {
promise.reject(e);
}
}
@ReactMethod void readFile(String path, Promise promise) {
try {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new FileReader(path));
String line;
while ((line = br.readLine()) != null) sb.append(line).append('\n');
promise.resolve(sb.toString());
} catch (Exception e) {
promise.reject(e);
}
}
}
@@ -1,32 +0,0 @@
package com.rajarsheechatterjee.TextFile;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TextFilePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
List<NativeModule> modules = new ArrayList<>();
try {
modules.add(new TextFile(reactApplicationContext));
} catch (Exception e) {
throw new RuntimeException(e);
}
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
return Collections.emptyList();
}
}
@@ -1,93 +0,0 @@
package com.rajarsheechatterjee.VolumeButtonListener;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class VolumeButtonListener extends ReactContextBaseJavaModule {
static ReactApplicationContext appContext;
public VolumeButtonListener(ReactApplicationContext context) {
super(context);
appContext = context;
}
private static boolean active = false;
public static boolean isActive() {
return active;
}
public static boolean prevent = false;
public static void sendEvent(boolean up) {
if (active) {
WritableMap args = Arguments.createMap();
if (up)
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeUp", args);
else
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeDown", args);
}
}
public static void down() {
sendEvent(false);
}
public static void up() {
sendEvent(true);
}
@ReactMethod
public void connect() {
active = true;
}
@ReactMethod
void disconnect() {
active = false;
}
@ReactMethod
public void pause() {
active = false;
}
@ReactMethod
public void unpause() {
active = true;
}
@ReactMethod
public void preventDefault() {
prevent = true;
}
@ReactMethod
public void noPreventDefault() {
prevent = false;
}
@NonNull
@Override
public String getName() {
return "VolumeButtonListener";
}
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}
@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}
}
@@ -1,30 +0,0 @@
package com.rajarsheechatterjee.VolumeButtonListener;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class VolumeButtonListenerPackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<NativeModule>();
modules.add(new VolumeButtonListener(reactContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
@@ -1,169 +0,0 @@
package com.rajarsheechatterjee.ZipArchive;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class ZipArchive extends ReactContextBaseJavaModule {
ZipArchive(ReactApplicationContext context) throws Exception {
super(context);
}
@NonNull
@Override
public String getName() {
return "ZipArchive";
}
private String escapeFilePath(String filePath){
return filePath.replaceAll(":", "\uA789");
}
private void unzipProcess(ZipInputStream zis, String distDirPath) throws Exception {
ZipEntry zipEntry;
int len;
byte[] buffer = new byte[4096];
while ((zipEntry = zis.getNextEntry()) != null) {
if(zipEntry.getName().endsWith("/")) continue;
String escapedFilePath = this.escapeFilePath(zipEntry.getName());
File newFile = new File(distDirPath, escapedFilePath);
newFile.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(newFile);
while ((len = zis.read(buffer)) > 0) fos.write(buffer, 0, len);
fos.close();
}
zis.closeEntry();
}
@ReactMethod
public void unzip(String sourceFilePath, String distDirPath, Promise promise) {
new Thread(new Runnable() {
@Override
public void run() {
try{
ZipInputStream zis = new ZipInputStream(new FileInputStream(sourceFilePath));
unzipProcess(zis, distDirPath);
zis.close();
promise.resolve(null);
} catch (Exception e) {
promise.reject(e.getCause());
}
}
}).start();
}
@ReactMethod
public void remoteUnzip(String distDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(_url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
if(headers != null){
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
while (it.hasNext()){
Map.Entry<String, Object> entry = it.next();
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
}
}
ZipInputStream zis = new ZipInputStream(connection.getInputStream());
unzipProcess(zis, distDirPath);
connection.disconnect();
promise.resolve(null);
}catch (Exception e){
promise.reject(e.getCause());
}
}
}).start();
}
private ArrayList<String> walkDir(String path) {
ArrayList<String> res = new ArrayList<>();
File node = new File(path);
if(node.isFile()){
res.add(path);
}else{
String[] children = node.list();
if(children != null){
for(String filename: children){
ArrayList<String> childPaths = walkDir(new File(path, filename).toString());
res.addAll(childPaths);
}
}
}
return res;
}
private void zipProcess(String sourceDirPath, ZipOutputStream zos) throws Exception{
ArrayList<String> paths = walkDir(sourceDirPath);
byte[] buffer = new byte[4096];
int len;
for(String path: paths){
ZipEntry zipEntry = new ZipEntry(path.replace(sourceDirPath + "/", ""));
zos.putNextEntry(zipEntry);
try (FileInputStream fis = new FileInputStream(path)) {
while ((len = fis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
}
zos.close();
}
@ReactMethod
public void remoteZip(String sourceDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(_url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
if(headers != null){
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
while (it.hasNext()){
Map.Entry<String, Object> entry = it.next();
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
}
}
ZipOutputStream zos = new ZipOutputStream(connection.getOutputStream());
zipProcess(sourceDirPath, zos);
if(connection.getResponseCode() == 200){
InputStream is = connection.getInputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (int length; (length = is.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
}
is.close();
connection.disconnect();
promise.resolve(result.toString());
}else{
throw new Exception("Network request failed");
}
}catch (Exception e){
promise.reject(e.getCause());
}
}
}).start();
}
}
@@ -1,32 +0,0 @@
package com.rajarsheechatterjee.ZipArchive;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ZipArchivePackage implements ReactPackage {
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
List<NativeModule> modules = new ArrayList<>();
try {
modules.add(new ZipArchive(reactApplicationContext));
} catch (Exception e) {
throw new RuntimeException(e);
}
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
return Collections.emptyList();
}
}
+17
View File
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,138 @@
/*
* 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; });
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<string name="app_name">LNReader</string>
</resources>
</resources>
+9 -1
View File
@@ -11,7 +11,15 @@
<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>
<item name="colorControlActivated">#2596be</item>
</style>
<style name="SplashScreen_SplashAnimation">
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
<style name="SplashScreen_SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowAnimationStyle">@style/SplashScreen_SplashAnimation</item>
</style>
<style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
@@ -1,20 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.rajarsheechatterjee.LNReader;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}
+9 -11
View File
@@ -1,16 +1,11 @@
import org.apache.tools.ant.taskdefs.condition.Os
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "33.0.0"
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
kotlinVersion = "1.8.10"
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
buildToolsVersion = "36.0.0"
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
kotlinVersion = "2.1.20"
}
repositories {
google()
@@ -19,5 +14,8 @@ buildscript {
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"
+7 -2
View File
@@ -12,7 +12,7 @@
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
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
@@ -36,7 +36,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# 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=false
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
@@ -49,3 +49,8 @@ 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.
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+23 -16
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# 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/.
@@ -83,10 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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"'
# 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
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -133,10 +133,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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.
@@ -144,7 +147,7 @@ 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=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,16 +200,20 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# 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" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
@@ -241,4 +248,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
exec "$JAVACMD" "$@"
+20 -13
View File
@@ -1,3 +1,8 @@
@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
@@ -13,6 +18,8 @@
@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 ##########################################################################
@@ -43,11 +50,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
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
@@ -57,22 +64,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
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=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%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
@@ -89,4 +96,4 @@ exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:omega
+4 -2
View File
@@ -1,6 +1,8 @@
rootProject.name = 'LNReader'
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'LNReader'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
+10 -1
View File
@@ -4,7 +4,16 @@
"slug": "LNReader",
"scheme": "lnreader",
"plugins": [
"expo-localization"
"expo-localization",
"react-native-edge-to-edge",
[
"expo-sqlite",
{
"enableFTS": false,
"useSQLCipher": false
}
],
"expo-web-browser"
]
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+12 -4
View File
@@ -1,8 +1,14 @@
const ReactCompilerConfig = {
target: '19',
};
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo', 'module:metro-react-native-babel-preset'],
presets: ['module:@react-native/babel-preset'],
plugins: [
'module:@babel/plugin-transform-export-namespace-from',
['babel-plugin-react-compiler', ReactCompilerConfig],
[
'module-resolver',
{
@@ -12,18 +18,20 @@ module.exports = function (api) {
'@hooks': './src/hooks',
'@screens': './src/screens',
'@strings': './strings',
'@redux': './src/redux',
'@services': './src/services',
'@plugins': './src/plugins',
'@utils': './src/utils',
'@theme': './src/theme',
'@navigators': './src/navigators',
'@native': './src/native',
'@api': './src/api',
'@type': './src/type',
'@specs': './specs',
'react-native-vector-icons/MaterialCommunityIcons':
'@react-native-vector-icons/material-design-icons',
},
},
],
'react-native-reanimated/plugin',
'react-native-worklets/plugin',
[
'module:react-native-dotenv',
{
+1 -1
View File
@@ -1,4 +1,4 @@
pull_request_title: 'chore: Update translations'
pull_request_title: 'chore: Update Translations'
commit_message: '[ci skip]'
files:
- source: /strings/languages/en/strings.json
Vendored
+3
View File
@@ -1,4 +1,7 @@
declare module '@env' {
export const MYANIMELIST_CLIENT_ID: string;
export const ANILIST_CLIENT_ID: string;
export const GIT_HASH: string;
export const RELEASE_DATE: string;
export const BUILD_TYPE: 'Debug' | 'Release' | 'Beta' | 'Github Action';
}
+180
View File
@@ -0,0 +1,180 @@
{
description = "React Native development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
android_sdk.accept_license = true;
};
androidEnv = pkgs.androidenv.override { licenseAccepted = true; };
# The androidComposition now expects enableEmulator to be passed in
# It's no longer defined here but passed from mkShell below.
mkAndroidComposition = { enableEmulatorInput }: androidEnv.composeAndroidPackages {
cmdLineToolsVersion = "8.0";
platformToolsVersion = "35.0.2";
buildToolsVersions = [ "35.0.0" ];
platformVersions = [ "35" ];
abiVersions = [ "x86_64" ];
includeNDK = true;
ndkVersions = [
"27.1.12297006"
"27.0.12077973"
];
includeSystemImages = enableEmulatorInput; # Use the input parameter
systemImageTypes = [ "google_apis" ];
includeEmulator = enableEmulatorInput; # Use the input parameter
useGoogleAPIs = true;
extraLicenses = [
"android-sdk-arm-dbt-license"
"android-sdk-license"
"android-sdk-preview-license"
"google-gdk-license"
"intel-android-extra-license"
"intel-android-sysimage-license"
"mips-android-sysimage-license"
];
};
in
{
devShells = {
# Define a specific devShell for when emulator is enabled
emulator =
let
enableEmulator = true;
androidComposition = mkAndroidComposition { enableEmulatorInput = enableEmulator; };
androidSdk = androidComposition.androidsdk;
in
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.pnpm
openjdk17
androidSdk
gradle
git
curl
unzip
which
rsync
scrcpy
]
++ pkgs.lib.optionals enableEmulator [
libglvnd
mesa
vulkan-loader
qemu_kvm
];
shellHook = ''
export ANDROID_USER_HOME="$HOME/.android-nix"
mkdir -p "$ANDROID_USER_HOME"
if [ ! -d "$ANDROID_USER_HOME/sdk" ]; then
echo "Setting up writable Android SDK. This may take a moment..."
mkdir -p "$ANDROID_USER_HOME/sdk"
rsync -a "${androidSdk}/libexec/android-sdk/" "$ANDROID_USER_HOME/sdk/"
chmod -R u+w "$ANDROID_USER_HOME/sdk"
echo "Writable Android SDK setup complete."
fi
export ANDROID_HOME="$ANDROID_USER_HOME/sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
export JAVA_HOME="${pkgs.openjdk17}"
export REACT_NATIVE_PACKAGER_HOSTNAME="127.0.0.1"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk-bundle"
export NDK_HOME="$ANDROID_NDK_HOME"
echo "Emulator support is ENABLED."
export VK_ICD_FILENAMES="${pkgs.mesa}/share/vulkan/icd.d/radeon_icd.x86_64.json"
export LD_LIBRARY_PATH="${pkgs.libglvnd}/lib:${pkgs.mesa}/lib''${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH"
export EGL_PLATFORM=x11
if ! "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" list avd | grep -q "ReactNative_API35"; then
echo "Creating default AVD (ReactNative_API35 - Pixel 8 profile)..."
echo "no" | "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" create avd -n "ReactNative_API35" -k "system-images;android-35;google_apis;x86_64" -d "pixel_8"
echo "AVD created. First launch may take longer."
fi
echo "To launch emulator: emulator -avd ReactNative_API35 -gpu host -no-metrics -no-audio"
echo "React Native development environment loaded!"
echo "Android SDK: $ANDROID_HOME"
echo "Android NDK: $ANDROID_NDK_HOME"
echo "Java: $JAVA_HOME"
echo "Emulator Enabled: Yes"
echo ""
echo "--- Available Commands ---"
echo " npx react-native run-android"
echo " adb devices"
echo " scrcpy"
echo " npm start"
echo "--------------------------"
'';
};
# Define a specific devShell for when emulator is disabled
default =
let
enableEmulator = false; # Define enableEmulator locally for this shell instance
androidComposition = mkAndroidComposition { enableEmulatorInput = enableEmulator; };
androidSdk = androidComposition.androidsdk;
in
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.pnpm
openjdk17
androidSdk
gradle
git
curl
unzip
which
rsync
scrcpy
]
++ pkgs.lib.optionals enableEmulator [ # This list will be empty as enableEmulator is false
libglvnd
mesa
vulkan-loader
qemu_kvm
];
shellHook = ''
export ANDROID_USER_HOME="$HOME/.android-nix"
mkdir -p "$ANDROID_USER_HOME"
if [ ! -d "$ANDROID_USER_HOME/sdk" ]; then
echo "Setting up writable Android SDK. This may take a moment..."
mkdir -p "$ANDROID_USER_HOME/sdk"
rsync -a "${androidSdk}/libexec/android-sdk/" "$ANDROID_USER_HOME/sdk/"
chmod -R u+w "$ANDROID_USER_HOME/sdk"
echo "Writable Android SDK setup complete."
fi
export ANDROID_HOME="$ANDROID_USER_HOME/sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
export JAVA_HOME="${pkgs.openjdk17}"
export REACT_NATIVE_PACKAGER_HOSTNAME="127.0.0.1"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk-bundle"
export NDK_HOME="$ANDROID_NDK_HOME"
echo "Emulator support is DISABLED. Focusing on physical device development."
echo "React Native development environment loaded!"
echo "Android SDK: $ANDROID_HOME"
echo "Android NDK: $ANDROID_NDK_HOME"
echo "Java: $JAVA_HOME"
echo "Emulator Enabled: No"
echo ""
echo "--- Available Commands ---"
echo " npx react-native run-android"
echo " adb devices"
echo " scrcpy"
echo " pnpm run dev:start"
echo "--------------------------"
'';
};
};
});
}
+6
View File
@@ -1,5 +1,11 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { I18nManager } from 'react-native';
import { i18n } from './strings/translations';
const isRTL = i18n.locale.startsWith('ar') || i18n.locale.startsWith('he');
I18nManager.allowRTL(isRTL);
I18nManager.forceRTL(isRTL);
import App from './App';
+1
View File
@@ -0,0 +1 @@
export NODE_BINARY=$(command -v node)
+22
View File
@@ -0,0 +1,22 @@
import UIKit
import Foundation
import Lottie
@objc class Dynamic: NSObject {
@objc func createAnimationView(rootView: UIView, lottieName: String) -> AnimationView {
let animationView = AnimationView(name: lottieName)
animationView.frame = rootView.frame
animationView.center = rootView.center
animationView.backgroundColor = UIColor.white;
return animationView;
}
@objc func play(animationView: AnimationView) {
animationView.play(
completion: { (success) in
RNSplashScreen.setAnimationFinished(true)
}
);
}
}
+596
View File
@@ -0,0 +1,596 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
0C80B921A6F3F58F76C31292 /* libPods-LNReader.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-LNReader.a */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
147488B02DD8E9BD00C6D0A2 /* Dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147488AF2DD8E9BD00C6D0A2 /* Dynamic.swift */; };
147488B22DD8EA0700C6D0A2 /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 147488B12DD8EA0700C6D0A2 /* loading.json */; };
147488B72DD97E8A00C6D0A2 /* RCTNativeFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147488B62DD97E8A00C6D0A2 /* RCTNativeFile.swift */; };
147488BC2DD9882000C6D0A2 /* RCTNativeFile.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488BB2DD9882000C6D0A2 /* RCTNativeFile.mm */; };
147488C02DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488BF2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm */; };
147488C42DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488C32DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm */; };
147488C82DD9A68600C6D0A2 /* RCTNativeZipArchive.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488C72DD9A68600C6D0A2 /* RCTNativeZipArchive.mm */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
A1FA4E89FDA7CFAD14C43693 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F74819B237E0768BB72DED8 /* ExpoModulesProvider.swift */; };
CF1A2676AC104DBC83DBFFF9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* LNReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LNReader.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = LNReader/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = LNReader/Info.plist; sourceTree = "<group>"; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = LNReader/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
147488AA2DD8E7DE00C6D0A2 /* LNReader-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "LNReader-Bridging-Header.h"; path = "LNReader/LNReader-Bridging-Header.h"; sourceTree = "<group>"; };
147488AF2DD8E9BD00C6D0A2 /* Dynamic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dynamic.swift; sourceTree = "<group>"; };
147488B12DD8EA0700C6D0A2 /* loading.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = loading.json; sourceTree = "<group>"; };
147488B62DD97E8A00C6D0A2 /* RCTNativeFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RCTNativeFile.swift; sourceTree = "<group>"; };
147488BA2DD9882000C6D0A2 /* RCTNativeFile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeFile.h; sourceTree = "<group>"; };
147488BB2DD9882000C6D0A2 /* RCTNativeFile.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeFile.mm; sourceTree = "<group>"; };
147488BE2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeVolumeButtonListener.h; sourceTree = "<group>"; };
147488BF2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeVolumeButtonListener.mm; sourceTree = "<group>"; };
147488C22DD9A27400C6D0A2 /* RCTNativeEpubUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeEpubUtil.h; sourceTree = "<group>"; };
147488C32DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeEpubUtil.mm; sourceTree = "<group>"; };
147488C62DD9A68600C6D0A2 /* RCTNativeZipArchive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeZipArchive.h; sourceTree = "<group>"; };
147488C72DD9A68600C6D0A2 /* RCTNativeZipArchive.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeZipArchive.mm; sourceTree = "<group>"; };
1F74819B237E0768BB72DED8 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-LNReader/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
3B4392A12AC88292D35C810B /* Pods-LNReader.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LNReader.debug.xcconfig"; path = "Target Support Files/Pods-LNReader/Pods-LNReader.debug.xcconfig"; sourceTree = "<group>"; };
5709B34CF0A7D63546082F79 /* Pods-LNReader.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LNReader.release.xcconfig"; path = "Target Support Files/Pods-LNReader/Pods-LNReader.release.xcconfig"; sourceTree = "<group>"; };
5DCACB8F33CDC322A6C60F78 /* libPods-LNReader.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-LNReader.a"; sourceTree = BUILT_PRODUCTS_DIR; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = LNReader/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = LNReader/LaunchScreen.storyboard; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0C80B921A6F3F58F76C31292 /* libPods-LNReader.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
13B07FAE1A68108700A75B9A /* LNReader */ = {
isa = PBXGroup;
children = (
13B07FB51A68108700A75B9A /* Images.xcassets */,
761780EC2CA45674006654EE /* AppDelegate.swift */,
147488B12DD8EA0700C6D0A2 /* loading.json */,
13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
147488AA2DD8E7DE00C6D0A2 /* LNReader-Bridging-Header.h */,
147488AF2DD8E9BD00C6D0A2 /* Dynamic.swift */,
147488B62DD97E8A00C6D0A2 /* RCTNativeFile.swift */,
);
name = LNReader;
sourceTree = "<group>";
};
147488B92DD987DF00C6D0A2 /* NativeFile */ = {
isa = PBXGroup;
children = (
147488BA2DD9882000C6D0A2 /* RCTNativeFile.h */,
147488BB2DD9882000C6D0A2 /* RCTNativeFile.mm */,
);
path = NativeFile;
sourceTree = "<group>";
};
147488BD2DD9A0E900C6D0A2 /* NativeVolumeButtonListener */ = {
isa = PBXGroup;
children = (
147488BE2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.h */,
147488BF2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm */,
);
path = NativeVolumeButtonListener;
sourceTree = "<group>";
};
147488C12DD9A25E00C6D0A2 /* NativeEpubUtil */ = {
isa = PBXGroup;
children = (
147488C22DD9A27400C6D0A2 /* RCTNativeEpubUtil.h */,
147488C32DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm */,
);
path = NativeEpubUtil;
sourceTree = "<group>";
};
147488C52DD9A66F00C6D0A2 /* NativeZipArchive */ = {
isa = PBXGroup;
children = (
147488C62DD9A68600C6D0A2 /* RCTNativeZipArchive.h */,
147488C72DD9A68600C6D0A2 /* RCTNativeZipArchive.mm */,
);
path = NativeZipArchive;
sourceTree = "<group>";
};
24615CBE8A657680F1260CBB /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
28BBFC4322E6EFCA529574FE /* LNReader */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
28BBFC4322E6EFCA529574FE /* LNReader */ = {
isa = PBXGroup;
children = (
1F74819B237E0768BB72DED8 /* ExpoModulesProvider.swift */,
);
name = LNReader;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
5DCACB8F33CDC322A6C60F78 /* libPods-LNReader.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
147488C52DD9A66F00C6D0A2 /* NativeZipArchive */,
147488C12DD9A25E00C6D0A2 /* NativeEpubUtil */,
147488BD2DD9A0E900C6D0A2 /* NativeVolumeButtonListener */,
147488B92DD987DF00C6D0A2 /* NativeFile */,
13B07FAE1A68108700A75B9A /* LNReader */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
BBD78D7AC51CEA395F1C20DB /* Pods */,
24615CBE8A657680F1260CBB /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* LNReader.app */,
);
name = Products;
sourceTree = "<group>";
};
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup;
children = (
3B4392A12AC88292D35C810B /* Pods-LNReader.debug.xcconfig */,
5709B34CF0A7D63546082F79 /* Pods-LNReader.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
13B07F861A680F5B00A75B9A /* LNReader */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "LNReader" */;
buildPhases = (
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
E2CB0E1FD58C9A385B2CE9DA /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = LNReader;
productName = LNReader;
productReference = 13B07F961A680F5B00A75B9A /* LNReader.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1210;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1120;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "LNReader" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* LNReader */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
147488B22DD8EA0700C6D0A2 /* loading.json in Resources */,
CF1A2676AC104DBC83DBFFF9 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/.xcode.env",
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
};
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-LNReader-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-resources.sh\"\n";
showEnvVarsInLog = 0;
};
E2CB0E1FD58C9A385B2CE9DA /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-LNReader/expo-configure-project.sh\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
147488C02DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm in Sources */,
147488B02DD8E9BD00C6D0A2 /* Dynamic.swift in Sources */,
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
147488BC2DD9882000C6D0A2 /* RCTNativeFile.mm in Sources */,
147488B72DD97E8A00C6D0A2 /* RCTNativeFile.swift in Sources */,
147488C82DD9A68600C6D0A2 /* RCTNativeZipArchive.mm in Sources */,
A1FA4E89FDA7CFAD14C43693 /* ExpoModulesProvider.swift in Sources */,
147488C42DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-LNReader.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = LNReader/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = LNReader;
SWIFT_OBJC_BRIDGING_HEADER = "LNReader/LNReader-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-LNReader.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = LNReader/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = LNReader;
SWIFT_OBJC_BRIDGING_HEADER = "LNReader/LNReader-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = (
"\"$(SDKROOT)/usr/lib/swift\"",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
USE_HERMES = true;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = (
"\"$(SDKROOT)/usr/lib/swift\"",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "LNReader" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "LNReader" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1210"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LNReader.app"
BlueprintName = "LNReader"
ReferencedContainer = "container:LNReader.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "LNReaderTests.xctest"
BlueprintName = "LNReaderTests"
ReferencedContainer = "container:LNReader.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LNReader.app"
BlueprintName = "LNReader"
ReferencedContainer = "container:LNReader.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LNReader.app"
BlueprintName = "LNReader"
ReferencedContainer = "container:LNReader.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:LNReader.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+71
View File
@@ -0,0 +1,71 @@
import UIKit
import Expo
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import Lottie
@main
class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ReactNativeDelegate?
var reactNativeFactory: RCTReactNativeFactory?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions
)
if let w = window {
let splashScreenBackground = UIColor(red: 31/255, green: 32/255, blue: 36/255, alpha: 1)
w.backgroundColor = splashScreenBackground
w.rootViewController?.view.backgroundColor = splashScreenBackground
let t = Dynamic()
let animationSize = CGSize(width: 200, height: 200)
let screenBounds = UIScreen.main.bounds
let animationX = (screenBounds.width - animationSize.width) / 2
let animationY = (screenBounds.height - animationSize.height) / 2
let animationUIView: UIView = t.createAnimationView(rootView: UIView(frame:CGRect(x: animationX, y: animationY, width: animationSize.width, height: animationSize.height)), lottieName:"loading")
RNSplashScreen.showLottieSplash(animationUIView, inRootView: w)
animationUIView.backgroundColor = UIColor(white: 1, alpha:0)
t.play(animationView: animationUIView as! AnimationView)
RNSplashScreen.setAnimationFinished(true)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return RCTLinkingManager.application(application, open: url, options: options);
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1 @@
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]}
+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>LNReader</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLIconFile</key>
<string></string>
<key>CFBundleURLName</key>
<string>LNReader</string>
<key>CFBundleURLSchemes</key>
<array>
<string>lnreader</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
+14
View File
@@ -0,0 +1,14 @@
//
// LNReader-Bridging-Header.h
// LNReader
//
// Created by QUAN on 17/5/25.
//
#ifndef LNReader_Bridging_Header_h
#define LNReader_Bridging_Header_h
#import "RNSplashScreen.h"
#import <React/RCTLinkingManager.h>
#endif /* LNReader_Bridging_Header_h */
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" red="0.12156862745098039" green="0.12549019607843137" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<color key="tintColor" red="0.12156862745098039" green="0.12549019607843137" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.173913043478265" y="375"/>
</scene>
</scenes>
</document>
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>0A2A.1</string>
<string>3B52.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
<string>85F4.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
+10
View File
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
#import <LNReaderSpec/LNReaderSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeEpubUtil : NSObject<NativeEpubUtilSpec>
@end
NS_ASSUME_NONNULL_END

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