@somecat/epub-reader 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.
package/dist/react.js ADDED
@@ -0,0 +1,1142 @@
1
+ import { forwardRef, useRef, useState, useMemo, useEffect, useCallback, useImperativeHandle } from 'react';
2
+ import 'foliate-js/view.js';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/react/EBookReader.tsx
6
+ var getContentCSS = (fontSize, isDark, extraCSS) => `
7
+ @namespace epub "http://www.idpf.org/2007/ops";
8
+ html {
9
+ color-scheme: ${isDark ? "dark" : "light"} !important;
10
+ }
11
+ body {
12
+ background-color: transparent !important;
13
+ color: ${isDark ? "#e0e0e0" : "black"} !important;
14
+ font-size: ${fontSize}% !important;
15
+ }
16
+ p {
17
+ line-height: 1.6;
18
+ margin-bottom: 1em;
19
+ }
20
+ a {
21
+ color: ${isDark ? "#64b5f6" : "#2563eb"} !important;
22
+ }
23
+ img {
24
+ max-width: 100%;
25
+ height: auto;
26
+ object-fit: contain;
27
+ ${isDark ? "filter: brightness(0.8) contrast(1.2);" : ""}
28
+ }
29
+ ${extraCSS ?? ""}
30
+ `;
31
+ function createEBookReader(container, options = {}) {
32
+ if (!container) throw new Error("container is required");
33
+ if (!customElements.get("foliate-view")) throw new Error("foliate-view is not defined");
34
+ const {
35
+ darkMode: initialDarkMode = false,
36
+ fontSize: initialFontSize = 100,
37
+ extraContentCSS,
38
+ onReady,
39
+ onError,
40
+ onProgress,
41
+ onToc,
42
+ onSearchProgress,
43
+ onContentLoad
44
+ } = options;
45
+ let destroyed = false;
46
+ let toc = [];
47
+ let fontSize = initialFontSize;
48
+ let darkMode = initialDarkMode;
49
+ let searchToken = 0;
50
+ container.innerHTML = "";
51
+ const viewer = document.createElement("foliate-view");
52
+ viewer.style.display = "block";
53
+ viewer.style.width = "100%";
54
+ viewer.style.height = "100%";
55
+ viewer.setAttribute("margin", "48");
56
+ viewer.setAttribute("gap", "0.07");
57
+ const applyStyles = () => {
58
+ if (destroyed) return;
59
+ if (!viewer.renderer?.setStyles) return;
60
+ viewer.renderer.setStyles(getContentCSS(fontSize, darkMode, extraContentCSS));
61
+ requestAnimationFrame(() => {
62
+ setTimeout(() => {
63
+ if (destroyed) return;
64
+ viewer.renderer?.render?.();
65
+ viewer.renderer?.expand?.();
66
+ }, 50);
67
+ });
68
+ };
69
+ const handleLoad = (e) => {
70
+ applyStyles();
71
+ const detail = e.detail;
72
+ if (detail?.doc) onContentLoad?.(detail.doc);
73
+ };
74
+ const handleRelocate = (e) => {
75
+ const detail = e.detail;
76
+ onProgress?.(detail);
77
+ };
78
+ viewer.addEventListener("load", handleLoad);
79
+ viewer.addEventListener("relocate", handleRelocate);
80
+ container.appendChild(viewer);
81
+ const handle = {
82
+ async open(file) {
83
+ if (destroyed) return;
84
+ if (!file) return;
85
+ try {
86
+ viewer.clearSearch?.();
87
+ searchToken++;
88
+ await viewer.open?.(file);
89
+ const nextToc = viewer.book?.toc ?? [];
90
+ toc = nextToc;
91
+ onToc?.(toc);
92
+ await viewer.init?.({ showTextStart: true });
93
+ applyStyles();
94
+ } catch (error) {
95
+ onError?.(error);
96
+ throw error;
97
+ }
98
+ },
99
+ destroy() {
100
+ if (destroyed) return;
101
+ destroyed = true;
102
+ searchToken++;
103
+ viewer.removeEventListener("load", handleLoad);
104
+ viewer.removeEventListener("relocate", handleRelocate);
105
+ container.innerHTML = "";
106
+ },
107
+ prevPage() {
108
+ viewer.goLeft?.();
109
+ },
110
+ nextPage() {
111
+ viewer.goRight?.();
112
+ },
113
+ prevSection() {
114
+ viewer.renderer?.prevSection?.();
115
+ },
116
+ nextSection() {
117
+ viewer.renderer?.nextSection?.();
118
+ },
119
+ goTo(target) {
120
+ if (!target) return;
121
+ viewer.goTo?.(target);
122
+ },
123
+ goToFraction(fraction) {
124
+ const safe = Math.min(1, Math.max(0, fraction));
125
+ viewer.goToFraction?.(safe);
126
+ },
127
+ setDarkMode(nextDarkMode) {
128
+ darkMode = nextDarkMode;
129
+ applyStyles();
130
+ },
131
+ setFontSize(nextFontSize) {
132
+ const safe = Math.min(300, Math.max(50, nextFontSize));
133
+ fontSize = safe;
134
+ applyStyles();
135
+ },
136
+ async search(query, opts = {}) {
137
+ const normalized = query.trim();
138
+ if (!normalized) {
139
+ viewer.clearSearch?.();
140
+ return [];
141
+ }
142
+ const token = ++searchToken;
143
+ const results = [];
144
+ try {
145
+ for await (const item of viewer.search?.({
146
+ query: normalized,
147
+ matchCase: Boolean(opts.matchCase),
148
+ matchWholeWords: Boolean(opts.wholeWords),
149
+ matchDiacritics: Boolean(opts.matchDiacritics)
150
+ }) ?? []) {
151
+ if (destroyed || token !== searchToken) return results;
152
+ if (item === "done") {
153
+ onSearchProgress?.({ done: true, progress: 1 });
154
+ break;
155
+ }
156
+ if (typeof item === "object" && item && "progress" in item) {
157
+ const progress = item.progress;
158
+ if (typeof progress === "number") onSearchProgress?.({ progress });
159
+ continue;
160
+ }
161
+ const anyItem = item;
162
+ if (anyItem?.subitems?.length) {
163
+ for (const sub of anyItem.subitems) {
164
+ results.push({
165
+ label: anyItem.label,
166
+ cfi: sub?.cfi,
167
+ excerpt: sub?.excerpt,
168
+ title: sub?.title
169
+ });
170
+ }
171
+ } else if (anyItem?.cfi) {
172
+ results.push({
173
+ cfi: anyItem.cfi,
174
+ excerpt: anyItem.excerpt,
175
+ title: anyItem.title
176
+ });
177
+ }
178
+ }
179
+ } catch (error) {
180
+ onError?.(error);
181
+ throw error;
182
+ }
183
+ return results;
184
+ },
185
+ cancelSearch() {
186
+ searchToken++;
187
+ },
188
+ clearSearch() {
189
+ viewer.clearSearch?.();
190
+ },
191
+ getToc() {
192
+ return toc;
193
+ }
194
+ };
195
+ onReady?.(handle);
196
+ return handle;
197
+ }
198
+
199
+ // src/core/icons.ts
200
+ var icons = {
201
+ list: '<path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
202
+ search: '<path d="M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
203
+ "chevron-left": '<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
204
+ "chevron-right": '<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
205
+ "chevrons-left": '<path d="M11 17l-5-5 5-5M18 17l-5-5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
206
+ "chevrons-right": '<path d="M13 17l5-5-5-5M6 17l5-5-5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
207
+ sun: '<circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
208
+ moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
209
+ plus: '<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
210
+ minus: '<path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
211
+ x: '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
212
+ type: '<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
213
+ sliders: '<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
214
+ };
215
+ var SvgIcon = ({ name, size = 24, color = "currentColor", className }) => {
216
+ const iconPath = icons[name] || "";
217
+ const style = useMemo(() => {
218
+ const sizeVal = typeof size === "number" ? `${size}px` : size;
219
+ return {
220
+ width: sizeVal,
221
+ height: sizeVal,
222
+ color,
223
+ minWidth: sizeVal,
224
+ display: "inline-block",
225
+ verticalAlign: "middle"
226
+ };
227
+ }, [size, color]);
228
+ if (!iconPath) return null;
229
+ return /* @__PURE__ */ jsx(
230
+ "svg",
231
+ {
232
+ xmlns: "http://www.w3.org/2000/svg",
233
+ viewBox: "0 0 24 24",
234
+ fill: "none",
235
+ style,
236
+ className,
237
+ dangerouslySetInnerHTML: { __html: iconPath }
238
+ }
239
+ );
240
+ };
241
+ var DesktopToolbar = ({
242
+ onToggleToc,
243
+ onToggleSearch,
244
+ onPrevSection,
245
+ onPrevPage,
246
+ onNextPage,
247
+ onNextSection,
248
+ darkMode,
249
+ onToggleDarkMode,
250
+ fontSize,
251
+ onFontSizeChange
252
+ }) => {
253
+ return /* @__PURE__ */ jsxs("div", { className: "ebook-reader__toolbar", children: [
254
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__panel", children: [
255
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onToggleToc, title: "\u76EE\u5F55", children: /* @__PURE__ */ jsx(SvgIcon, { name: "list" }) }),
256
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onToggleSearch, title: "\u641C\u7D22", children: /* @__PURE__ */ jsx(SvgIcon, { name: "search" }) }),
257
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__divider" }),
258
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onPrevSection, title: "\u4E0A\u4E00\u7AE0", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevrons-left" }) }),
259
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onPrevPage, title: "\u4E0A\u4E00\u9875", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevron-left" }) }),
260
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onNextPage, title: "\u4E0B\u4E00\u9875", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevron-right" }) }),
261
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onNextSection, title: "\u4E0B\u4E00\u7AE0", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevrons-right" }) })
262
+ ] }),
263
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__panel", children: [
264
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onToggleDarkMode, title: "\u4E3B\u9898", children: /* @__PURE__ */ jsx(SvgIcon, { name: darkMode ? "sun" : "moon" }) }),
265
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__divider" }),
266
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onFontSizeChange(fontSize + 10), title: "\u589E\u5927\u5B57\u53F7", children: /* @__PURE__ */ jsx(SvgIcon, { name: "plus" }) }),
267
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__font", children: [
268
+ fontSize,
269
+ "%"
270
+ ] }),
271
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onFontSizeChange(fontSize - 10), title: "\u51CF\u5C0F\u5B57\u53F7", children: /* @__PURE__ */ jsx(SvgIcon, { name: "minus" }) })
272
+ ] })
273
+ ] });
274
+ };
275
+ var DesktopBottomBar = ({
276
+ status,
277
+ errorText,
278
+ sectionLabel,
279
+ displayedPercent,
280
+ onSeekStart,
281
+ onSeekChange,
282
+ onSeekEnd,
283
+ onSeekCommit
284
+ }) => {
285
+ return /* @__PURE__ */ jsxs("div", { className: "ebook-reader__bottom", children: [
286
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__bottom-left", children: [
287
+ /* @__PURE__ */ jsx("span", { className: "ebook-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5C31\u7EEA" }),
288
+ sectionLabel ? /* @__PURE__ */ jsx("span", { className: "ebook-reader__section", children: sectionLabel }) : null
289
+ ] }),
290
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__bottom-right", children: [
291
+ /* @__PURE__ */ jsx(
292
+ "input",
293
+ {
294
+ className: "ebook-reader__range",
295
+ type: "range",
296
+ min: 0,
297
+ max: 100,
298
+ step: 1,
299
+ value: displayedPercent,
300
+ onChange: (e) => {
301
+ onSeekStart();
302
+ onSeekChange(Number(e.target.value));
303
+ },
304
+ onPointerUp: (e) => {
305
+ const v = Number(e.target.value);
306
+ onSeekEnd(v);
307
+ },
308
+ onKeyUp: (e) => {
309
+ if (e.key !== "Enter") return;
310
+ const v = Number(e.target.value);
311
+ onSeekCommit(v);
312
+ }
313
+ }
314
+ ),
315
+ /* @__PURE__ */ jsxs("span", { className: "ebook-reader__percent", children: [
316
+ displayedPercent,
317
+ "%"
318
+ ] })
319
+ ] })
320
+ ] });
321
+ };
322
+ var TocTree = ({ items, onSelect }) => {
323
+ return /* @__PURE__ */ jsx("ul", { className: "ebook-reader__toc-list", children: items.map((item, idx) => {
324
+ const key = item.href || `${item.label ?? "item"}-${idx}`;
325
+ const hasChildren = Boolean(item.subitems?.length);
326
+ const label = item.label || item.href || "\u672A\u547D\u540D";
327
+ if (!hasChildren) {
328
+ return /* @__PURE__ */ jsx("li", { className: "ebook-reader__toc-item", children: /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__toc-btn", onClick: () => onSelect(item.href), children: label }) }, key);
329
+ }
330
+ return /* @__PURE__ */ jsx("li", { className: "ebook-reader__toc-item", children: /* @__PURE__ */ jsxs("details", { className: "ebook-reader__toc-details", children: [
331
+ /* @__PURE__ */ jsx("summary", { className: "ebook-reader__toc-summary", children: label }),
332
+ /* @__PURE__ */ jsx(TocTree, { items: item.subitems ?? [], onSelect })
333
+ ] }) }, key);
334
+ }) });
335
+ };
336
+ var TocDrawer = ({ isOpen, onClose, toc, onSelect }) => {
337
+ return /* @__PURE__ */ jsxs("aside", { className: `ebook-reader__drawer ${isOpen ? "is-open" : ""}`, "aria-hidden": !isOpen, children: [
338
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__drawer-header", children: [
339
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__drawer-title", children: "\u76EE\u5F55" }),
340
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onClose, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
341
+ ] }),
342
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__drawer-body", children: toc.length ? /* @__PURE__ */ jsx(
343
+ TocTree,
344
+ {
345
+ items: toc,
346
+ onSelect: (href) => {
347
+ onSelect(href);
348
+ onClose();
349
+ }
350
+ }
351
+ ) : /* @__PURE__ */ jsx("div", { className: "ebook-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) })
352
+ ] });
353
+ };
354
+ var SearchResultList = ({ results, onSelect }) => {
355
+ return /* @__PURE__ */ jsx("ul", { className: "ebook-reader__search-list", children: results.map((r, idx) => /* @__PURE__ */ jsx("li", { className: "ebook-reader__search-item", children: /* @__PURE__ */ jsxs(
356
+ "button",
357
+ {
358
+ type: "button",
359
+ className: "ebook-reader__search-btn",
360
+ onClick: () => {
361
+ if (r.cfi) onSelect(r.cfi);
362
+ },
363
+ children: [
364
+ r.label ? /* @__PURE__ */ jsx("div", { className: "ebook-reader__search-label", children: r.label }) : null,
365
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__search-excerpt", children: typeof r.excerpt === "string" ? r.excerpt : `${r.excerpt?.pre ?? ""}${r.excerpt?.match ?? ""}${r.excerpt?.post ?? ""}` })
366
+ ]
367
+ }
368
+ ) }, `${r.cfi ?? "no-cfi"}-${idx}`)) });
369
+ };
370
+ var SearchDrawer = ({
371
+ isOpen,
372
+ onClose,
373
+ status,
374
+ search,
375
+ onSearch,
376
+ onQueryChange,
377
+ onOptionChange,
378
+ onCancelSearch,
379
+ onResultSelect
380
+ }) => {
381
+ return /* @__PURE__ */ jsxs("aside", { className: `ebook-reader__drawer right ${isOpen ? "is-open" : ""}`, "aria-hidden": !isOpen, children: [
382
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__drawer-header", children: [
383
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__drawer-title", children: "\u641C\u7D22" }),
384
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onClose, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
385
+ ] }),
386
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__drawer-body", children: [
387
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__field", children: [
388
+ /* @__PURE__ */ jsx(
389
+ "input",
390
+ {
391
+ className: "ebook-reader__input",
392
+ placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
393
+ value: search.query,
394
+ onChange: (e) => {
395
+ const v = e.target.value;
396
+ onQueryChange(v);
397
+ if (!v.trim()) onSearch("");
398
+ },
399
+ disabled: status !== "ready",
400
+ onKeyDown: (e) => {
401
+ if (e.key === "Enter") onSearch(search.query);
402
+ }
403
+ }
404
+ ),
405
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: /* @__PURE__ */ jsx(SvgIcon, { name: "search" }) })
406
+ ] }),
407
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__checks", children: [
408
+ /* @__PURE__ */ jsxs("label", { className: "ebook-reader__check", children: [
409
+ /* @__PURE__ */ jsx(
410
+ "input",
411
+ {
412
+ type: "checkbox",
413
+ checked: Boolean(search.options.matchCase),
414
+ onChange: (e) => onOptionChange({ matchCase: e.target.checked })
415
+ }
416
+ ),
417
+ "\u533A\u5206\u5927\u5C0F\u5199"
418
+ ] }),
419
+ /* @__PURE__ */ jsxs("label", { className: "ebook-reader__check", children: [
420
+ /* @__PURE__ */ jsx(
421
+ "input",
422
+ {
423
+ type: "checkbox",
424
+ checked: Boolean(search.options.wholeWords),
425
+ onChange: (e) => onOptionChange({ wholeWords: e.target.checked })
426
+ }
427
+ ),
428
+ "\u5168\u8BCD\u5339\u914D"
429
+ ] }),
430
+ /* @__PURE__ */ jsxs("label", { className: "ebook-reader__check", children: [
431
+ /* @__PURE__ */ jsx(
432
+ "input",
433
+ {
434
+ type: "checkbox",
435
+ checked: Boolean(search.options.matchDiacritics),
436
+ onChange: (e) => onOptionChange({ matchDiacritics: e.target.checked })
437
+ }
438
+ ),
439
+ "\u533A\u5206\u53D8\u97F3"
440
+ ] })
441
+ ] }),
442
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__meta", children: [
443
+ /* @__PURE__ */ jsxs("span", { children: [
444
+ "\u8FDB\u5EA6 ",
445
+ search.progressPercent,
446
+ "%"
447
+ ] }),
448
+ search.searching ? /* @__PURE__ */ jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
449
+ search.searching ? /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
450
+ ] }),
451
+ search.results.length ? /* @__PURE__ */ jsx(SearchResultList, { results: search.results, onSelect: onResultSelect }) : /* @__PURE__ */ jsx("div", { className: "ebook-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
452
+ ] })
453
+ ] });
454
+ };
455
+ var MobileUI = ({
456
+ barVisible,
457
+ activePanel,
458
+ onTogglePanel,
459
+ onClosePanel,
460
+ toc,
461
+ search,
462
+ status,
463
+ errorText,
464
+ sectionLabel,
465
+ displayedPercent,
466
+ darkMode,
467
+ fontSize,
468
+ onTocSelect,
469
+ onSearch,
470
+ onSearchQueryChange,
471
+ onSearchOptionChange,
472
+ onCancelSearch,
473
+ onSearchResultSelect,
474
+ onSeekStart,
475
+ onSeekChange,
476
+ onSeekEnd,
477
+ onSeekCommit,
478
+ onToggleDarkMode,
479
+ onFontSizeChange
480
+ }) => {
481
+ const mobileTitle = activePanel === "menu" ? "\u76EE\u5F55" : activePanel === "search" ? "\u641C\u7D22" : activePanel === "progress" ? "\u8FDB\u5EA6" : activePanel === "theme" ? "\u660E\u6697" : activePanel === "font" ? "\u5B57\u53F7" : "";
482
+ const [tooltip, setTooltip] = useState(null);
483
+ const timerRef = useRef(null);
484
+ const handleTouchStart = (e, text) => {
485
+ const rect = e.currentTarget.getBoundingClientRect();
486
+ timerRef.current = window.setTimeout(() => {
487
+ setTooltip({
488
+ text,
489
+ left: rect.left + rect.width / 2
490
+ });
491
+ }, 500);
492
+ };
493
+ const handleTouchEnd = () => {
494
+ if (timerRef.current) {
495
+ clearTimeout(timerRef.current);
496
+ timerRef.current = null;
497
+ }
498
+ setTooltip(null);
499
+ };
500
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
501
+ /* @__PURE__ */ jsxs("div", { className: `ebook-reader__mbar ${barVisible ? "is-visible" : ""}`, children: [
502
+ tooltip && /* @__PURE__ */ jsx(
503
+ "div",
504
+ {
505
+ className: "ebook-reader__tooltip",
506
+ style: {
507
+ position: "fixed",
508
+ bottom: "60px",
509
+ // 位于工具栏上方
510
+ left: tooltip.left,
511
+ transform: "translateX(-50%)",
512
+ background: "rgba(0,0,0,0.8)",
513
+ color: "#fff",
514
+ padding: "4px 8px",
515
+ borderRadius: "4px",
516
+ fontSize: "12px",
517
+ pointerEvents: "none",
518
+ zIndex: 1e3,
519
+ whiteSpace: "nowrap"
520
+ },
521
+ children: tooltip.text
522
+ }
523
+ ),
524
+ /* @__PURE__ */ jsx(
525
+ "button",
526
+ {
527
+ type: "button",
528
+ className: "ebook-reader__btn",
529
+ onClick: () => onTogglePanel("menu"),
530
+ "aria-pressed": activePanel === "menu",
531
+ onTouchStart: (e) => handleTouchStart(e, "\u76EE\u5F55"),
532
+ onTouchEnd: handleTouchEnd,
533
+ onTouchCancel: handleTouchEnd,
534
+ title: "\u76EE\u5F55",
535
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "list" })
536
+ }
537
+ ),
538
+ /* @__PURE__ */ jsx(
539
+ "button",
540
+ {
541
+ type: "button",
542
+ className: "ebook-reader__btn",
543
+ onClick: () => onTogglePanel("search"),
544
+ "aria-pressed": activePanel === "search",
545
+ onTouchStart: (e) => handleTouchStart(e, "\u641C\u7D22"),
546
+ onTouchEnd: handleTouchEnd,
547
+ onTouchCancel: handleTouchEnd,
548
+ title: "\u641C\u7D22",
549
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "search" })
550
+ }
551
+ ),
552
+ /* @__PURE__ */ jsx(
553
+ "button",
554
+ {
555
+ type: "button",
556
+ className: "ebook-reader__btn",
557
+ onClick: () => onTogglePanel("progress"),
558
+ "aria-pressed": activePanel === "progress",
559
+ onTouchStart: (e) => handleTouchStart(e, "\u8FDB\u5EA6"),
560
+ onTouchEnd: handleTouchEnd,
561
+ onTouchCancel: handleTouchEnd,
562
+ title: "\u8FDB\u5EA6",
563
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "sliders" })
564
+ }
565
+ ),
566
+ /* @__PURE__ */ jsx(
567
+ "button",
568
+ {
569
+ type: "button",
570
+ className: "ebook-reader__btn",
571
+ onClick: () => onTogglePanel("theme"),
572
+ "aria-pressed": activePanel === "theme",
573
+ onTouchStart: (e) => handleTouchStart(e, "\u660E\u6697"),
574
+ onTouchEnd: handleTouchEnd,
575
+ onTouchCancel: handleTouchEnd,
576
+ title: "\u660E\u6697",
577
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "sun" })
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsx(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ className: "ebook-reader__btn",
585
+ onClick: () => onTogglePanel("font"),
586
+ "aria-pressed": activePanel === "font",
587
+ onTouchStart: (e) => handleTouchStart(e, "\u5B57\u53F7"),
588
+ onTouchEnd: handleTouchEnd,
589
+ onTouchCancel: handleTouchEnd,
590
+ title: "\u5B57\u53F7",
591
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "type" })
592
+ }
593
+ )
594
+ ] }),
595
+ activePanel ? /* @__PURE__ */ jsx("div", { className: "ebook-reader__moverlay", onClick: onClosePanel }) : null,
596
+ /* @__PURE__ */ jsxs("div", { className: `ebook-reader__msheet ${activePanel ? "is-open" : ""}`, "aria-hidden": !activePanel, children: [
597
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__msheet-header", children: [
598
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__msheet-title", children: mobileTitle }),
599
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: onClosePanel, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
600
+ ] }),
601
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__msheet-body", children: [
602
+ activePanel === "menu" ? toc.length ? /* @__PURE__ */ jsx(
603
+ TocTree,
604
+ {
605
+ items: toc,
606
+ onSelect: (href) => {
607
+ onTocSelect(href);
608
+ onClosePanel();
609
+ }
610
+ }
611
+ ) : /* @__PURE__ */ jsx("div", { className: "ebook-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) : null,
612
+ activePanel === "search" ? /* @__PURE__ */ jsxs(Fragment, { children: [
613
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__field", children: [
614
+ /* @__PURE__ */ jsx(
615
+ "input",
616
+ {
617
+ className: "ebook-reader__input",
618
+ placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
619
+ value: search.query,
620
+ onChange: (e) => {
621
+ const v = e.target.value;
622
+ onSearchQueryChange(v);
623
+ if (!v.trim()) onSearch("");
624
+ },
625
+ disabled: status !== "ready",
626
+ onKeyDown: (e) => {
627
+ if (e.key === "Enter") onSearch(search.query);
628
+ }
629
+ }
630
+ ),
631
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: "\u641C\u7D22" })
632
+ ] }),
633
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__checks", children: [
634
+ /* @__PURE__ */ jsxs("label", { className: "ebook-reader__check", children: [
635
+ /* @__PURE__ */ jsx(
636
+ "input",
637
+ {
638
+ type: "checkbox",
639
+ checked: Boolean(search.options.matchCase),
640
+ onChange: (e) => onSearchOptionChange({ matchCase: e.target.checked })
641
+ }
642
+ ),
643
+ "\u533A\u5206\u5927\u5C0F\u5199"
644
+ ] }),
645
+ /* @__PURE__ */ jsxs("label", { className: "ebook-reader__check", children: [
646
+ /* @__PURE__ */ jsx(
647
+ "input",
648
+ {
649
+ type: "checkbox",
650
+ checked: Boolean(search.options.wholeWords),
651
+ onChange: (e) => onSearchOptionChange({ wholeWords: e.target.checked })
652
+ }
653
+ ),
654
+ "\u5168\u8BCD\u5339\u914D"
655
+ ] }),
656
+ /* @__PURE__ */ jsxs("label", { className: "ebook-reader__check", children: [
657
+ /* @__PURE__ */ jsx(
658
+ "input",
659
+ {
660
+ type: "checkbox",
661
+ checked: Boolean(search.options.matchDiacritics),
662
+ onChange: (e) => onSearchOptionChange({ matchDiacritics: e.target.checked })
663
+ }
664
+ ),
665
+ "\u533A\u5206\u53D8\u97F3"
666
+ ] })
667
+ ] }),
668
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__meta", children: [
669
+ /* @__PURE__ */ jsxs("span", { children: [
670
+ "\u8FDB\u5EA6 ",
671
+ search.progressPercent,
672
+ "%"
673
+ ] }),
674
+ search.searching ? /* @__PURE__ */ jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
675
+ search.searching ? /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
676
+ ] }),
677
+ search.results.length ? /* @__PURE__ */ jsx(SearchResultList, { results: search.results, onSelect: onSearchResultSelect }) : /* @__PURE__ */ jsx("div", { className: "ebook-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
678
+ ] }) : null,
679
+ activePanel === "progress" ? /* @__PURE__ */ jsxs(Fragment, { children: [
680
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__meta", children: [
681
+ /* @__PURE__ */ jsx("span", { className: "ebook-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5C31\u7EEA" }),
682
+ sectionLabel ? /* @__PURE__ */ jsx("span", { children: sectionLabel }) : null
683
+ ] }),
684
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__mprogress", children: [
685
+ /* @__PURE__ */ jsx(
686
+ "input",
687
+ {
688
+ className: "ebook-reader__range",
689
+ type: "range",
690
+ min: 0,
691
+ max: 100,
692
+ step: 1,
693
+ value: displayedPercent,
694
+ onChange: (e) => {
695
+ onSeekStart();
696
+ onSeekChange(Number(e.target.value));
697
+ },
698
+ onPointerUp: (e) => {
699
+ const v = Number(e.target.value);
700
+ onSeekEnd(v);
701
+ },
702
+ onKeyUp: (e) => {
703
+ if (e.key !== "Enter") return;
704
+ const v = Number(e.target.value);
705
+ onSeekCommit(v);
706
+ }
707
+ }
708
+ ),
709
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__mprogress-percent", children: [
710
+ displayedPercent,
711
+ "%"
712
+ ] })
713
+ ] })
714
+ ] }) : null,
715
+ activePanel === "theme" ? /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onToggleDarkMode(!darkMode), children: darkMode ? "\u5207\u6362\u5230\u4EAE\u8272" : "\u5207\u6362\u5230\u6697\u9ED1" }) : null,
716
+ activePanel === "font" ? /* @__PURE__ */ jsxs("div", { className: "ebook-reader__mfont", children: [
717
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onFontSizeChange(fontSize - 10), children: "A-" }),
718
+ /* @__PURE__ */ jsxs("div", { className: "ebook-reader__font", children: [
719
+ fontSize,
720
+ "%"
721
+ ] }),
722
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ebook-reader__btn", onClick: () => onFontSizeChange(fontSize + 10), children: "A+" })
723
+ ] }) : null
724
+ ] })
725
+ ] })
726
+ ] });
727
+ };
728
+ var MOBILE_MAX_WIDTH = 768;
729
+ var WIDE_MIN_WIDTH = 1024;
730
+ var clamp = (n, min, max) => Math.min(max, Math.max(min, n));
731
+ var mergeClassName = (...parts) => parts.filter(Boolean).join(" ");
732
+ var EBookReader = forwardRef(function EBookReader2({
733
+ file,
734
+ className,
735
+ style,
736
+ defaultFontSize = 100,
737
+ fontSize: controlledFontSize,
738
+ onFontSizeChange,
739
+ defaultDarkMode = false,
740
+ darkMode: controlledDarkMode,
741
+ onDarkModeChange,
742
+ enableKeyboardNav = true,
743
+ defaultSearchOptions = { matchCase: false, wholeWords: false, matchDiacritics: false },
744
+ onReady,
745
+ onError,
746
+ onProgress
747
+ }, ref) {
748
+ const rootRef = useRef(null);
749
+ const viewerHostRef = useRef(null);
750
+ const readerRef = useRef(null);
751
+ const [status, setStatus] = useState("idle");
752
+ const [errorText, setErrorText] = useState("");
753
+ const [toc, setToc] = useState([]);
754
+ const [tocOpen, setTocOpen] = useState(false);
755
+ const [searchOpen, setSearchOpen] = useState(false);
756
+ const [progressInfo, setProgressInfo] = useState(null);
757
+ const [isSeeking, setIsSeeking] = useState(false);
758
+ const [seekPercent, setSeekPercent] = useState(0);
759
+ const [uncontrolledFontSize, setUncontrolledFontSize] = useState(defaultFontSize);
760
+ const [uncontrolledDarkMode, setUncontrolledDarkMode] = useState(defaultDarkMode);
761
+ const fontSize = controlledFontSize ?? uncontrolledFontSize;
762
+ const darkMode = controlledDarkMode ?? uncontrolledDarkMode;
763
+ const [search, setSearch] = useState({
764
+ query: "",
765
+ options: defaultSearchOptions,
766
+ progressPercent: 0,
767
+ searching: false,
768
+ results: []
769
+ });
770
+ const [layout, setLayout] = useState("default");
771
+ const [mobileBarVisible, setMobileBarVisible] = useState(false);
772
+ const [mobilePanel, setMobilePanel] = useState(null);
773
+ const layoutRef = useRef(layout);
774
+ const boundDocsRef = useRef(/* @__PURE__ */ new WeakSet());
775
+ const gestureRef = useRef({ startX: 0, startY: 0, startAt: 0, tracking: false, moved: false, actionTaken: false });
776
+ const percentage = useMemo(() => Math.round((progressInfo?.fraction ?? 0) * 100), [progressInfo]);
777
+ const displayedPercent = isSeeking ? seekPercent : percentage;
778
+ const sectionLabel = progressInfo?.tocItem?.label ?? "";
779
+ useEffect(() => {
780
+ const el = rootRef.current;
781
+ if (!el) return;
782
+ const ro = new ResizeObserver((entries) => {
783
+ const w = entries[0]?.contentRect?.width ?? el.getBoundingClientRect().width;
784
+ const next = w <= MOBILE_MAX_WIDTH ? "mobile" : w >= WIDE_MIN_WIDTH ? "wide" : "default";
785
+ setLayout((prev) => prev === next ? prev : next);
786
+ });
787
+ ro.observe(el);
788
+ return () => ro.disconnect();
789
+ }, []);
790
+ useEffect(() => {
791
+ if (layout === "mobile") return;
792
+ setMobilePanel(null);
793
+ setMobileBarVisible(false);
794
+ }, [layout]);
795
+ useEffect(() => {
796
+ layoutRef.current = layout;
797
+ }, [layout]);
798
+ const closeMobileSheet = useCallback(() => setMobilePanel(null), []);
799
+ const toggleMobilePanel = useCallback((panel) => {
800
+ setMobileBarVisible(true);
801
+ setMobilePanel((prev) => prev === panel ? null : panel);
802
+ }, []);
803
+ const onPointerDown = useCallback((e) => {
804
+ if (layoutRef.current !== "mobile") return;
805
+ const t = e.target;
806
+ if (!t) return;
807
+ if (t.closest(".ebook-reader__mbar") || t.closest(".ebook-reader__msheet")) return;
808
+ if (t.closest('a,button,input,textarea,select,label,[role="button"],[contenteditable="true"]')) return;
809
+ gestureRef.current.tracking = true;
810
+ gestureRef.current.moved = false;
811
+ gestureRef.current.actionTaken = false;
812
+ gestureRef.current.startAt = e.timeStamp;
813
+ gestureRef.current.startX = e.screenX;
814
+ gestureRef.current.startY = e.screenY;
815
+ }, []);
816
+ const onPointerMove = useCallback((e) => {
817
+ if (!gestureRef.current.tracking) return;
818
+ const dx = e.screenX - gestureRef.current.startX;
819
+ const dy = e.screenY - gestureRef.current.startY;
820
+ if (Math.abs(dx) > 8 || Math.abs(dy) > 8) gestureRef.current.moved = true;
821
+ if (Math.abs(dy) < 8) return;
822
+ if (Math.abs(dy) < Math.abs(dx)) {
823
+ if (Math.abs(dx) >= 8) gestureRef.current.tracking = false;
824
+ return;
825
+ }
826
+ if (dy <= -24) {
827
+ gestureRef.current.actionTaken = true;
828
+ gestureRef.current.tracking = false;
829
+ setMobileBarVisible(true);
830
+ return;
831
+ }
832
+ if (dy >= 24) {
833
+ gestureRef.current.actionTaken = true;
834
+ gestureRef.current.tracking = false;
835
+ setMobileBarVisible(false);
836
+ setMobilePanel(null);
837
+ }
838
+ }, []);
839
+ const onPointerEnd = useCallback((e) => {
840
+ if (layoutRef.current !== "mobile") {
841
+ gestureRef.current.tracking = false;
842
+ return;
843
+ }
844
+ if (gestureRef.current.actionTaken) {
845
+ gestureRef.current.tracking = false;
846
+ gestureRef.current.moved = false;
847
+ gestureRef.current.actionTaken = false;
848
+ return;
849
+ }
850
+ if (!gestureRef.current.tracking) {
851
+ gestureRef.current.moved = false;
852
+ return;
853
+ }
854
+ const dx = e.screenX - gestureRef.current.startX;
855
+ const dy = e.screenY - gestureRef.current.startY;
856
+ const dt = e.timeStamp - gestureRef.current.startAt;
857
+ const isTap = !gestureRef.current.moved && Math.hypot(dx, dy) <= 10 && dt <= 300;
858
+ if (isTap) {
859
+ setMobileBarVisible((prev) => {
860
+ const next = !prev;
861
+ if (!next) setMobilePanel(null);
862
+ return next;
863
+ });
864
+ }
865
+ gestureRef.current.tracking = false;
866
+ gestureRef.current.moved = false;
867
+ }, []);
868
+ useEffect(() => {
869
+ const root = rootRef.current;
870
+ if (!root) return;
871
+ root.addEventListener("pointerdown", onPointerDown);
872
+ root.addEventListener("pointermove", onPointerMove);
873
+ root.addEventListener("pointerup", onPointerEnd);
874
+ root.addEventListener("pointercancel", onPointerEnd);
875
+ return () => {
876
+ root.removeEventListener("pointerdown", onPointerDown);
877
+ root.removeEventListener("pointermove", onPointerMove);
878
+ root.removeEventListener("pointerup", onPointerEnd);
879
+ root.removeEventListener("pointercancel", onPointerEnd);
880
+ };
881
+ }, [onPointerDown, onPointerMove, onPointerEnd]);
882
+ const setDarkModeInternal = useCallback(
883
+ (next) => {
884
+ if (controlledDarkMode == null) setUncontrolledDarkMode(next);
885
+ onDarkModeChange?.(next);
886
+ readerRef.current?.setDarkMode(next);
887
+ },
888
+ [controlledDarkMode, onDarkModeChange]
889
+ );
890
+ const setFontSizeInternal = useCallback(
891
+ (next) => {
892
+ const safe = clamp(next, 50, 300);
893
+ if (controlledFontSize == null) setUncontrolledFontSize(safe);
894
+ onFontSizeChange?.(safe);
895
+ readerRef.current?.setFontSize(safe);
896
+ },
897
+ [controlledFontSize, onFontSizeChange]
898
+ );
899
+ const closeDrawers = useCallback(() => {
900
+ setTocOpen(false);
901
+ setSearchOpen(false);
902
+ }, []);
903
+ const handleOpenFile = useCallback(
904
+ async (nextFile) => {
905
+ const reader = readerRef.current;
906
+ if (!reader) return;
907
+ setStatus("opening");
908
+ setErrorText("");
909
+ setToc([]);
910
+ setProgressInfo(null);
911
+ setIsSeeking(false);
912
+ setSeekPercent(0);
913
+ setSearch((prev) => ({ ...prev, results: [], progressPercent: 0, searching: false }));
914
+ try {
915
+ await reader.open(nextFile);
916
+ setStatus("ready");
917
+ } catch (e) {
918
+ setStatus("error");
919
+ setErrorText(e?.message ? String(e.message) : "\u6253\u5F00\u5931\u8D25");
920
+ onError?.(e);
921
+ }
922
+ },
923
+ [onError]
924
+ );
925
+ const runSearch = useCallback(
926
+ async (query) => {
927
+ const reader = readerRef.current;
928
+ const normalized = query.trim();
929
+ setSearch((prev) => ({
930
+ ...prev,
931
+ query,
932
+ results: normalized ? prev.results : [],
933
+ progressPercent: 0,
934
+ searching: Boolean(normalized)
935
+ }));
936
+ if (!reader || !normalized) {
937
+ reader?.clearSearch();
938
+ return;
939
+ }
940
+ reader.cancelSearch();
941
+ try {
942
+ const results = await reader.search(normalized, search.options);
943
+ setSearch((prev) => ({ ...prev, results, searching: false, progressPercent: 100 }));
944
+ } catch (e) {
945
+ setSearch((prev) => ({ ...prev, searching: false }));
946
+ onError?.(e);
947
+ }
948
+ },
949
+ [onError, search.options]
950
+ );
951
+ useEffect(() => {
952
+ const host = viewerHostRef.current;
953
+ if (!host) return;
954
+ try {
955
+ const reader = createEBookReader(host, {
956
+ darkMode,
957
+ fontSize,
958
+ onReady: (h) => onReady?.(h),
959
+ onError: (e) => onError?.(e),
960
+ onProgress: (info) => {
961
+ setProgressInfo(info);
962
+ onProgress?.(info);
963
+ },
964
+ onToc: (items) => setToc(items),
965
+ onSearchProgress: (info) => {
966
+ const p = info.progress;
967
+ if (typeof p === "number") setSearch((prev) => ({ ...prev, progressPercent: Math.round(p * 100) }));
968
+ },
969
+ onContentLoad: (doc) => {
970
+ if (boundDocsRef.current.has(doc)) return;
971
+ boundDocsRef.current.add(doc);
972
+ doc.addEventListener("pointerdown", onPointerDown);
973
+ doc.addEventListener("pointermove", onPointerMove);
974
+ doc.addEventListener("pointerup", onPointerEnd);
975
+ doc.addEventListener("pointercancel", onPointerEnd);
976
+ }
977
+ });
978
+ readerRef.current = reader;
979
+ setStatus("ready");
980
+ } catch (e) {
981
+ setStatus("error");
982
+ setErrorText(e?.message ? String(e.message) : "\u521D\u59CB\u5316\u5931\u8D25");
983
+ onError?.(e);
984
+ return;
985
+ }
986
+ return () => {
987
+ readerRef.current?.destroy();
988
+ readerRef.current = null;
989
+ };
990
+ }, [onError, onProgress, onReady]);
991
+ useEffect(() => {
992
+ if (!file) return;
993
+ void handleOpenFile(file);
994
+ }, [file, handleOpenFile]);
995
+ useEffect(() => {
996
+ readerRef.current?.setDarkMode(darkMode);
997
+ }, [darkMode]);
998
+ useEffect(() => {
999
+ readerRef.current?.setFontSize(fontSize);
1000
+ }, [fontSize]);
1001
+ useEffect(() => {
1002
+ if (!enableKeyboardNav) return;
1003
+ const root = rootRef.current;
1004
+ if (!root) return;
1005
+ const handleKeyDown = (e) => {
1006
+ if (e.key === "ArrowLeft") readerRef.current?.prevPage();
1007
+ if (e.key === "ArrowRight") readerRef.current?.nextPage();
1008
+ if (e.key === "Escape") closeDrawers();
1009
+ };
1010
+ root.addEventListener("keydown", handleKeyDown);
1011
+ return () => {
1012
+ root.removeEventListener("keydown", handleKeyDown);
1013
+ };
1014
+ }, [closeDrawers, enableKeyboardNav]);
1015
+ useImperativeHandle(
1016
+ ref,
1017
+ () => ({
1018
+ prevPage: () => readerRef.current?.prevPage(),
1019
+ nextPage: () => readerRef.current?.nextPage(),
1020
+ prevSection: () => readerRef.current?.prevSection(),
1021
+ nextSection: () => readerRef.current?.nextSection(),
1022
+ goTo: (target) => readerRef.current?.goTo(target),
1023
+ goToFraction: (frac) => readerRef.current?.goToFraction(frac),
1024
+ search: (q, o) => readerRef.current ? readerRef.current.search(q, o) : Promise.resolve([]),
1025
+ cancelSearch: () => readerRef.current?.cancelSearch(),
1026
+ clearSearch: () => readerRef.current?.clearSearch()
1027
+ }),
1028
+ []
1029
+ );
1030
+ const handleSeekStart = useCallback(() => setIsSeeking(true), []);
1031
+ const handleSeekChange = useCallback((v) => setSeekPercent(v), []);
1032
+ const handleSeekEnd = useCallback((v) => {
1033
+ setIsSeeking(false);
1034
+ readerRef.current?.goToFraction(v / 100);
1035
+ }, []);
1036
+ const handleTocSelect = useCallback((href) => {
1037
+ if (href) readerRef.current?.goTo(href);
1038
+ }, []);
1039
+ const handleSearchResultSelect = useCallback((cfi) => {
1040
+ if (cfi) readerRef.current?.goTo(cfi);
1041
+ }, []);
1042
+ return /* @__PURE__ */ jsxs(
1043
+ "div",
1044
+ {
1045
+ ref: rootRef,
1046
+ className: mergeClassName("ebook-reader", className),
1047
+ style,
1048
+ "data-theme": darkMode ? "dark" : "light",
1049
+ "data-layout": layout,
1050
+ tabIndex: 0,
1051
+ children: [
1052
+ /* @__PURE__ */ jsx("div", { className: "ebook-reader__viewer", ref: viewerHostRef }),
1053
+ layout === "mobile" ? /* @__PURE__ */ jsx(
1054
+ MobileUI,
1055
+ {
1056
+ barVisible: mobileBarVisible,
1057
+ activePanel: mobilePanel,
1058
+ onTogglePanel: toggleMobilePanel,
1059
+ onClosePanel: closeMobileSheet,
1060
+ toc,
1061
+ search,
1062
+ status,
1063
+ errorText,
1064
+ sectionLabel,
1065
+ displayedPercent,
1066
+ darkMode,
1067
+ fontSize,
1068
+ onTocSelect: handleTocSelect,
1069
+ onSearch: (q) => void runSearch(q),
1070
+ onSearchQueryChange: (v) => setSearch((prev) => ({ ...prev, query: v })),
1071
+ onSearchOptionChange: (opt) => setSearch((prev) => ({ ...prev, options: { ...prev.options, ...opt } })),
1072
+ onCancelSearch: () => readerRef.current?.cancelSearch(),
1073
+ onSearchResultSelect: handleSearchResultSelect,
1074
+ onSeekStart: handleSeekStart,
1075
+ onSeekChange: handleSeekChange,
1076
+ onSeekEnd: handleSeekEnd,
1077
+ onSeekCommit: handleSeekEnd,
1078
+ onToggleDarkMode: setDarkModeInternal,
1079
+ onFontSizeChange: setFontSizeInternal
1080
+ }
1081
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1082
+ /* @__PURE__ */ jsx(
1083
+ DesktopToolbar,
1084
+ {
1085
+ onToggleToc: () => setTocOpen(true),
1086
+ onToggleSearch: () => setSearchOpen(true),
1087
+ onPrevSection: () => readerRef.current?.prevSection(),
1088
+ onPrevPage: () => readerRef.current?.prevPage(),
1089
+ onNextPage: () => readerRef.current?.nextPage(),
1090
+ onNextSection: () => readerRef.current?.nextSection(),
1091
+ darkMode,
1092
+ onToggleDarkMode: () => setDarkModeInternal(!darkMode),
1093
+ fontSize,
1094
+ onFontSizeChange: setFontSizeInternal
1095
+ }
1096
+ ),
1097
+ (tocOpen || searchOpen) && /* @__PURE__ */ jsx("div", { className: "ebook-reader__overlay", onClick: closeDrawers }),
1098
+ /* @__PURE__ */ jsx(
1099
+ TocDrawer,
1100
+ {
1101
+ isOpen: tocOpen,
1102
+ onClose: () => setTocOpen(false),
1103
+ toc,
1104
+ onSelect: handleTocSelect
1105
+ }
1106
+ ),
1107
+ /* @__PURE__ */ jsx(
1108
+ SearchDrawer,
1109
+ {
1110
+ isOpen: searchOpen,
1111
+ onClose: () => setSearchOpen(false),
1112
+ status,
1113
+ search,
1114
+ onSearch: (q) => void runSearch(q),
1115
+ onQueryChange: (v) => setSearch((prev) => ({ ...prev, query: v })),
1116
+ onOptionChange: (opt) => setSearch((prev) => ({ ...prev, options: { ...prev.options, ...opt } })),
1117
+ onCancelSearch: () => readerRef.current?.cancelSearch(),
1118
+ onResultSelect: handleSearchResultSelect
1119
+ }
1120
+ ),
1121
+ /* @__PURE__ */ jsx(
1122
+ DesktopBottomBar,
1123
+ {
1124
+ status,
1125
+ errorText,
1126
+ sectionLabel,
1127
+ displayedPercent,
1128
+ onSeekStart: handleSeekStart,
1129
+ onSeekChange: handleSeekChange,
1130
+ onSeekEnd: handleSeekEnd,
1131
+ onSeekCommit: handleSeekEnd
1132
+ }
1133
+ )
1134
+ ] })
1135
+ ]
1136
+ }
1137
+ );
1138
+ });
1139
+
1140
+ export { EBookReader };
1141
+ //# sourceMappingURL=react.js.map
1142
+ //# sourceMappingURL=react.js.map