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