aksos-react-native-epub-reader 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -2
  3. package/lib/EpubReader.d.ts +63 -0
  4. package/lib/EpubReader.js +199 -0
  5. package/lib/EpubReaderView.d.ts +24 -0
  6. package/lib/EpubReaderView.js +504 -0
  7. package/lib/assets/page-turn-1.wav +0 -0
  8. package/lib/assets/page-turn-2.wav +0 -0
  9. package/lib/assets/page-turn-3.wav +0 -0
  10. package/lib/assets/page-turn-4.wav +0 -0
  11. package/lib/engine/html.d.ts +1 -0
  12. package/lib/engine/html.js +5 -0
  13. package/lib/index.d.ts +6 -0
  14. package/lib/index.js +30 -0
  15. package/lib/sound.d.ts +1 -0
  16. package/lib/sound.js +71 -0
  17. package/lib/themes.d.ts +56 -0
  18. package/lib/themes.js +166 -0
  19. package/lib/types.d.ts +133 -0
  20. package/lib/types.js +2 -0
  21. package/lib/ui/AppearanceSheet.d.ts +15 -0
  22. package/lib/ui/AppearanceSheet.js +127 -0
  23. package/lib/ui/ContentsSheet.d.ts +27 -0
  24. package/lib/ui/ContentsSheet.js +87 -0
  25. package/lib/ui/CustomizeSheet.d.ts +16 -0
  26. package/lib/ui/CustomizeSheet.js +139 -0
  27. package/lib/ui/Glass.d.ts +15 -0
  28. package/lib/ui/Glass.js +27 -0
  29. package/lib/ui/HighlightMenu.d.ts +25 -0
  30. package/lib/ui/HighlightMenu.js +43 -0
  31. package/lib/ui/Icons.d.ts +17 -0
  32. package/lib/ui/Icons.js +193 -0
  33. package/lib/ui/LineGuide.d.ts +21 -0
  34. package/lib/ui/LineGuide.js +92 -0
  35. package/lib/ui/NoteEditor.d.ts +12 -0
  36. package/lib/ui/NoteEditor.js +61 -0
  37. package/lib/ui/Scrubber.d.ts +15 -0
  38. package/lib/ui/Scrubber.js +86 -0
  39. package/lib/ui/SearchSheet.d.ts +14 -0
  40. package/lib/ui/SearchSheet.js +67 -0
  41. package/lib/ui/Sheet.d.ts +19 -0
  42. package/lib/ui/Sheet.js +38 -0
  43. package/lib/ui/Slider.d.ts +14 -0
  44. package/lib/ui/Slider.js +53 -0
  45. package/package.json +75 -5
  46. package/src/EpubReader.tsx +256 -0
  47. package/src/EpubReaderView.tsx +721 -0
  48. package/src/assets/page-turn-1.wav +0 -0
  49. package/src/assets/page-turn-2.wav +0 -0
  50. package/src/assets/page-turn-3.wav +0 -0
  51. package/src/assets/page-turn-4.wav +0 -0
  52. package/src/engine/html.ts +2 -0
  53. package/src/engine/reader.js +1696 -0
  54. package/src/index.ts +6 -0
  55. package/src/sound.ts +66 -0
  56. package/src/themes.ts +188 -0
  57. package/src/types.ts +138 -0
  58. package/src/ui/AppearanceSheet.tsx +167 -0
  59. package/src/ui/ContentsSheet.tsx +144 -0
  60. package/src/ui/CustomizeSheet.tsx +189 -0
  61. package/src/ui/Glass.tsx +37 -0
  62. package/src/ui/HighlightMenu.tsx +64 -0
  63. package/src/ui/Icons.tsx +246 -0
  64. package/src/ui/LineGuide.tsx +87 -0
  65. package/src/ui/NoteEditor.tsx +55 -0
  66. package/src/ui/Scrubber.tsx +78 -0
  67. package/src/ui/SearchSheet.tsx +79 -0
  68. package/src/ui/Sheet.tsx +69 -0
  69. package/src/ui/Slider.tsx +30 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aksos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,339 @@
1
- # Temporary Holding Version
1
+ # aksos-react-native-epub-reader
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A complete EPUB reader for **React Native and Expo**, with the feel of a modern book app: real page curl, slide, fade and
4
+ continuous scroll, themes with light and dark variants, highlights, notes, bookmarks, whole-book search, a line guide, and
5
+ first-class **right-to-left** support (Pashto, Dari, Persian, Arabic, Urdu, Hebrew).
6
+
7
+ It is one component with a full reading UI (`EpubReaderView`), plus a headless engine (`EpubReader`) if you want to build your own.
8
+
9
+ - Works in **Expo Go**, Expo development builds and bare React Native (iOS and Android).
10
+ - No native code of its own: it uses `react-native-webview` and `react-native-svg`.
11
+ - You own the storage. The reader gives you the reading position, highlights, bookmarks and settings through callbacks.
12
+
13
+ ---
14
+
15
+ ## Contents
16
+
17
+ 1. [Install](#install)
18
+ 2. [Quick start](#quick-start)
19
+ 3. [Saving and restoring](#saving-and-restoring)
20
+ 4. [`EpubReaderView` props](#epubreaderview-props)
21
+ 5. [Headless `EpubReader`](#headless-epubreader)
22
+ 6. [Data types](#data-types)
23
+ 7. [Settings reference](#settings-reference)
24
+ 8. [Right-to-left books](#right-to-left-books)
25
+ 9. [What the user can do](#what-the-user-can-do)
26
+ 10. [Troubleshooting](#troubleshooting)
27
+ 11. [Limitations](#limitations)
28
+ 12. [Credits and license](#credits-and-license)
29
+
30
+ ---
31
+
32
+ ## Install
33
+
34
+ ### Expo
35
+
36
+ ```bash
37
+ npx expo install react-native-webview react-native-svg
38
+ npm install aksos-react-native-epub-reader
39
+ ```
40
+
41
+ Optional, each one adds a feature and the reader simply works without it:
42
+
43
+ ```bash
44
+ npx expo install expo-blur expo-audio expo-screen-orientation
45
+ ```
46
+
47
+ | Package | Adds |
48
+ |---|---|
49
+ | `expo-blur` | frosted-glass controls and sheets (without it they are plain translucent) |
50
+ | `expo-audio` | the page-turn sound |
51
+ | `expo-screen-orientation` | the rotation-lock button |
52
+
53
+ > After installing a new package, restart Metro with a cleared cache: `npx expo start --clear`.
54
+
55
+ ### Bare React Native
56
+
57
+ ```bash
58
+ npm install aksos-react-native-epub-reader react-native-webview react-native-svg
59
+ cd ios && pod install
60
+ ```
61
+
62
+ Add the optional packages above if you want those features.
63
+
64
+ ### Opening files from the device
65
+
66
+ The reader takes a file URI or a base64 string. To let users pick a book, add a document picker:
67
+
68
+ ```bash
69
+ npx expo install expo-document-picker expo-file-system
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Quick start
75
+
76
+ ```tsx
77
+ import React from 'react';
78
+ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
79
+ import { EpubReaderView } from 'aksos-react-native-epub-reader';
80
+
81
+ function Reader({ uri, onClose }: { uri: string; onClose: () => void }) {
82
+ const insets = useSafeAreaInsets();
83
+ return (
84
+ <EpubReaderView
85
+ source={{ uri }} // file:// or http(s):// (or { base64: '...' })
86
+ safeInsets={{ top: insets.top, bottom: insets.bottom }}
87
+ onClose={onClose}
88
+ />
89
+ );
90
+ }
91
+
92
+ export default function App() {
93
+ return (
94
+ <SafeAreaProvider>
95
+ <Reader uri="file:///path/to/book.epub" onClose={() => {}} />
96
+ </SafeAreaProvider>
97
+ );
98
+ }
99
+ ```
100
+
101
+ That is a complete reader: tap the middle of the page to show the controls, long-press a word to highlight.
102
+
103
+ ### Pick a book from the device (Expo)
104
+
105
+ ```tsx
106
+ import * as DocumentPicker from 'expo-document-picker';
107
+ import * as FileSystem from 'expo-file-system/legacy';
108
+
109
+ async function importBook() {
110
+ const res = await DocumentPicker.getDocumentAsync({ type: ['application/epub+zip', '*/*'], copyToCacheDirectory: true });
111
+ if (res.canceled) return null;
112
+ const dest = `${FileSystem.documentDirectory}${Date.now()}.epub`;
113
+ await FileSystem.copyAsync({ from: res.assets[0].uri, to: dest });
114
+ return dest; // pass as source={{ uri: dest }}
115
+ }
116
+ ```
117
+
118
+ A complete example app (library, import, saving) is in [`example/`](example/).
119
+
120
+ ---
121
+
122
+ ## Saving and restoring
123
+
124
+ The reader never stores anything itself. Save what the callbacks give you, and pass it back next time:
125
+
126
+ ```tsx
127
+ import AsyncStorage from '@react-native-async-storage/async-storage';
128
+
129
+ const key = (id: string) => `book:${id}`;
130
+
131
+ <EpubReaderView
132
+ source={{ uri }}
133
+ initialLocation={saved.location} // { spine, offset }
134
+ initialHighlights={saved.highlights}
135
+ initialBookmarks={saved.bookmarks}
136
+ initialSettings={saved.settings} // partial ReaderSettings
137
+
138
+ onLocationChange={(e) => save({ location: { spine: e.spine, offset: e.offset, progress: e.progress } })}
139
+ onHighlightsChange={(list) => save({ highlights: list })}
140
+ onBookmarksChange={(list) => save({ bookmarks: list })}
141
+ onSettingsChange={(s) => save({ settings: s })}
142
+ />
143
+ ```
144
+
145
+ Locations are `{ spine, offset }`: the chapter number and a character offset inside it. They stay valid when the user
146
+ changes font size, theme or rotates the device.
147
+
148
+ `onLocationChange` fires on every page, so debounce the write (about 400 ms is plenty). The example app does this in
149
+ [`example/App.tsx`](example/App.tsx).
150
+
151
+ ---
152
+
153
+ ## `EpubReaderView` props
154
+
155
+ | Prop | Type | Description |
156
+ |---|---|---|
157
+ | `source` | `{ uri: string } \| { base64: string }` | **Required.** The EPUB. |
158
+ | `title` | `string` | Shown in Contents until the book's own title is read. |
159
+ | `safeInsets` | `{ top: number; bottom: number }` | Status-bar and home-indicator space. Use `useSafeAreaInsets()`. |
160
+ | `initialLocation` | `{ spine: number; offset: number }` | Where to open. Defaults to the start. |
161
+ | `initialSettings` | `Partial<ReaderSettings>` | Theme, font, size, spacing ... |
162
+ | `initialHighlights` | `Highlight[]` | |
163
+ | `initialBookmarks` | `Bookmark[]` | |
164
+ | `onClose` | `() => void` | The X button. |
165
+ | `onLocationChange` | `(e: RelocateEvent) => void` | Every page turn. Has `spine`, `offset`, `progress` (0..1), `page`, `pageCount`, `chapter`. |
166
+ | `onHighlightsChange` | `(list: Highlight[]) => void` | After any create / edit / delete. |
167
+ | `onBookmarksChange` | `(list: Bookmark[]) => void` | |
168
+ | `onSettingsChange` | `(s: ReaderSettings) => void` | |
169
+ | `onMetadata` | `(m: BookMetadata) => void` | `title`, `author`, `language`, `publisher`. |
170
+ | `onCover` | `(dataUri: string) => void` | The cover image, if the book has one. |
171
+ | `onError` | `(message: string) => void` | Unreadable file, DRM, ... |
172
+
173
+ ---
174
+
175
+ ## Headless `EpubReader`
176
+
177
+ `EpubReader` is only the page engine, with no buttons and no sheets. Use it to build your own interface.
178
+
179
+ ```tsx
180
+ import { EpubReader, DEFAULT_SETTINGS } from 'aksos-react-native-epub-reader';
181
+ import type { EpubReaderHandle } from 'aksos-react-native-epub-reader';
182
+
183
+ const ref = useRef<EpubReaderHandle>(null);
184
+
185
+ <EpubReader
186
+ ref={ref}
187
+ source={{ uri }}
188
+ settings={{ ...DEFAULT_SETTINGS, themeId: 'calm', fontSize: 21 }}
189
+ systemDark={useColorScheme() === 'dark'}
190
+ safeInsets={{ top: 50, bottom: 30 }}
191
+ highlights={highlights}
192
+ onRelocate={(e) => console.log(e.chapter, e.progress)}
193
+ onSelection={(sel) => sel && console.log(sel.text)} // sel.start / sel.end / sel.rect
194
+ onTap={() => toggleMyControls()}
195
+ onToc={(items) => setToc(items)}
196
+ />
197
+
198
+ ref.current?.next();
199
+ ref.current?.prev();
200
+ ref.current?.goTo({ spine: 3 });
201
+ ref.current?.goTo({ spine: 3, fragment: 'section-2' });
202
+ ref.current?.goTo({ progress: 0.5 }); // 0..1 across the whole book
203
+ ref.current?.search('word', 1); // results arrive in onSearchResults(1, results, done)
204
+ ref.current?.showSearchResult(result);
205
+ ref.current?.clearSelection();
206
+ ref.current?.setLabels('Chapter 3', '42 of 300'); // running header / footer drawn on the page
207
+ ```
208
+
209
+ Extra callbacks: `onReady`, `onCover`, `onIndexed`, `onHighlightTap`, `onLink`, `onSearchResults`, `onEndReached`, `onPageTurn`, `onError`.
210
+
211
+ ---
212
+
213
+ ## Data types
214
+
215
+ ```ts
216
+ interface Highlight {
217
+ id: string; spine: number; start: number; end: number; // character offsets in that chapter
218
+ text: string; color: 'yellow' | 'green' | 'blue' | 'pink' | 'purple';
219
+ style?: 'highlight' | 'underline'; note?: string; chapter?: string; createdAt: number;
220
+ }
221
+
222
+ interface Bookmark {
223
+ id: string; spine: number; offset: number; chapter: string; excerpt: string; progress: number; createdAt: number;
224
+ }
225
+
226
+ interface RelocateEvent {
227
+ spine: number; offset: number; end: number; page: number; pageCount: number; spineCount: number;
228
+ textLength: number; progress: number; chapter: string; excerpt: string; indexed: boolean;
229
+ }
230
+ ```
231
+
232
+ All types are exported: `import type { Highlight, Bookmark, ReaderSettings, TocItem, ... } from 'aksos-react-native-epub-reader'`.
233
+
234
+ ---
235
+
236
+ ## Settings reference
237
+
238
+ `ReaderSettings` (all optional in `initialSettings`; defaults are in `DEFAULT_SETTINGS`):
239
+
240
+ | Field | Values | Default |
241
+ |---|---|---|
242
+ | `themeId` | `original` `quiet` `paper` `bold` `calm` `focus` | `original` |
243
+ | `appearance` | `light` `dark` `device` | `device` |
244
+ | `fontId` | `original` (the book's own) `system` `newyork` `athelas` `charter` `georgia` `iowan` `palatino` `seravek` `times` `naskh` | `original` |
245
+ | `fontSize` | 12 - 40 | `19` |
246
+ | `bold` | `boolean` | `false` |
247
+ | `customize` | turns on the spacing values below | `false` |
248
+ | `lineHeight` | 1.1 - 2.2 | `1.5` |
249
+ | `letterSpacing` / `wordSpacing` | -50 .. 100 (percent) | `0` |
250
+ | `margins` | -50 .. 100 (percent) | `0` |
251
+ | `columns` | `auto` `1` `2` | `auto` |
252
+ | `justify` | `boolean` | `false` |
253
+ | `pageTurn` | `slide` `curl` `fade` `scroll` | `slide` |
254
+ | `pageSound` | paper sound on page turns | `true` |
255
+ | `brightness` | 0.3 - 1 | `1` |
256
+ | `direction` | `auto` `ltr` `rtl` | `auto` |
257
+
258
+ Exported helpers: `THEMES`, `FONTS`, `HIGHLIGHT_COLORS`, `DEFAULT_SETTINGS`, `getTheme(id)`, `getFont(id)`, `chromeColors(...)`.
259
+
260
+ > The fonts are CSS stacks, so what you see depends on the device. `original` always uses the fonts inside the book.
261
+ > For Pashto / Arabic text, `naskh` asks for Noto Naskh, Geeza Pro or Scheherazade.
262
+
263
+ ---
264
+
265
+ ## Right-to-left books
266
+
267
+ Direction is detected automatically, in this order:
268
+
269
+ 1. `page-progression-direction` in the book,
270
+ 2. the book's language (`ps`, `fa`, `ar`, `ur`, `he`, ...),
271
+ 3. **the text itself**: if the first chapters are mostly Arabic script, the book is treated as right-to-left, even when
272
+ the metadata says English. (Many Pashto and Dari EPUBs have wrong or missing language tags.)
273
+
274
+ In a right-to-left book pages turn from left to right, taps and swipes are mirrored, the progress bar fills from the right,
275
+ and the page curl folds the other way. Set `settings.direction` to `ltr` or `rtl` to override the detection.
276
+
277
+ ---
278
+
279
+ ## What the user can do
280
+
281
+ - **Turn pages** with a tap on the left or right edge, or a swipe. Choose Slide, Curl (a real corner fold, following the finger),
282
+ Fast Fade or Scroll (continuous, chapter after chapter).
283
+ - **Tap the middle** to show the controls: close, Contents (chapters, bookmarks, highlights with page numbers), Search Book,
284
+ Themes & Settings, Customize, share, rotation lock, Line Guide, bookmark.
285
+ - **Long-press a word** and drag to select, then choose **Highlight** or **Note**. Tap a highlight to change its colour,
286
+ underline it, edit the note or delete it.
287
+ - **Line Guide** dims everything except one line that can be dragged.
288
+ - **Links** inside the book work; external links open in the browser.
289
+
290
+ ---
291
+
292
+ ## Troubleshooting
293
+
294
+ **"Unable to resolve module" right after installing a package.** Metro has a stale cache: `npx expo start --clear`.
295
+
296
+ **No page-turn sound.** Install `expo-audio`, turn up the media volume, and check the iPhone silent switch.
297
+ The sound can be disabled by the user in Themes & Settings, Customize.
298
+
299
+ **Controls are not blurred on Android.** `expo-blur` blur is limited on Android; the controls fall back to translucent colour.
300
+
301
+ **A book will not open.** `onError` receives the reason. DRM-protected files (Adobe, Apple FairPlay) cannot be opened.
302
+
303
+ **Opening is slow for a very large book.** The file is sent to the page engine as base64. Typical novels take well under
304
+ a second; a 50 MB illustrated book will be slow.
305
+
306
+ ---
307
+
308
+ ## Limitations
309
+
310
+ - Reflowable EPUBs only. Fixed-layout EPUBs and media overlays are not supported.
311
+ - Themes and fonts are chosen from the built-in lists (no custom theme prop yet).
312
+ - Embedded fonts that are obfuscated by the publisher fall back to a system font.
313
+ - Text selection and highlights work in the chapter that is on screen; in Scroll mode a neighbouring chapter becomes
314
+ selectable once it scrolls into the middle of the screen.
315
+ - Search folds case and common Arabic / Persian letter variants (for example ي and ی) but does not strip diacritics.
316
+ - The page curl is a geometric corner fold, not a physical simulation of paper.
317
+
318
+ ---
319
+
320
+ ## Credits and license
321
+
322
+ MIT, see [LICENSE](LICENSE).
323
+
324
+ The page-turn sounds (`src/assets/page-turn-*.wav`) are trimmed and softened recordings from
325
+ "10 Book Page Flips" by StarNinjas (https://opengameart.org/content/10-book-page-flips), released under CC0.
326
+
327
+ EPUB parsing uses [JSZip](https://stuk.github.io/jszip/).
328
+
329
+ ### Developing
330
+
331
+ ```bash
332
+ npm install
333
+ npm run build:engine # rebuilds src/engine/html.ts from src/engine/reader.js
334
+ npm run build:lib # compiles src/ to lib/
335
+ npm run typecheck
336
+ ```
337
+
338
+ The reading engine is plain JavaScript in `src/engine/reader.js`, bundled with JSZip into a single HTML page for the WebView.
339
+ `test/serve.js` serves that page in a browser together with sample books (`node test/make-epub.js` creates them).
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import type { BookLocation, BookMetadata, EpubSource, Highlight, ReaderSettings, RelocateEvent, SearchResult, SelectionEvent, TocItem } from './types';
3
+ export interface EpubReaderProps {
4
+ source: EpubSource;
5
+ settings: ReaderSettings;
6
+ /** system colour scheme, used when settings.appearance is 'device' */
7
+ systemDark?: boolean;
8
+ /** Space reserved for status bar / home indicator. The engine adds room for the running header and footer. */
9
+ safeInsets?: {
10
+ top: number;
11
+ bottom: number;
12
+ };
13
+ initialLocation?: BookLocation | {
14
+ progress: number;
15
+ };
16
+ highlights?: Highlight[];
17
+ /** Items added to the native text-selection menu. `onMenuAction` fires with the item key. */
18
+ menuItems?: {
19
+ label: string;
20
+ key: string;
21
+ }[];
22
+ onMenuAction?: (key: string, selection: SelectionEvent | null) => void;
23
+ onReady?: (info: {
24
+ metadata: BookMetadata;
25
+ rtl: boolean;
26
+ }) => void;
27
+ onToc?: (items: TocItem[]) => void;
28
+ onCover?: (dataUri: string) => void;
29
+ onRelocate?: (e: RelocateEvent) => void;
30
+ /** fired once the whole book has been indexed: cumulative character offset of every spine item */
31
+ onIndexed?: (info: {
32
+ totalLength: number;
33
+ cum: number[];
34
+ }) => void;
35
+ onSelection?: (e: SelectionEvent | null) => void;
36
+ onHighlightTap?: (id: string, rect: SelectionEvent['rect']) => void;
37
+ onTap?: () => void;
38
+ /** a page turn animation is starting (use it to play a sound) */
39
+ onPageTurn?: () => void;
40
+ onLink?: (href: string) => void;
41
+ onSearchResults?: (searchId: number, results: SearchResult[], done: boolean) => void;
42
+ onEndReached?: (edge: 'start' | 'end') => void;
43
+ onError?: (message: string) => void;
44
+ }
45
+ export interface EpubReaderHandle {
46
+ next(): void;
47
+ prev(): void;
48
+ goTo(location: {
49
+ spine: number;
50
+ offset?: number;
51
+ fragment?: string;
52
+ } | {
53
+ progress: number;
54
+ }): void;
55
+ search(query: string, searchId: number): void;
56
+ cancelSearch(searchId?: number): void;
57
+ /** Jump to a search result and flash the match. */
58
+ showSearchResult(r: SearchResult): void;
59
+ clearSelection(): void;
60
+ /** Replace the running header / footer text drawn on the page. */
61
+ setLabels(header: string, footer: string): void;
62
+ }
63
+ export declare const EpubReader: React.ForwardRefExoticComponent<EpubReaderProps & React.RefAttributes<EpubReaderHandle>>;
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EpubReader = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const react_native_webview_1 = require("react-native-webview");
40
+ const html_1 = require("./engine/html");
41
+ const themes_1 = require("./themes");
42
+ async function uriToBase64(uri) {
43
+ const res = await fetch(uri);
44
+ const blob = await res.blob();
45
+ return new Promise((resolve, reject) => {
46
+ const fr = new FileReader();
47
+ fr.onerror = () => reject(fr.error);
48
+ fr.onloadend = () => {
49
+ const s = String(fr.result);
50
+ resolve(s.slice(s.indexOf(',') + 1));
51
+ };
52
+ fr.readAsDataURL(blob);
53
+ });
54
+ }
55
+ const LS = String.fromCharCode(0x2028);
56
+ const PS = String.fromCharCode(0x2029);
57
+ // U+2028/2029 are legal in JSON but terminate a JS string literal, so escape them.
58
+ function toScript(cmd) {
59
+ const json = JSON.stringify(JSON.stringify(cmd)).split(LS).join('\\u2028').split(PS).join('\\u2029');
60
+ return `window.__cmd(${json});true;`;
61
+ }
62
+ exports.EpubReader = (0, react_1.forwardRef)(function EpubReader(props, ref) {
63
+ const { source, settings, systemDark = false, safeInsets = { top: 0, bottom: 0 }, highlights, menuItems } = props;
64
+ const web = (0, react_1.useRef)(null);
65
+ const cb = (0, react_1.useRef)(props);
66
+ cb.current = props;
67
+ const [engineReady, setEngineReady] = (0, react_1.useState)(false);
68
+ const opened = (0, react_1.useRef)(false);
69
+ const selection = (0, react_1.useRef)(null);
70
+ const metaRef = (0, react_1.useRef)({ title: '', author: '', language: '', publisher: '' });
71
+ const engineSettings = (0, react_1.useMemo)(() => (0, themes_1.toEngineSettings)(settings, systemDark, safeInsets),
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ [settings, systemDark, safeInsets.top, safeInsets.bottom]);
74
+ const send = (0, react_1.useCallback)((cmd) => {
75
+ web.current?.injectJavaScript(toScript(cmd));
76
+ }, []);
77
+ // open (or re-open) the book once the engine is up
78
+ const sourceKey = 'uri' in source ? source.uri : source.base64.length + ':' + source.base64.slice(0, 32);
79
+ (0, react_1.useEffect)(() => {
80
+ if (!engineReady)
81
+ return;
82
+ let cancelled = false;
83
+ opened.current = false;
84
+ (async () => {
85
+ try {
86
+ const data = 'uri' in source ? await uriToBase64(source.uri) : source.base64;
87
+ if (cancelled)
88
+ return;
89
+ send({
90
+ cmd: 'open',
91
+ data,
92
+ settings: engineSettings,
93
+ highlights: highlights || [],
94
+ location: cb.current.initialLocation
95
+ });
96
+ opened.current = true;
97
+ }
98
+ catch (e) {
99
+ cb.current.onError?.(String(e?.message || e));
100
+ }
101
+ })();
102
+ return () => {
103
+ cancelled = true;
104
+ };
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, [engineReady, sourceKey]);
107
+ (0, react_1.useEffect)(() => {
108
+ if (opened.current)
109
+ send({ cmd: 'settings', settings: engineSettings });
110
+ }, [engineSettings, send]);
111
+ (0, react_1.useEffect)(() => {
112
+ if (opened.current)
113
+ send({ cmd: 'highlights', list: highlights || [] });
114
+ }, [highlights, send]);
115
+ (0, react_1.useImperativeHandle)(ref, () => ({
116
+ next: () => send({ cmd: 'next' }),
117
+ prev: () => send({ cmd: 'prev' }),
118
+ goTo: (location) => send({ cmd: 'goto', location }),
119
+ search: (query, searchId) => send({ cmd: 'search', query, id: searchId }),
120
+ cancelSearch: (searchId) => send({ cmd: 'cancelSearch', id: searchId }),
121
+ showSearchResult: (r) => {
122
+ send({ cmd: 'goto', location: { spine: r.spine, offset: r.offset } });
123
+ send({ cmd: 'showHit', offset: r.offset, length: r.length });
124
+ },
125
+ clearSelection: () => {
126
+ selection.current = null;
127
+ send({ cmd: 'clearSelection' });
128
+ },
129
+ setLabels: (header, footer) => send({ cmd: 'labels', header, footer })
130
+ }), [send]);
131
+ const onMessage = (0, react_1.useCallback)((e) => {
132
+ let msg;
133
+ try {
134
+ msg = JSON.parse(e.nativeEvent.data);
135
+ }
136
+ catch {
137
+ return;
138
+ }
139
+ const p = cb.current;
140
+ const d = msg.data;
141
+ switch (msg.type) {
142
+ case 'ready':
143
+ setEngineReady(true);
144
+ break;
145
+ case 'metadata':
146
+ metaRef.current = d;
147
+ break;
148
+ case 'toc':
149
+ p.onReady?.({ metadata: metaRef.current, rtl: !!d.rtl });
150
+ p.onToc?.(d.items);
151
+ break;
152
+ case 'cover':
153
+ p.onCover?.(d.uri);
154
+ break;
155
+ case 'relocate':
156
+ p.onRelocate?.(d);
157
+ break;
158
+ case 'indexed':
159
+ p.onIndexed?.(d);
160
+ break;
161
+ case 'selection':
162
+ selection.current = d;
163
+ p.onSelection?.(d);
164
+ break;
165
+ case 'selectionClear':
166
+ selection.current = null;
167
+ p.onSelection?.(null);
168
+ break;
169
+ case 'highlightTap':
170
+ p.onHighlightTap?.(d.id, d.rect);
171
+ break;
172
+ case 'tap':
173
+ p.onTap?.();
174
+ break;
175
+ case 'pageTurn':
176
+ p.onPageTurn?.();
177
+ break;
178
+ case 'link':
179
+ p.onLink?.(d.href);
180
+ break;
181
+ case 'searchResults':
182
+ p.onSearchResults?.(d.id, d.results, d.done);
183
+ break;
184
+ case 'end':
185
+ p.onEndReached?.(d.edge);
186
+ break;
187
+ case 'error':
188
+ p.onError?.(d.message);
189
+ break;
190
+ }
191
+ }, []);
192
+ const bg = (0, themes_1.themeColors)(settings, systemDark).bg;
193
+ return (react_1.default.createElement(react_native_1.View, { style: [styles.fill, { backgroundColor: bg }] },
194
+ react_1.default.createElement(react_native_webview_1.WebView, { ref: web, style: [styles.fill, { backgroundColor: bg }], originWhitelist: ['*'], source: { html: html_1.READER_HTML }, onMessage: onMessage, javaScriptEnabled: true, domStorageEnabled: false, scrollEnabled: false, bounces: false, overScrollMode: "never", showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", allowsInlineMediaPlayback: true, hideKeyboardAccessoryView: true, setSupportMultipleWindows: false, menuItems: menuItems, onCustomMenuSelection: (e) => {
195
+ const key = e.nativeEvent.key;
196
+ cb.current.onMenuAction?.(key, selection.current);
197
+ }, onShouldStartLoadWithRequest: (req) => req.url === 'about:blank' || req.url.startsWith('data:') || req.url.startsWith('about:'), onError: (e) => cb.current.onError?.(e.nativeEvent.description) })));
198
+ });
199
+ const styles = react_native_1.StyleSheet.create({ fill: { flex: 1 } });
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import type { Bookmark, BookLocation, BookMetadata, EpubSource, Highlight, ReaderSettings, RelocateEvent } from './types';
3
+ export interface EpubReaderViewProps {
4
+ source: EpubSource;
5
+ /** shown in the Contents sheet until the book's own title is read */
6
+ title?: string;
7
+ safeInsets?: {
8
+ top: number;
9
+ bottom: number;
10
+ };
11
+ initialLocation?: BookLocation;
12
+ initialSettings?: Partial<ReaderSettings>;
13
+ initialHighlights?: Highlight[];
14
+ initialBookmarks?: Bookmark[];
15
+ onClose?: () => void;
16
+ onLocationChange?: (e: RelocateEvent) => void;
17
+ onHighlightsChange?: (list: Highlight[]) => void;
18
+ onBookmarksChange?: (list: Bookmark[]) => void;
19
+ onSettingsChange?: (s: ReaderSettings) => void;
20
+ onMetadata?: (m: BookMetadata) => void;
21
+ onCover?: (dataUri: string) => void;
22
+ onError?: (message: string) => void;
23
+ }
24
+ export declare function EpubReaderView(props: EpubReaderViewProps): React.JSX.Element;