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
@@ -0,0 +1,504 @@
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.EpubReaderView = EpubReaderView;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const EpubReader_1 = require("./EpubReader");
40
+ const themes_1 = require("./themes");
41
+ const AppearanceSheet_1 = require("./ui/AppearanceSheet");
42
+ const ContentsSheet_1 = require("./ui/ContentsSheet");
43
+ const CustomizeSheet_1 = require("./ui/CustomizeSheet");
44
+ const sound_1 = require("./sound");
45
+ const Glass_1 = require("./ui/Glass");
46
+ const HighlightMenu_1 = require("./ui/HighlightMenu");
47
+ const Icons_1 = require("./ui/Icons");
48
+ const LineGuide_1 = require("./ui/LineGuide");
49
+ const NoteEditor_1 = require("./ui/NoteEditor");
50
+ const SearchSheet_1 = require("./ui/SearchSheet");
51
+ const RESET_THEME = {
52
+ fontId: 'original',
53
+ bold: false,
54
+ customize: false,
55
+ lineHeight: 1.5,
56
+ letterSpacing: 0,
57
+ wordSpacing: 0,
58
+ margins: 0,
59
+ columns: 'auto',
60
+ justify: false
61
+ };
62
+ // expo-screen-orientation is optional (needed for the rotation lock button)
63
+ let ScreenOrientation = null;
64
+ try {
65
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
66
+ ScreenOrientation = require('expo-screen-orientation');
67
+ }
68
+ catch {
69
+ ScreenOrientation = null;
70
+ }
71
+ const DIM_LABELS = [
72
+ { id: 'high', label: 'High' },
73
+ { id: 'medium', label: 'Medium' },
74
+ { id: 'low', label: 'Low' },
75
+ { id: 'none', label: 'None' }
76
+ ];
77
+ const SAMPLE = 'The reader turned the page slowly, listening to the wind move across the empty hills.';
78
+ const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
79
+ function defaultInsets() {
80
+ return react_native_1.Platform.OS === 'ios' ? { top: 50, bottom: 30 } : { top: react_native_1.StatusBar.currentHeight || 24, bottom: 0 };
81
+ }
82
+ function EpubReaderView(props) {
83
+ const safe = props.safeInsets || defaultInsets();
84
+ const screen = (0, react_native_1.useWindowDimensions)();
85
+ const systemDark = (0, react_native_1.useColorScheme)() === 'dark';
86
+ const reader = (0, react_1.useRef)(null);
87
+ const [settings, setSettings] = (0, react_1.useState)({ ...themes_1.DEFAULT_SETTINGS, ...props.initialSettings });
88
+ const [highlights, setHighlights] = (0, react_1.useState)(props.initialHighlights || []);
89
+ const [bookmarks, setBookmarks] = (0, react_1.useState)(props.initialBookmarks || []);
90
+ const [toc, setToc] = (0, react_1.useState)([]);
91
+ const [meta, setMeta] = (0, react_1.useState)(null);
92
+ const [rtl, setRtl] = (0, react_1.useState)(false);
93
+ const [loc, setLoc] = (0, react_1.useState)(null);
94
+ const [index, setIndex] = (0, react_1.useState)(null);
95
+ const [cpp, setCpp] = (0, react_1.useState)(1200); // characters per page, measured from the chapters seen so far
96
+ // reading starts with nothing but the running header/footer; a tap shows the close button and the menu button
97
+ const [chrome, setChrome] = (0, react_1.useState)(false);
98
+ const [menuOpen, setMenuOpen] = (0, react_1.useState)(false);
99
+ const [pillW, setPillW] = (0, react_1.useState)(0);
100
+ const [lineGuide, setLineGuide] = (0, react_1.useState)(false);
101
+ const [dimLevel, setDimLevel] = (0, react_1.useState)('high');
102
+ const [lineMenu, setLineMenu] = (0, react_1.useState)(false);
103
+ const [rotLocked, setRotLocked] = (0, react_1.useState)(false);
104
+ const [returnTo, setReturnTo] = (0, react_1.useState)(null);
105
+ const [sheet, setSheet] = (0, react_1.useState)(null);
106
+ const [active, setActive] = (0, react_1.useState)(null);
107
+ const [selection, setSelection] = (0, react_1.useState)(null);
108
+ const [noteFor, setNoteFor] = (0, react_1.useState)(null);
109
+ const [results, setResults] = (0, react_1.useState)([]);
110
+ const [searching, setSearching] = (0, react_1.useState)(false);
111
+ const [error, setError] = (0, react_1.useState)(null);
112
+ const searchId = (0, react_1.useRef)(0);
113
+ const seen = (0, react_1.useRef)({});
114
+ const layoutKey = (0, react_1.useRef)('');
115
+ const dark = (0, themes_1.isDark)(settings, systemDark);
116
+ const page = (0, themes_1.themeColors)(settings, systemDark);
117
+ const colors = (0, react_1.useMemo)(() => (0, themes_1.chromeColors)(page, dark), [page, dark]);
118
+ // ----- chrome visibility animation
119
+ const fade = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
120
+ (0, react_1.useEffect)(() => {
121
+ react_native_1.Animated.timing(fade, { toValue: chrome ? 1 : 0, duration: 180, useNativeDriver: true }).start();
122
+ }, [chrome, fade]);
123
+ const fadeOut = fade.interpolate({ inputRange: [0, 1], outputRange: [1, 0] });
124
+ const menuFade = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
125
+ (0, react_1.useEffect)(() => {
126
+ react_native_1.Animated.timing(menuFade, { toValue: menuOpen ? 1 : 0, duration: 160, useNativeDriver: true }).start();
127
+ }, [menuOpen, menuFade]);
128
+ (0, react_1.useEffect)(() => {
129
+ if (!chrome)
130
+ setMenuOpen(false);
131
+ }, [chrome]);
132
+ // ----- persistence helpers
133
+ const updateHighlights = (0, react_1.useCallback)((fn) => {
134
+ setHighlights((prev) => {
135
+ const next = fn(prev);
136
+ props.onHighlightsChange?.(next);
137
+ return next;
138
+ });
139
+ },
140
+ // eslint-disable-next-line react-hooks/exhaustive-deps
141
+ [props.onHighlightsChange]);
142
+ const updateBookmarks = (0, react_1.useCallback)((fn) => {
143
+ setBookmarks((prev) => {
144
+ const next = fn(prev);
145
+ props.onBookmarksChange?.(next);
146
+ return next;
147
+ });
148
+ },
149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
150
+ [props.onBookmarksChange]);
151
+ const changeSettings = (patch) => {
152
+ const next = { ...settings, ...patch };
153
+ setSettings(next);
154
+ props.onSettingsChange?.(next);
155
+ };
156
+ // ----- book page numbers (estimated from characters per page)
157
+ const onRelocate = (e) => {
158
+ const key = [settings.fontSize, settings.fontId, settings.lineHeight, settings.letterSpacing, settings.wordSpacing, settings.margins, settings.customize, settings.columns, settings.bold, settings.pageTurn === 'scroll'].join('|');
159
+ if (key !== layoutKey.current) {
160
+ layoutKey.current = key;
161
+ seen.current = {};
162
+ }
163
+ seen.current[e.spine] = { len: e.textLength, pc: e.pageCount };
164
+ let len = 0;
165
+ let pc = 0;
166
+ Object.values(seen.current).forEach((v) => {
167
+ len += v.len;
168
+ pc += v.pc;
169
+ });
170
+ const next = pc > 0 && len > 0 ? len / pc : 1200;
171
+ setCpp((old) => (Math.abs(old - next) / next > 0.01 ? next : old));
172
+ setLoc(e);
173
+ setActive(null);
174
+ props.onLocationChange?.(e);
175
+ };
176
+ const pageOf = (0, react_1.useCallback)((spine, offset) => {
177
+ if (!index)
178
+ return spine + 1;
179
+ return Math.round(((index.cum[spine] || 0) + offset) / cpp) + 1;
180
+ }, [index, cpp]);
181
+ const totalPages = index ? Math.max(1, Math.round(index.totalLength / cpp)) : loc ? loc.pageCount * loc.spineCount : 1;
182
+ const curPage = loc ? Math.min(totalPages, pageOf(loc.spine, loc.offset)) : 1;
183
+ const percent = Math.round((loc?.progress ?? 0) * 100);
184
+ const pagesLeft = loc ? Math.max(0, loc.pageCount - loc.page - 1) : 0;
185
+ // header / footer drawn on the page: chapter title + page number, or "N pages left" + "x of y" while the controls are shown
186
+ (0, react_1.useEffect)(() => {
187
+ if (!loc)
188
+ return;
189
+ const header = chrome ? (pagesLeft === 0 ? 'Last page in chapter' : `${pagesLeft} page${pagesLeft === 1 ? '' : 's'} left in chapter`) : loc.chapter;
190
+ const footer = chrome ? `${curPage} of ${totalPages}` : `${curPage}`;
191
+ reader.current?.setLabels(header, footer);
192
+ }, [chrome, loc, curPage, totalPages, pagesLeft]);
193
+ // ----- bookmarks
194
+ const bookmarkHere = (0, react_1.useMemo)(() => (loc ? bookmarks.filter((b) => b.spine === loc.spine && b.offset >= loc.offset && b.offset < Math.max(loc.end, loc.offset + 1)) : []), [bookmarks, loc]);
195
+ const toggleBookmark = () => {
196
+ if (!loc)
197
+ return;
198
+ if (bookmarkHere.length) {
199
+ const ids = new Set(bookmarkHere.map((b) => b.id));
200
+ updateBookmarks((prev) => prev.filter((b) => !ids.has(b.id)));
201
+ }
202
+ else {
203
+ updateBookmarks((prev) => [
204
+ ...prev,
205
+ { id: uid(), spine: loc.spine, offset: loc.offset, chapter: loc.chapter, excerpt: loc.excerpt, progress: loc.progress, createdAt: Date.now() }
206
+ ]);
207
+ }
208
+ };
209
+ // ----- highlights
210
+ const createHighlight = (sel, withNote) => {
211
+ setSelection(null);
212
+ const h = {
213
+ id: uid(),
214
+ spine: sel.spine,
215
+ start: sel.start,
216
+ end: sel.end,
217
+ text: sel.text.replace(/\s+/g, ' ').trim(),
218
+ color: 'yellow',
219
+ chapter: loc?.chapter,
220
+ createdAt: Date.now()
221
+ };
222
+ updateHighlights((prev) => [...prev, h]);
223
+ reader.current?.clearSelection();
224
+ if (withNote)
225
+ setNoteFor(h.id);
226
+ else
227
+ setActive({ id: h.id, rect: sel.rect });
228
+ };
229
+ const patchHighlight = (id, patch) => updateHighlights((prev) => prev.map((h) => (h.id === id ? { ...h, ...patch } : h)));
230
+ const removeHighlight = (id) => {
231
+ updateHighlights((prev) => prev.filter((h) => h.id !== id));
232
+ setActive(null);
233
+ };
234
+ const activeHighlight = active ? highlights.find((h) => h.id === active.id) : undefined;
235
+ const noteHighlight = noteFor ? highlights.find((h) => h.id === noteFor) : undefined;
236
+ // ----- search
237
+ const runSearch = (q) => {
238
+ const id = ++searchId.current;
239
+ setResults([]);
240
+ if (q.trim().length < 2) {
241
+ setSearching(false);
242
+ reader.current?.cancelSearch(id);
243
+ return;
244
+ }
245
+ setSearching(true);
246
+ reader.current?.search(q, id);
247
+ };
248
+ const closeSheet = () => setSheet(null);
249
+ // remember where we were so the "back" pill can return after a jump (contents, search, bookmark ...)
250
+ const jump = (to) => {
251
+ if (loc)
252
+ setReturnTo({ spine: loc.spine, offset: loc.offset, page: curPage });
253
+ setChrome(false);
254
+ reader.current?.goTo(to);
255
+ };
256
+ // two modals cannot be presented at once on iOS, so switch sheets after the first one is gone
257
+ const switchSheet = (to) => {
258
+ setSheet(null);
259
+ setTimeout(() => setSheet(to), 380);
260
+ };
261
+ const toggleRotationLock = async () => {
262
+ if (!ScreenOrientation) {
263
+ react_native_1.Alert.alert('Rotation lock', 'Install expo-screen-orientation to enable the rotation lock.');
264
+ return;
265
+ }
266
+ try {
267
+ if (rotLocked) {
268
+ await ScreenOrientation.unlockAsync();
269
+ setRotLocked(false);
270
+ }
271
+ else {
272
+ const o = await ScreenOrientation.getOrientationAsync();
273
+ const O = ScreenOrientation.Orientation;
274
+ const L = ScreenOrientation.OrientationLock;
275
+ await ScreenOrientation.lockAsync(o === O.LANDSCAPE_LEFT ? L.LANDSCAPE_LEFT : o === O.LANDSCAPE_RIGHT ? L.LANDSCAPE_RIGHT : L.PORTRAIT_UP);
276
+ setRotLocked(true);
277
+ }
278
+ }
279
+ catch {
280
+ // not supported on this device
281
+ }
282
+ };
283
+ (0, react_1.useEffect)(() => () => {
284
+ if (ScreenOrientation)
285
+ ScreenOrientation.unlockAsync().catch(() => undefined);
286
+ }, []);
287
+ const onLineGuideButton = () => {
288
+ setMenuOpen(false);
289
+ if (!lineGuide) {
290
+ setLineGuide(true);
291
+ setChrome(false);
292
+ }
293
+ else
294
+ setLineMenu(true);
295
+ };
296
+ const share = () => {
297
+ const text = [loc?.excerpt, meta?.title && `— ${meta.title}`].filter(Boolean).join(' ');
298
+ react_native_1.Share.share({ message: text || meta?.title || '' }).catch(() => undefined);
299
+ };
300
+ const progressFill = Math.max(0, Math.min(1, loc?.progress ?? 0));
301
+ const tx = colors.text;
302
+ return (react_1.default.createElement(react_native_1.View, { style: [styles.root, { backgroundColor: page.bg }] },
303
+ react_1.default.createElement(EpubReader_1.EpubReader, { ref: reader, source: props.source, settings: settings, systemDark: systemDark, safeInsets: safe, initialLocation: props.initialLocation, highlights: highlights, onReady: ({ metadata, rtl: isRtl }) => {
304
+ setMeta(metadata);
305
+ setRtl(isRtl);
306
+ props.onMetadata?.(metadata);
307
+ }, onToc: setToc, onCover: props.onCover, onIndexed: setIndex, onRelocate: onRelocate, onPageTurn: () => {
308
+ if (settings.pageSound)
309
+ (0, sound_1.playPageTurn)();
310
+ }, onTap: () => {
311
+ if (active)
312
+ setActive(null);
313
+ else if (menuOpen)
314
+ setMenuOpen(false);
315
+ else
316
+ setChrome((c) => !c);
317
+ }, onHighlightTap: (id, rect) => setActive({ id, rect }), onSelection: (sel) => {
318
+ setSelection(sel);
319
+ if (sel) {
320
+ setChrome(false);
321
+ setActive(null);
322
+ }
323
+ }, onLink: (href) => react_native_1.Linking.openURL(href).catch(() => undefined), onSearchResults: (id, found, done) => {
324
+ if (id !== searchId.current)
325
+ return;
326
+ setResults((prev) => prev.concat(found));
327
+ if (done)
328
+ setSearching(false);
329
+ }, onError: (m) => {
330
+ setError(m);
331
+ props.onError?.(m);
332
+ } }),
333
+ react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [react_native_1.StyleSheet.absoluteFill, { backgroundColor: '#000000', opacity: (1 - settings.brightness) * 0.75 }] }),
334
+ lineGuide && (react_1.default.createElement(LineGuide_1.LineGuide, { bg: page.bg, dark: dark, dim: dimLevel, lineHeight: settings.fontSize * (settings.customize ? settings.lineHeight : 1.5), minY: safe.top + 30, maxY: screen.height - safe.bottom - 30 })),
335
+ bookmarkHere.length > 0 && (react_1.default.createElement(react_native_1.Animated.View, { pointerEvents: "none", style: [styles.ribbon, { right: 22, opacity: fadeOut }] },
336
+ react_1.default.createElement(react_native_1.View, { style: styles.ribbonBody }),
337
+ react_1.default.createElement(react_native_1.View, { style: styles.ribbonNotch }))),
338
+ react_1.default.createElement(react_native_1.Animated.View, { pointerEvents: chrome ? 'auto' : 'none', style: [styles.closeWrap, { top: safe.top + 2, opacity: fade }] },
339
+ react_1.default.createElement(react_native_1.Pressable, { onPress: props.onClose, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Close book" },
340
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.circle },
341
+ react_1.default.createElement(Icons_1.Icon, { name: "close", color: tx, size: 22 })))),
342
+ returnTo && (react_1.default.createElement(react_native_1.Animated.View, { pointerEvents: chrome ? 'auto' : 'none', style: [styles.returnWrap, { top: safe.top + 2, opacity: fade }] },
343
+ react_1.default.createElement(react_native_1.Pressable, { onPress: () => {
344
+ reader.current?.goTo({ spine: returnTo.spine, offset: returnTo.offset });
345
+ setReturnTo(null);
346
+ }, hitSlop: 8, accessibilityRole: "button", accessibilityLabel: `Return to page ${returnTo.page}` },
347
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.returnPill },
348
+ react_1.default.createElement(Icons_1.Icon, { name: "undo", color: tx, size: 22 }),
349
+ react_1.default.createElement(react_native_1.Text, { style: { color: tx, fontSize: 20, marginStart: 8 } }, returnTo.page))))),
350
+ react_1.default.createElement(react_native_1.Animated.View, { pointerEvents: chrome && !menuOpen ? 'auto' : 'none', style: [styles.menuBtnWrap, { bottom: safe.bottom + 14, opacity: react_native_1.Animated.multiply(fade, react_native_1.Animated.subtract(1, menuFade)) }] },
351
+ react_1.default.createElement(react_native_1.Pressable, { onPress: () => setMenuOpen(true), accessibilityRole: "button", accessibilityLabel: "Reader menu" },
352
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.menuBtn },
353
+ react_1.default.createElement(Icons_1.Icon, { name: "list", color: tx, size: 24 })))),
354
+ react_1.default.createElement(react_native_1.Animated.View, { pointerEvents: chrome && menuOpen ? 'box-none' : 'none', style: [styles.controls, { bottom: safe.bottom + 44, opacity: menuFade }] },
355
+ react_1.default.createElement(react_native_1.Pressable, { onPress: () => {
356
+ setMenuOpen(false);
357
+ setSheet('contents');
358
+ }, accessibilityRole: "button", accessibilityLabel: "Contents", style: styles.pillWrap },
359
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.pill },
360
+ react_1.default.createElement(react_native_1.View, { style: styles.pillRow, onLayout: (e) => setPillW(e.nativeEvent.layout.width) },
361
+ react_1.default.createElement(react_native_1.Text, { style: [styles.pillText, { color: tx }] },
362
+ "Contents \u2022 ",
363
+ percent,
364
+ "%"),
365
+ react_1.default.createElement(Icons_1.Icon, { name: "list", color: tx, size: 24 })),
366
+ react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.fill, { width: progressFill * pillW, backgroundColor: dark ? 'rgba(255,255,255,0.35)' : '#1c1408', [rtl ? 'right' : 'left']: 0 }] },
367
+ react_1.default.createElement(react_native_1.View, { style: [styles.pillRow, { position: 'absolute', top: 0, width: pillW, [rtl ? 'right' : 'left']: 0 }] },
368
+ react_1.default.createElement(react_native_1.Text, { style: [styles.pillText, { color: dark ? tx : '#ffffff' }] },
369
+ "Contents \u2022 ",
370
+ percent,
371
+ "%"),
372
+ react_1.default.createElement(Icons_1.Icon, { name: "list", color: dark ? tx : '#ffffff', size: 24 }))))),
373
+ react_1.default.createElement(react_native_1.Pressable, { onPress: () => {
374
+ setMenuOpen(false);
375
+ setSheet('search');
376
+ }, accessibilityRole: "button", accessibilityLabel: "Search book", style: styles.pillWrap },
377
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.pill },
378
+ react_1.default.createElement(react_native_1.View, { style: styles.pillRow },
379
+ react_1.default.createElement(react_native_1.Text, { style: [styles.pillText, { color: tx }] }, "Search Book"),
380
+ react_1.default.createElement(Icons_1.Icon, { name: "search", color: tx, size: 24 })))),
381
+ react_1.default.createElement(react_native_1.Pressable, { onPress: () => {
382
+ setMenuOpen(false);
383
+ setSheet('appearance');
384
+ }, accessibilityRole: "button", accessibilityLabel: "Themes and settings", style: styles.pillWrap },
385
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.pill },
386
+ react_1.default.createElement(react_native_1.View, { style: styles.pillRow },
387
+ react_1.default.createElement(react_native_1.Text, { style: [styles.pillText, { color: tx }] }, "Themes & Settings"),
388
+ react_1.default.createElement(Icons_1.Icon, { name: "textSize", color: tx, size: 26 })))),
389
+ react_1.default.createElement(react_native_1.View, { style: styles.roundRow },
390
+ react_1.default.createElement(react_native_1.Pressable, { onPress: share, style: styles.roundWrap, accessibilityRole: "button", accessibilityLabel: "Share" },
391
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.round },
392
+ react_1.default.createElement(Icons_1.Icon, { name: "share", color: tx, size: 24 }))),
393
+ react_1.default.createElement(react_native_1.Pressable, { onPress: toggleRotationLock, style: styles.roundWrap, accessibilityRole: "button", accessibilityLabel: "Rotation lock", accessibilityState: { selected: rotLocked } },
394
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: rotLocked ? (dark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.18)') : colors.glass, border: colors.glassBorder, style: styles.round },
395
+ react_1.default.createElement(Icons_1.Icon, { name: "rotationLock", color: tx, size: 26, active: rotLocked }))),
396
+ react_1.default.createElement(react_native_1.Pressable, { onPress: onLineGuideButton, style: styles.roundWrap, accessibilityRole: "button", accessibilityLabel: "Line guide", accessibilityState: { selected: lineGuide } },
397
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: lineGuide ? (dark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.18)') : colors.glass, border: colors.glassBorder, style: styles.round },
398
+ react_1.default.createElement(Icons_1.Icon, { name: "lineGuide", color: tx, size: 26, active: lineGuide }))),
399
+ react_1.default.createElement(react_native_1.Pressable, { onPress: toggleBookmark, style: styles.roundWrap, accessibilityRole: "button", accessibilityLabel: "Bookmark this page", accessibilityState: { selected: bookmarkHere.length > 0 } },
400
+ react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.glass, border: colors.glassBorder, style: styles.round },
401
+ react_1.default.createElement(Icons_1.Icon, { name: "bookmark", color: tx, size: 24, active: bookmarkHere.length > 0 }))))),
402
+ selection && !active && !noteFor && (() => {
403
+ const w = 214;
404
+ const r = selection.rect;
405
+ const left = Math.max(8, Math.min(screen.width - w - 8, r.x + r.width / 2 - w / 2));
406
+ // the system callout (Copy / Look Up) sits above the selection, so ours goes below it
407
+ const below = r.y + r.height + 16;
408
+ const top = below + 54 > screen.height - safe.bottom ? Math.max(safe.top + 8, r.y - 120) : below;
409
+ return (react_1.default.createElement(Glass_1.Glass, { dark: dark, background: colors.sheetGlass, border: colors.glassBorder, intensity: 55, style: [styles.selBar, { left, top, width: w }] },
410
+ react_1.default.createElement(react_native_1.Pressable, { style: styles.selBtn, onPress: () => createHighlight(selection, false), accessibilityRole: "button", accessibilityLabel: "Highlight" },
411
+ react_1.default.createElement(Icons_1.Icon, { name: "highlighter", color: tx, size: 22 }),
412
+ react_1.default.createElement(react_native_1.Text, { style: [styles.selText, { color: tx }] }, "Highlight")),
413
+ react_1.default.createElement(react_native_1.View, { style: [styles.selSep, { backgroundColor: colors.separator }] }),
414
+ react_1.default.createElement(react_native_1.Pressable, { style: styles.selBtn, onPress: () => createHighlight(selection, true), accessibilityRole: "button", accessibilityLabel: "Add note" },
415
+ react_1.default.createElement(Icons_1.Icon, { name: "pencil", color: tx, size: 22 }),
416
+ react_1.default.createElement(react_native_1.Text, { style: [styles.selText, { color: tx }] }, "Note"))));
417
+ })(),
418
+ activeHighlight && active && (react_1.default.createElement(HighlightMenu_1.HighlightMenu, { highlight: activeHighlight, rect: active.rect, screen: screen, colors: colors, onColor: (c) => patchHighlight(activeHighlight.id, { color: c }), onToggleUnderline: () => patchHighlight(activeHighlight.id, { style: activeHighlight.style === 'underline' ? 'highlight' : 'underline' }), onNote: () => {
419
+ setNoteFor(activeHighlight.id);
420
+ setActive(null);
421
+ }, onDelete: () => removeHighlight(activeHighlight.id), onDismiss: () => setActive(null) })),
422
+ error && (react_1.default.createElement(react_native_1.View, { style: [styles.error, { backgroundColor: colors.sheet }] },
423
+ react_1.default.createElement(react_native_1.Text, { style: { color: colors.text, fontSize: 15, textAlign: 'center' } }, error),
424
+ react_1.default.createElement(react_native_1.Pressable, { onPress: () => setError(null), style: { marginTop: 12 } },
425
+ react_1.default.createElement(react_native_1.Text, { style: { color: colors.accent, fontSize: 16 } }, "OK")))),
426
+ lineMenu && (react_1.default.createElement(react_1.default.Fragment, null,
427
+ react_1.default.createElement(react_native_1.Pressable, { style: react_native_1.StyleSheet.absoluteFill, onPress: () => setLineMenu(false), accessibilityLabel: "Dismiss" }),
428
+ react_1.default.createElement(react_native_1.View, { style: [styles.lineMenu, { bottom: safe.bottom + 20, backgroundColor: colors.dark ? '#4a4742' : '#ffffff', borderColor: colors.glassBorder }] },
429
+ react_1.default.createElement(react_native_1.Text, { style: [styles.lineMenuHead, { color: colors.subtext }] }, "Background Dimming"),
430
+ DIM_LABELS.map((d) => (react_1.default.createElement(react_native_1.Pressable, { key: d.id, style: styles.lineItem, onPress: () => {
431
+ setDimLevel(d.id);
432
+ setLineMenu(false);
433
+ } },
434
+ react_1.default.createElement(react_native_1.View, { style: styles.lineCheck }, dimLevel === d.id && react_1.default.createElement(Icons_1.Icon, { name: "check", color: tx, size: 20 })),
435
+ react_1.default.createElement(react_native_1.Text, { style: { color: tx, fontSize: 19 } }, d.label)))),
436
+ react_1.default.createElement(react_native_1.View, { style: [styles.lineSep, { backgroundColor: colors.separator }] }),
437
+ react_1.default.createElement(react_native_1.Pressable, { style: styles.lineItem, onPress: () => {
438
+ setLineGuide(false);
439
+ setLineMenu(false);
440
+ } },
441
+ react_1.default.createElement(react_native_1.View, { style: styles.lineCheck }),
442
+ react_1.default.createElement(react_native_1.Text, { style: { color: tx, fontSize: 19 } }, "Turn Off Line Guide"))))),
443
+ react_1.default.createElement(ContentsSheet_1.ContentsSheet, { visible: sheet === 'contents', colors: colors, title: meta?.title || props.title || '', subtitle: `Page ${curPage} of ${totalPages}`, toc: toc, currentSpine: loc?.spine ?? 0, pageOf: pageOf, bookmarks: bookmarks, highlights: highlights, onClose: closeSheet, onGoToc: (item) => {
444
+ closeSheet();
445
+ jump({ spine: item.spine, fragment: item.fragment });
446
+ }, onGoBookmark: (b) => {
447
+ closeSheet();
448
+ jump({ spine: b.spine, offset: b.offset });
449
+ }, onGoHighlight: (h) => {
450
+ closeSheet();
451
+ jump({ spine: h.spine, offset: h.start });
452
+ }, onDeleteBookmark: (id) => updateBookmarks((prev) => prev.filter((b) => b.id !== id)), onDeleteHighlight: (id) => updateHighlights((prev) => prev.filter((h) => h.id !== id)) }),
453
+ react_1.default.createElement(SearchSheet_1.SearchSheet, { visible: sheet === 'search', colors: colors, results: results, searching: searching, onClose: closeSheet, onSearch: runSearch, onPick: (r) => {
454
+ closeSheet();
455
+ if (loc)
456
+ setReturnTo({ spine: loc.spine, offset: loc.offset, page: curPage });
457
+ setChrome(false);
458
+ reader.current?.showSearchResult(r);
459
+ } }),
460
+ react_1.default.createElement(AppearanceSheet_1.AppearanceSheet, { visible: sheet === 'appearance', colors: colors, settings: settings, systemDark: systemDark, onChange: changeSettings, onCustomize: () => switchSheet('customize'), onClose: closeSheet }),
461
+ react_1.default.createElement(CustomizeSheet_1.CustomizeSheet, { visible: sheet === 'customize', colors: colors, page: page, settings: settings, sample: loc?.excerpt || SAMPLE, onChange: changeSettings, onReset: () => changeSettings(RESET_THEME), onClose: () => switchSheet('appearance') }),
462
+ react_1.default.createElement(NoteEditor_1.NoteEditor, { visible: !!noteHighlight, quote: noteHighlight?.text || '', initial: noteHighlight?.note || '', colors: colors, onCancel: () => setNoteFor(null), onSave: (text) => {
463
+ if (noteFor)
464
+ patchHighlight(noteFor, { note: text || undefined });
465
+ setNoteFor(null);
466
+ } })));
467
+ }
468
+ const styles = react_native_1.StyleSheet.create({
469
+ root: { flex: 1 },
470
+ closeWrap: { position: 'absolute', right: 16 },
471
+ circle: { width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center' },
472
+ controls: { position: 'absolute', right: 16, width: '68%' },
473
+ selBar: { position: 'absolute', height: 48, borderRadius: 24, flexDirection: 'row', alignItems: 'center' },
474
+ selBtn: { flex: 1, height: '100%', flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6 },
475
+ selText: { fontSize: 15, fontWeight: '600' },
476
+ selSep: { width: react_native_1.StyleSheet.hairlineWidth, height: 24 },
477
+ lineMenu: {
478
+ position: 'absolute', left: 16, width: 290, borderRadius: 28, paddingVertical: 10, borderWidth: react_native_1.StyleSheet.hairlineWidth,
479
+ shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 16, shadowOffset: { width: 0, height: 6 }, elevation: 12
480
+ },
481
+ lineMenuHead: { fontSize: 15, fontWeight: '600', marginStart: 52, marginTop: 8, marginBottom: 4 },
482
+ lineItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 18 },
483
+ lineCheck: { width: 34 },
484
+ lineSep: { height: react_native_1.StyleSheet.hairlineWidth, marginVertical: 6, marginHorizontal: 18 },
485
+ menuBtnWrap: { position: 'absolute', right: 16 },
486
+ menuBtn: { width: 54, height: 54, borderRadius: 27, alignItems: 'center', justifyContent: 'center' },
487
+ returnWrap: { position: 'absolute', left: 16 },
488
+ returnPill: { height: 46, borderRadius: 23, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16 },
489
+ pillRow: { height: 50, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 22 },
490
+ pillWrap: { marginBottom: 8 },
491
+ pill: { height: 50, borderRadius: 25 },
492
+ pillText: { fontSize: 17, fontWeight: '500' },
493
+ fill: { position: 'absolute', top: 0, bottom: 0, overflow: 'hidden' },
494
+ roundRow: { flexDirection: 'row', gap: 8 },
495
+ roundWrap: { flex: 1 },
496
+ round: { height: 50, borderRadius: 25, alignItems: 'center', justifyContent: 'center' },
497
+ ribbon: { position: 'absolute', top: 0, width: 16 },
498
+ ribbonBody: { height: 26, backgroundColor: '#ff3b30' },
499
+ ribbonNotch: {
500
+ width: 0, height: 0, borderLeftWidth: 8, borderRightWidth: 8, borderBottomWidth: 7,
501
+ borderLeftColor: '#ff3b30', borderRightColor: '#ff3b30', borderBottomColor: 'transparent'
502
+ },
503
+ error: { position: 'absolute', left: 24, right: 24, top: '40%', padding: 20, borderRadius: 14, alignItems: 'center', elevation: 10 }
504
+ });
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ export declare const READER_HTML: string;