@somecat/epub-reader 0.1.3 → 0.1.4

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 CHANGED
@@ -4,503 +4,7 @@ var react = require('react');
4
4
  require('foliate-js/view.js');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
- // src/styles/epub-reader.cssText.ts
8
- var epubReaderCssText = `
9
- :root {
10
- --epub-reader-panel-bg: #ffffff;
11
- --epub-reader-panel-fg: #1f2937;
12
- --epub-reader-border: rgba(0, 0, 0, 0.12);
13
- --epub-reader-accent: #2563eb;
14
- --epub-reader-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
15
- --epub-reader-radius: 10px;
16
- --epub-reader-bottom-bar-height: 40px;
17
- }
18
-
19
- .epub-reader-icon {
20
- display: inline-block;
21
- vertical-align: middle;
22
- }
23
-
24
- .epub-reader[data-theme='dark'] {
25
- --epub-reader-panel-bg: #141414;
26
- --epub-reader-panel-fg: #e5e7eb;
27
- --epub-reader-border: rgba(255, 255, 255, 0.14);
28
- --epub-reader-accent: #60a5fa;
29
- --epub-reader-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
30
- }
31
-
32
- .epub-reader {
33
- position: relative;
34
- width: 100%;
35
- height: 100%;
36
- outline: none;
37
- overflow: hidden;
38
- }
39
-
40
- .epub-reader__viewer {
41
- width: 100%;
42
- height: calc(100% - var(--epub-reader-bottom-bar-height));
43
- background: var(--epub-reader-panel-bg);
44
- transition: background-color 0.2s ease;
45
- }
46
-
47
- .epub-reader__toolbar {
48
- position: absolute;
49
- top: 16px;
50
- right: 16px;
51
- z-index: 10;
52
- display: flex;
53
- flex-direction: column;
54
- gap: 10px;
55
- }
56
-
57
- .epub-reader[data-layout='wide'] .epub-reader__toolbar {
58
- top: 50%;
59
- transform: translateY(-50%);
60
- }
61
-
62
- .epub-reader__panel {
63
- width: 88px;
64
- padding: 8px;
65
- border: 1px solid var(--epub-reader-border);
66
- border-radius: var(--epub-reader-radius);
67
- background: color-mix(in srgb, var(--epub-reader-panel-bg) 92%, transparent);
68
- color: var(--epub-reader-panel-fg);
69
- box-shadow: var(--epub-reader-shadow);
70
- backdrop-filter: blur(10px);
71
- display: flex;
72
- flex-direction: column;
73
- gap: 6px;
74
- align-items: stretch;
75
- }
76
-
77
- .epub-reader__btn {
78
- appearance: none;
79
- border: 1px solid var(--epub-reader-border);
80
- background: transparent;
81
- color: inherit;
82
- padding: 6px 8px;
83
- border-radius: 8px;
84
- font-size: 12px;
85
- line-height: 1;
86
- cursor: pointer;
87
- user-select: none;
88
- }
89
-
90
- .epub-reader__btn:disabled {
91
- opacity: 0.6;
92
- cursor: not-allowed;
93
- }
94
-
95
- .epub-reader__btn:hover:not(:disabled) {
96
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
97
- }
98
-
99
- .epub-reader__divider {
100
- height: 1px;
101
- background: var(--epub-reader-border);
102
- margin: 2px 0;
103
- }
104
-
105
- .epub-reader__font {
106
- text-align: center;
107
- font-size: 12px;
108
- font-weight: 700;
109
- color: var(--epub-reader-accent);
110
- padding: 2px 0;
111
- }
112
-
113
- .epub-reader__overlay {
114
- position: absolute;
115
- inset: 0;
116
- background: rgba(0, 0, 0, 0.35);
117
- z-index: 20;
118
- }
119
-
120
- .epub-reader__drawer {
121
- position: absolute;
122
- top: 0;
123
- bottom: 0;
124
- left: 0;
125
- width: 320px;
126
- background: var(--epub-reader-panel-bg);
127
- color: var(--epub-reader-panel-fg);
128
- border-right: 1px solid var(--epub-reader-border);
129
- transform: translateX(-100%);
130
- transition: transform 0.2s ease;
131
- z-index: 30;
132
- display: flex;
133
- flex-direction: column;
134
- }
135
-
136
- .epub-reader__drawer.right {
137
- left: auto;
138
- right: 0;
139
- border-right: none;
140
- border-left: 1px solid var(--epub-reader-border);
141
- transform: translateX(100%);
142
- }
143
-
144
- .epub-reader__drawer.is-open {
145
- transform: translateX(0);
146
- }
147
-
148
- .epub-reader__drawer-header {
149
- display: flex;
150
- align-items: center;
151
- justify-content: space-between;
152
- gap: 8px;
153
- padding: 12px;
154
- border-bottom: 1px solid var(--epub-reader-border);
155
- }
156
-
157
- .epub-reader__drawer-title {
158
- font-size: 14px;
159
- font-weight: 700;
160
- }
161
-
162
- .epub-reader__drawer-body {
163
- padding: 12px;
164
- overflow: auto;
165
- flex: 1;
166
- }
167
-
168
- .epub-reader__empty {
169
- padding: 20px 8px;
170
- opacity: 0.7;
171
- text-align: center;
172
- font-size: 12px;
173
- }
174
-
175
- .epub-reader__toc-list {
176
- list-style: none;
177
- padding: 0;
178
- margin: 0;
179
- display: flex;
180
- flex-direction: column;
181
- gap: 4px;
182
- }
183
-
184
- .epub-reader__toc-item {
185
- margin: 0;
186
- }
187
-
188
- .epub-reader__toc-btn {
189
- width: 100%;
190
- text-align: left;
191
- padding: 8px 10px;
192
- border-radius: 8px;
193
- border: 1px solid var(--epub-reader-border);
194
- background: transparent;
195
- color: inherit;
196
- cursor: pointer;
197
- }
198
-
199
- .epub-reader__toc-btn:hover {
200
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
201
- }
202
-
203
- .epub-reader__toc-details {
204
- border-radius: 8px;
205
- }
206
-
207
- .epub-reader__toc-summary {
208
- padding: 8px 10px;
209
- border-radius: 8px;
210
- cursor: pointer;
211
- border: 1px solid var(--epub-reader-border);
212
- user-select: none;
213
- }
214
-
215
- .epub-reader__toc-details[open] > .epub-reader__toc-summary {
216
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
217
- }
218
-
219
- .epub-reader__toc-details > .epub-reader__toc-list {
220
- padding-left: 12px;
221
- margin-top: 6px;
222
- }
223
-
224
- .epub-reader__field {
225
- display: flex;
226
- align-items: center;
227
- gap: 8px;
228
- }
229
-
230
- .epub-reader__input {
231
- width: 100%;
232
- padding: 8px 10px;
233
- border-radius: 8px;
234
- border: 1px solid var(--epub-reader-border);
235
- background: transparent;
236
- color: inherit;
237
- outline: none;
238
- }
239
-
240
- .epub-reader__checks {
241
- display: flex;
242
- flex-wrap: wrap;
243
- gap: 10px;
244
- margin: 10px 0;
245
- font-size: 12px;
246
- }
247
-
248
- .epub-reader__check {
249
- display: inline-flex;
250
- align-items: center;
251
- gap: 6px;
252
- user-select: none;
253
- }
254
-
255
- .epub-reader__meta {
256
- display: flex;
257
- align-items: center;
258
- justify-content: space-between;
259
- gap: 8px;
260
- font-size: 12px;
261
- opacity: 0.9;
262
- margin: 6px 0 10px;
263
- }
264
-
265
- .epub-reader__link {
266
- appearance: none;
267
- border: none;
268
- background: transparent;
269
- color: var(--epub-reader-accent);
270
- cursor: pointer;
271
- padding: 0;
272
- }
273
-
274
- .epub-reader__search-list {
275
- list-style: none;
276
- padding: 0;
277
- margin: 0;
278
- display: flex;
279
- flex-direction: column;
280
- gap: 6px;
281
- }
282
-
283
- .epub-reader__search-item {
284
- margin: 0;
285
- }
286
-
287
- .epub-reader__search-btn {
288
- width: 100%;
289
- text-align: left;
290
- padding: 10px;
291
- border-radius: 10px;
292
- border: 1px solid var(--epub-reader-border);
293
- background: transparent;
294
- color: inherit;
295
- cursor: pointer;
296
- display: flex;
297
- flex-direction: column;
298
- gap: 6px;
299
- }
300
-
301
- .epub-reader__search-btn:hover {
302
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
303
- }
304
-
305
- .epub-reader__search-label {
306
- font-size: 12px;
307
- opacity: 0.7;
308
- }
309
-
310
- .epub-reader__search-excerpt {
311
- font-size: 12px;
312
- line-height: 1.4;
313
- word-break: break-word;
314
- }
315
-
316
- .epub-reader__bottom {
317
- position: absolute;
318
- left: 0;
319
- right: 0;
320
- bottom: 0;
321
- height: var(--epub-reader-bottom-bar-height);
322
- border-top: 1px solid var(--epub-reader-border);
323
- background: color-mix(in srgb, var(--epub-reader-panel-bg) 92%, transparent);
324
- display: flex;
325
- align-items: center;
326
- justify-content: space-between;
327
- gap: 10px;
328
- padding: 0 12px;
329
- z-index: 10;
330
- backdrop-filter: blur(10px);
331
- }
332
-
333
- .epub-reader__bottom-left {
334
- display: flex;
335
- align-items: center;
336
- gap: 8px;
337
- min-width: 0;
338
- }
339
-
340
- .epub-reader__status {
341
- font-size: 12px;
342
- opacity: 0.8;
343
- white-space: nowrap;
344
- }
345
-
346
- .epub-reader__section {
347
- font-size: 12px;
348
- font-weight: 600;
349
- white-space: nowrap;
350
- overflow: hidden;
351
- text-overflow: ellipsis;
352
- max-width: 280px;
353
- }
354
-
355
- .epub-reader__bottom-right {
356
- display: flex;
357
- align-items: center;
358
- gap: 10px;
359
- min-width: 260px;
360
- }
361
-
362
- .epub-reader__range {
363
- width: 260px;
364
- }
365
-
366
- .epub-reader__percent {
367
- font-size: 12px;
368
- font-variant-numeric: tabular-nums;
369
- min-width: 42px;
370
- text-align: right;
371
- }
372
-
373
- .epub-reader[data-layout='mobile'] .epub-reader__viewer {
374
- height: 100%;
375
- touch-action: pan-x;
376
- }
377
-
378
- .epub-reader[data-layout='mobile'] .epub-reader__toolbar,
379
- .epub-reader[data-layout='mobile'] .epub-reader__bottom,
380
- .epub-reader[data-layout='mobile'] .epub-reader__drawer,
381
- .epub-reader[data-layout='mobile'] .epub-reader__overlay {
382
- display: none;
383
- }
384
-
385
- .epub-reader__mbar {
386
- position: absolute;
387
- left: 10px;
388
- right: 10px;
389
- bottom: 0;
390
- z-index: 15;
391
- display: flex;
392
- align-items: center;
393
- gap: 8px;
394
- padding: 10px;
395
- border: 1px solid var(--epub-reader-border);
396
- border-radius: calc(var(--epub-reader-radius) + 6px);
397
- background: color-mix(in srgb, var(--epub-reader-panel-bg) 92%, transparent);
398
- color: var(--epub-reader-panel-fg);
399
- box-shadow: var(--epub-reader-shadow);
400
- backdrop-filter: blur(10px);
401
- transform: translateY(calc(100% + 10px));
402
- transition: transform 0.2s ease;
403
- }
404
-
405
- .epub-reader__mbar.is-visible {
406
- transform: translateY(-4px);
407
- }
408
-
409
- .epub-reader__mbar .epub-reader__btn {
410
- flex: 1 1 0;
411
- text-align: center;
412
- padding: 10px 8px;
413
- font-size: 13px;
414
- }
415
-
416
- .epub-reader__moverlay {
417
- position: absolute;
418
- inset: 0;
419
- background: rgba(0, 0, 0, 0.35);
420
- z-index: 20;
421
- }
422
-
423
- .epub-reader__msheet {
424
- position: absolute;
425
- left: 0;
426
- right: 0;
427
- bottom: 0;
428
- height: 60%;
429
- max-height: calc(100% - 70px);
430
- background: var(--epub-reader-panel-bg);
431
- color: var(--epub-reader-panel-fg);
432
- border-top: 1px solid var(--epub-reader-border);
433
- border-top-left-radius: calc(var(--epub-reader-radius) + 10px);
434
- border-top-right-radius: calc(var(--epub-reader-radius) + 10px);
435
- transform: translateY(100%);
436
- transition: transform 0.25s ease;
437
- z-index: 30;
438
- display: flex;
439
- flex-direction: column;
440
- pointer-events: none;
441
- }
442
-
443
- .epub-reader__msheet.is-open {
444
- transform: translateY(0);
445
- pointer-events: auto;
446
- }
447
-
448
- .epub-reader__msheet-header {
449
- display: flex;
450
- align-items: center;
451
- justify-content: space-between;
452
- gap: 8px;
453
- padding: 12px 12px 10px;
454
- border-bottom: 1px solid var(--epub-reader-border);
455
- }
456
-
457
- .epub-reader__msheet-title {
458
- font-size: 14px;
459
- font-weight: 800;
460
- }
461
-
462
- .epub-reader__msheet-body {
463
- padding: 12px;
464
- overflow: auto;
465
- flex: 1;
466
- }
467
-
468
- .epub-reader__mprogress {
469
- display: flex;
470
- flex-direction: column;
471
- gap: 10px;
472
- margin-top: 10px;
473
- }
474
-
475
- .epub-reader__mprogress .epub-reader__range {
476
- width: 100%;
477
- }
478
-
479
- .epub-reader__mprogress-percent {
480
- text-align: center;
481
- font-weight: 800;
482
- color: var(--epub-reader-accent);
483
- }
484
-
485
- .epub-reader__mfont {
486
- display: flex;
487
- align-items: center;
488
- justify-content: center;
489
- gap: 12px;
490
- }
491
-
492
- `;
493
-
494
- // src/styles/ensureStyle.ts
495
- var STYLE_ELEMENT_ID = "somecat-epub-reader-style";
496
- var ensureEpubReaderStyle = () => {
497
- if (typeof document === "undefined") return;
498
- if (document.getElementById(STYLE_ELEMENT_ID)) return;
499
- const style = document.createElement("style");
500
- style.id = STYLE_ELEMENT_ID;
501
- style.textContent = epubReaderCssText;
502
- (document.head ?? document.documentElement).append(style);
503
- };
7
+ // src/react/EBookReader.tsx
504
8
  var getContentCSS = (fontSize, isDark, extraCSS) => `
505
9
  @namespace epub "http://www.idpf.org/2007/ops";
506
10
  html {
@@ -708,7 +212,8 @@ var icons = {
708
212
  minus: '<path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
709
213
  x: '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
710
214
  type: '<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
711
- 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"/>'
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
+ settings: '<path fill="currentColor" fill-rule="evenodd" d="M12.563 3.2h-1.126l-.645 2.578l-.647.2a6.3 6.3 0 0 0-1.091.452l-.599.317l-2.28-1.368l-.796.797l1.368 2.28l-.317.598a6.3 6.3 0 0 0-.453 1.091l-.199.647l-2.578.645v1.126l2.578.645l.2.647q.173.568.452 1.091l.317.599l-1.368 2.28l.797.796l2.28-1.368l.598.317q.523.278 1.091.453l.647.199l.645 2.578h1.126l.645-2.578l.647-.2a6.3 6.3 0 0 0 1.091-.452l.599-.317l2.28 1.368l.796-.797l-1.368-2.28l.317-.598q.278-.523.453-1.091l.199-.647l2.578-.645v-1.126l-2.578-.645l-.2-.647a6.3 6.3 0 0 0-.452-1.091l-.317-.599l1.368-2.28l-.797-.796l-2.28 1.368l-.598-.317a6.3 6.3 0 0 0-1.091-.453l-.647-.199zm2.945 2.17l1.833-1.1a1 1 0 0 1 1.221.15l1.018 1.018a1 1 0 0 1 .15 1.221l-1.1 1.833q.33.62.54 1.3l2.073.519a1 1 0 0 1 .757.97v1.438a1 1 0 0 1-.757.97l-2.073.519q-.21.68-.54 1.3l1.1 1.833a1 1 0 0 1-.15 1.221l-1.018 1.018a1 1 0 0 1-1.221.15l-1.833-1.1q-.62.33-1.3.54l-.519 2.073a1 1 0 0 1-.97.757h-1.438a1 1 0 0 1-.97-.757l-.519-2.073a7.5 7.5 0 0 1-1.3-.54l-1.833 1.1a1 1 0 0 1-1.221-.15L4.42 18.562a1 1 0 0 1-.15-1.221l1.1-1.833a7.5 7.5 0 0 1-.54-1.3l-2.073-.519A1 1 0 0 1 2 12.72v-1.438a1 1 0 0 1 .757-.97l2.073-.519q.21-.68.54-1.3L4.27 6.66a1 1 0 0 1 .15-1.221L5.438 4.42a1 1 0 0 1 1.221-.15l1.833 1.1q.62-.33 1.3-.54l.519-2.073A1 1 0 0 1 11.28 2h1.438a1 1 0 0 1 .97.757l.519 2.073q.68.21 1.3.54zM12 14.8a2.8 2.8 0 1 0 0-5.6a2.8 2.8 0 0 0 0 5.6m0 1.2a4 4 0 1 1 0-8a4 4 0 0 1 0 8"/>'
712
217
  };
713
218
  var SvgIcon = ({ name, size = 24, color = "currentColor", className }) => {
714
219
  const iconPath = icons[name] || "";
@@ -782,7 +287,7 @@ var DesktopBottomBar = ({
782
287
  }) => {
783
288
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__bottom", children: [
784
289
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__bottom-left", children: [
785
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5C31\u7EEA" }),
290
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "" }),
786
291
  sectionLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__section", children: sectionLabel }) : null
787
292
  ] }),
788
293
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__bottom-right", children: [
@@ -955,6 +460,7 @@ var MobileUI = ({
955
460
  activePanel,
956
461
  onTogglePanel,
957
462
  onClosePanel,
463
+ toolbarRight,
958
464
  toc,
959
465
  search,
960
466
  status,
@@ -976,9 +482,80 @@ var MobileUI = ({
976
482
  onToggleDarkMode,
977
483
  onFontSizeChange
978
484
  }) => {
979
- const mobileTitle = activePanel === "menu" ? "\u76EE\u5F55" : activePanel === "search" ? "\u641C\u7D22" : activePanel === "progress" ? "\u8FDB\u5EA6" : activePanel === "theme" ? "\u660E\u6697" : activePanel === "font" ? "\u5B57\u53F7" : "";
485
+ const mobileTitle = activePanel === "menu" ? "\u76EE\u5F55" : activePanel === "search" ? "\u641C\u7D22" : activePanel === "progress" ? "\u8FDB\u5EA6" : activePanel === "settings" ? "\u8BBE\u7F6E" : "";
486
+ const displayedFontSize = Math.min(40, Math.max(10, Math.round(fontSize / 5)));
487
+ const [fontSliderValue, setFontSliderValue] = react.useState(displayedFontSize);
488
+ const [isFontDragging, setIsFontDragging] = react.useState(false);
489
+ const fontDebounceRef = react.useRef(null);
490
+ const fontPendingRef = react.useRef(displayedFontSize);
491
+ const fontSliderWrapRef = react.useRef(null);
492
+ const [fontSliderWidth, setFontSliderWidth] = react.useState(0);
493
+ const fontThumbSize = 34;
494
+ const fontMin = 10;
495
+ const fontMax = 40;
496
+ react.useEffect(() => {
497
+ setFontSliderValue(displayedFontSize);
498
+ fontPendingRef.current = displayedFontSize;
499
+ }, [displayedFontSize]);
500
+ react.useEffect(() => {
501
+ return () => {
502
+ if (fontDebounceRef.current) {
503
+ clearTimeout(fontDebounceRef.current);
504
+ fontDebounceRef.current = null;
505
+ }
506
+ };
507
+ }, []);
508
+ react.useEffect(() => {
509
+ if (activePanel !== "settings") return;
510
+ const el = fontSliderWrapRef.current;
511
+ if (!el) return;
512
+ const update = () => setFontSliderWidth(el.getBoundingClientRect().width);
513
+ update();
514
+ const ro = new ResizeObserver(() => update());
515
+ ro.observe(el);
516
+ return () => ro.disconnect();
517
+ }, [activePanel]);
518
+ const fontProgressPercent = (fontSliderValue - fontMin) / (fontMax - fontMin) * 100;
519
+ const fontThumbLeft = (() => {
520
+ if (!fontSliderWidth) return 0;
521
+ const percent = (fontSliderValue - fontMin) / (fontMax - fontMin);
522
+ const half = fontThumbSize / 2;
523
+ return Math.min(fontSliderWidth - half, Math.max(half, half + percent * (fontSliderWidth - fontThumbSize)));
524
+ })();
525
+ const flushFontSize = () => {
526
+ if (fontDebounceRef.current) {
527
+ clearTimeout(fontDebounceRef.current);
528
+ fontDebounceRef.current = null;
529
+ }
530
+ onFontSizeChange(fontPendingRef.current * 5);
531
+ };
532
+ const scheduleFontSize = (next) => {
533
+ fontPendingRef.current = next;
534
+ if (fontDebounceRef.current) clearTimeout(fontDebounceRef.current);
535
+ fontDebounceRef.current = window.setTimeout(() => {
536
+ fontDebounceRef.current = null;
537
+ onFontSizeChange(fontPendingRef.current * 5);
538
+ }, 80);
539
+ };
980
540
  const [tooltip, setTooltip] = react.useState(null);
981
541
  const timerRef = react.useRef(null);
542
+ const ignoreToggleRef = react.useRef(false);
543
+ const markIgnoreToggle = () => {
544
+ ignoreToggleRef.current = true;
545
+ window.setTimeout(() => {
546
+ ignoreToggleRef.current = false;
547
+ }, 350);
548
+ };
549
+ const closePanelSafe = () => {
550
+ onClosePanel();
551
+ handleTouchEnd();
552
+ const el = document.activeElement;
553
+ if (el && el instanceof HTMLElement) el.blur();
554
+ };
555
+ const togglePanelSafe = (panel) => {
556
+ if (ignoreToggleRef.current) return;
557
+ onTogglePanel(panel);
558
+ };
982
559
  const handleTouchStart = (e, text) => {
983
560
  const rect = e.currentTarget.getBoundingClientRect();
984
561
  timerRef.current = window.setTimeout(() => {
@@ -995,6 +572,42 @@ var MobileUI = ({
995
572
  }
996
573
  setTooltip(null);
997
574
  };
575
+ const sheetRef = react.useRef(null);
576
+ const dragRef = react.useRef({ startY: 0, currentY: 0, isDragging: false });
577
+ const handleHeaderTouchStart = (e) => {
578
+ handleTouchEnd();
579
+ dragRef.current.startY = e.touches[0].clientY;
580
+ dragRef.current.isDragging = true;
581
+ if (sheetRef.current) {
582
+ sheetRef.current.style.transition = "none";
583
+ }
584
+ };
585
+ const handleHeaderTouchMove = (e) => {
586
+ if (!dragRef.current.isDragging) return;
587
+ e.preventDefault();
588
+ const deltaY = e.touches[0].clientY - dragRef.current.startY;
589
+ if (deltaY > 0 && sheetRef.current) {
590
+ sheetRef.current.style.transform = `translateY(${deltaY}px)`;
591
+ dragRef.current.currentY = deltaY;
592
+ }
593
+ };
594
+ const handleHeaderTouchEnd = () => {
595
+ if (!dragRef.current.isDragging) return;
596
+ dragRef.current.isDragging = false;
597
+ if (sheetRef.current) {
598
+ sheetRef.current.style.transition = "";
599
+ if (dragRef.current.currentY > 80) {
600
+ markIgnoreToggle();
601
+ closePanelSafe();
602
+ setTimeout(() => {
603
+ if (sheetRef.current) sheetRef.current.style.transform = "";
604
+ }, 300);
605
+ } else {
606
+ sheetRef.current.style.transform = "";
607
+ }
608
+ }
609
+ dragRef.current.currentY = 0;
610
+ };
998
611
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
999
612
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `epub-reader__mbar ${barVisible ? "is-visible" : ""}`, children: [
1000
613
  tooltip && /* @__PURE__ */ jsxRuntime.jsx(
@@ -1019,208 +632,259 @@ var MobileUI = ({
1019
632
  children: tooltip.text
1020
633
  }
1021
634
  ),
1022
- /* @__PURE__ */ jsxRuntime.jsx(
1023
- "button",
1024
- {
1025
- type: "button",
1026
- className: "epub-reader__btn",
1027
- onClick: () => onTogglePanel("menu"),
1028
- "aria-pressed": activePanel === "menu",
1029
- onTouchStart: (e) => handleTouchStart(e, "\u76EE\u5F55"),
1030
- onTouchEnd: handleTouchEnd,
1031
- onTouchCancel: handleTouchEnd,
1032
- title: "\u76EE\u5F55",
1033
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "list" })
1034
- }
1035
- ),
1036
- /* @__PURE__ */ jsxRuntime.jsx(
1037
- "button",
1038
- {
1039
- type: "button",
1040
- className: "epub-reader__btn",
1041
- onClick: () => onTogglePanel("search"),
1042
- "aria-pressed": activePanel === "search",
1043
- onTouchStart: (e) => handleTouchStart(e, "\u641C\u7D22"),
1044
- onTouchEnd: handleTouchEnd,
1045
- onTouchCancel: handleTouchEnd,
1046
- title: "\u641C\u7D22",
1047
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "search" })
1048
- }
1049
- ),
1050
- /* @__PURE__ */ jsxRuntime.jsx(
1051
- "button",
1052
- {
1053
- type: "button",
1054
- className: "epub-reader__btn",
1055
- onClick: () => onTogglePanel("progress"),
1056
- "aria-pressed": activePanel === "progress",
1057
- onTouchStart: (e) => handleTouchStart(e, "\u8FDB\u5EA6"),
1058
- onTouchEnd: handleTouchEnd,
1059
- onTouchCancel: handleTouchEnd,
1060
- title: "\u8FDB\u5EA6",
1061
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "sliders" })
1062
- }
1063
- ),
1064
- /* @__PURE__ */ jsxRuntime.jsx(
1065
- "button",
1066
- {
1067
- type: "button",
1068
- className: "epub-reader__btn",
1069
- onClick: () => onTogglePanel("theme"),
1070
- "aria-pressed": activePanel === "theme",
1071
- onTouchStart: (e) => handleTouchStart(e, "\u660E\u6697"),
1072
- onTouchEnd: handleTouchEnd,
1073
- onTouchCancel: handleTouchEnd,
1074
- title: "\u660E\u6697",
1075
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "sun" })
1076
- }
1077
- ),
1078
- /* @__PURE__ */ jsxRuntime.jsx(
1079
- "button",
1080
- {
1081
- type: "button",
1082
- className: "epub-reader__btn",
1083
- onClick: () => onTogglePanel("font"),
1084
- "aria-pressed": activePanel === "font",
1085
- onTouchStart: (e) => handleTouchStart(e, "\u5B57\u53F7"),
1086
- onTouchEnd: handleTouchEnd,
1087
- onTouchCancel: handleTouchEnd,
1088
- title: "\u5B57\u53F7",
1089
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "type" })
1090
- }
1091
- )
1092
- ] }),
1093
- activePanel ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__moverlay", onClick: onClosePanel }) : null,
1094
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `epub-reader__msheet ${activePanel ? "is-open" : ""}`, "aria-hidden": !activePanel, children: [
1095
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msheet-header", children: [
1096
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__msheet-title", children: mobileTitle }),
1097
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onClosePanel, children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "x" }) })
1098
- ] }),
1099
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msheet-body", children: [
1100
- activePanel === "menu" ? toc.length ? /* @__PURE__ */ jsxRuntime.jsx(
1101
- TocTree,
635
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mbar-left", children: [
636
+ /* @__PURE__ */ jsxRuntime.jsx(
637
+ "button",
1102
638
  {
1103
- items: toc,
1104
- onSelect: (href) => {
1105
- onTocSelect(href);
1106
- onClosePanel();
1107
- }
639
+ type: "button",
640
+ className: "epub-reader__btn",
641
+ onClick: () => togglePanelSafe("menu"),
642
+ "aria-pressed": activePanel === "menu",
643
+ onTouchStart: (e) => handleTouchStart(e, "\u76EE\u5F55"),
644
+ onTouchEnd: handleTouchEnd,
645
+ onTouchCancel: handleTouchEnd,
646
+ title: "\u76EE\u5F55",
647
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "list" })
1108
648
  }
1109
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) : null,
1110
- activePanel === "search" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1111
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__field", children: [
1112
- /* @__PURE__ */ jsxRuntime.jsx(
1113
- "input",
649
+ ),
650
+ /* @__PURE__ */ jsxRuntime.jsx(
651
+ "button",
652
+ {
653
+ type: "button",
654
+ className: "epub-reader__btn",
655
+ onClick: () => togglePanelSafe("search"),
656
+ "aria-pressed": activePanel === "search",
657
+ onTouchStart: (e) => handleTouchStart(e, "\u641C\u7D22"),
658
+ onTouchEnd: handleTouchEnd,
659
+ onTouchCancel: handleTouchEnd,
660
+ title: "\u641C\u7D22",
661
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "search" })
662
+ }
663
+ ),
664
+ /* @__PURE__ */ jsxRuntime.jsx(
665
+ "button",
666
+ {
667
+ type: "button",
668
+ className: "epub-reader__btn",
669
+ onClick: () => togglePanelSafe("progress"),
670
+ "aria-pressed": activePanel === "progress",
671
+ onTouchStart: (e) => handleTouchStart(e, "\u8FDB\u5EA6"),
672
+ onTouchEnd: handleTouchEnd,
673
+ onTouchCancel: handleTouchEnd,
674
+ title: "\u8FDB\u5EA6",
675
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "sliders" })
676
+ }
677
+ ),
678
+ /* @__PURE__ */ jsxRuntime.jsx(
679
+ "button",
680
+ {
681
+ type: "button",
682
+ className: "epub-reader__btn",
683
+ onClick: () => togglePanelSafe("settings"),
684
+ "aria-pressed": activePanel === "settings",
685
+ onTouchStart: (e) => handleTouchStart(e, "\u8BBE\u7F6E"),
686
+ onTouchEnd: handleTouchEnd,
687
+ onTouchCancel: handleTouchEnd,
688
+ title: "\u8BBE\u7F6E",
689
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "settings" })
690
+ }
691
+ )
692
+ ] }),
693
+ toolbarRight ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__mbar-right", children: toolbarRight }) : null
694
+ ] }),
695
+ activePanel ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__moverlay", onClick: closePanelSafe }) : null,
696
+ /* @__PURE__ */ jsxRuntime.jsxs(
697
+ "div",
698
+ {
699
+ ref: sheetRef,
700
+ className: `epub-reader__msheet ${activePanel ? "is-open" : ""}`,
701
+ "aria-hidden": !activePanel,
702
+ children: [
703
+ /* @__PURE__ */ jsxRuntime.jsxs(
704
+ "div",
705
+ {
706
+ className: "epub-reader__msheet-header",
707
+ onTouchStart: handleHeaderTouchStart,
708
+ onTouchMove: handleHeaderTouchMove,
709
+ onTouchEnd: handleHeaderTouchEnd,
710
+ children: [
711
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__msheet-title", children: mobileTitle }),
712
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: closePanelSafe, children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "x" }) })
713
+ ]
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msheet-body", children: [
717
+ activePanel === "menu" ? toc.length ? /* @__PURE__ */ jsxRuntime.jsx(
718
+ TocTree,
1114
719
  {
1115
- className: "epub-reader__input",
1116
- placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
1117
- value: search.query,
1118
- onChange: (e) => {
1119
- const v = e.target.value;
1120
- onSearchQueryChange(v);
1121
- if (!v.trim()) onSearch("");
1122
- },
1123
- disabled: status !== "ready",
1124
- onKeyDown: (e) => {
1125
- if (e.key === "Enter") onSearch(search.query);
720
+ items: toc,
721
+ onSelect: (href) => {
722
+ onTocSelect(href);
723
+ closePanelSafe();
1126
724
  }
1127
725
  }
1128
- ),
1129
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: "\u641C\u7D22" })
1130
- ] }),
1131
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__checks", children: [
1132
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
1133
- /* @__PURE__ */ jsxRuntime.jsx(
1134
- "input",
1135
- {
1136
- type: "checkbox",
1137
- checked: Boolean(search.options.matchCase),
1138
- onChange: (e) => onSearchOptionChange({ matchCase: e.target.checked })
1139
- }
1140
- ),
1141
- "\u533A\u5206\u5927\u5C0F\u5199"
1142
- ] }),
1143
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
726
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) : null,
727
+ activePanel === "search" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
728
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__field", children: [
729
+ /* @__PURE__ */ jsxRuntime.jsx(
730
+ "input",
731
+ {
732
+ className: "epub-reader__input",
733
+ placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
734
+ value: search.query,
735
+ onChange: (e) => {
736
+ const v = e.target.value;
737
+ onSearchQueryChange(v);
738
+ if (!v.trim()) onSearch("");
739
+ },
740
+ disabled: status !== "ready",
741
+ onKeyDown: (e) => {
742
+ if (e.key === "Enter") onSearch(search.query);
743
+ }
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: "\u641C\u7D22" })
747
+ ] }),
748
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__checks", children: [
749
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
750
+ /* @__PURE__ */ jsxRuntime.jsx(
751
+ "input",
752
+ {
753
+ type: "checkbox",
754
+ checked: Boolean(search.options.matchCase),
755
+ onChange: (e) => onSearchOptionChange({ matchCase: e.target.checked })
756
+ }
757
+ ),
758
+ "\u533A\u5206\u5927\u5C0F\u5199"
759
+ ] }),
760
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
761
+ /* @__PURE__ */ jsxRuntime.jsx(
762
+ "input",
763
+ {
764
+ type: "checkbox",
765
+ checked: Boolean(search.options.wholeWords),
766
+ onChange: (e) => onSearchOptionChange({ wholeWords: e.target.checked })
767
+ }
768
+ ),
769
+ "\u5168\u8BCD\u5339\u914D"
770
+ ] }),
771
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
772
+ /* @__PURE__ */ jsxRuntime.jsx(
773
+ "input",
774
+ {
775
+ type: "checkbox",
776
+ checked: Boolean(search.options.matchDiacritics),
777
+ onChange: (e) => onSearchOptionChange({ matchDiacritics: e.target.checked })
778
+ }
779
+ ),
780
+ "\u533A\u5206\u53D8\u97F3"
781
+ ] })
782
+ ] }),
783
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__meta", children: [
784
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
785
+ "\u8FDB\u5EA6 ",
786
+ search.progressPercent,
787
+ "%"
788
+ ] }),
789
+ search.searching ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
790
+ search.searching ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
791
+ ] }),
792
+ search.results.length ? /* @__PURE__ */ jsxRuntime.jsx(SearchResultList, { results: search.results, onSelect: onSearchResultSelect }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
793
+ ] }) : null,
794
+ activePanel === "progress" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
795
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__meta", children: [
796
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "" }),
797
+ sectionLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: sectionLabel }) : null
798
+ ] }),
799
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mprogress", children: [
800
+ /* @__PURE__ */ jsxRuntime.jsx(
801
+ "input",
802
+ {
803
+ className: "epub-reader__range",
804
+ type: "range",
805
+ min: 0,
806
+ max: 100,
807
+ step: 1,
808
+ value: displayedPercent,
809
+ onChange: (e) => {
810
+ onSeekStart();
811
+ onSeekChange(Number(e.target.value));
812
+ },
813
+ onPointerUp: (e) => {
814
+ const v = Number(e.target.value);
815
+ onSeekEnd(v);
816
+ },
817
+ onKeyUp: (e) => {
818
+ if (e.key !== "Enter") return;
819
+ const v = Number(e.target.value);
820
+ onSeekCommit(v);
821
+ }
822
+ }
823
+ ),
824
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mprogress-percent", children: [
825
+ displayedPercent,
826
+ "%"
827
+ ] })
828
+ ] })
829
+ ] }) : null,
830
+ activePanel === "settings" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msettings", children: [
831
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mfont-range", children: [
832
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__mfont-a is-small", children: "A" }),
833
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: fontSliderWrapRef, className: `epub-reader__mfont-slider ${isFontDragging ? "is-dragging" : ""}`, children: [
834
+ /* @__PURE__ */ jsxRuntime.jsx(
835
+ "input",
836
+ {
837
+ className: "epub-reader__range",
838
+ type: "range",
839
+ min: fontMin,
840
+ max: fontMax,
841
+ step: 1,
842
+ value: fontSliderValue,
843
+ onChange: (e) => {
844
+ const next = Number(e.target.value);
845
+ setFontSliderValue(next);
846
+ scheduleFontSize(next);
847
+ },
848
+ onPointerDown: () => setIsFontDragging(true),
849
+ onPointerUp: () => {
850
+ setIsFontDragging(false);
851
+ flushFontSize();
852
+ },
853
+ onPointerCancel: () => {
854
+ setIsFontDragging(false);
855
+ flushFontSize();
856
+ },
857
+ onKeyUp: (e) => {
858
+ if (e.key !== "Enter") return;
859
+ flushFontSize();
860
+ },
861
+ style: {
862
+ background: `linear-gradient(to right, var(--epub-reader-range-fill) 0%, var(--epub-reader-range-fill) ${fontProgressPercent}%, var(--epub-reader-range-track) ${fontProgressPercent}%, var(--epub-reader-range-track) 100%)`
863
+ },
864
+ "aria-label": "\u5B57\u53F7"
865
+ }
866
+ ),
867
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__mfont-thumb", style: { left: `${fontThumbLeft}px`, width: `${fontThumbSize}px`, height: `${fontThumbSize}px` }, children: fontSliderValue })
868
+ ] }),
869
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__mfont-a is-big", children: "A" })
870
+ ] }),
1144
871
  /* @__PURE__ */ jsxRuntime.jsx(
1145
- "input",
872
+ "button",
1146
873
  {
1147
- type: "checkbox",
1148
- checked: Boolean(search.options.wholeWords),
1149
- onChange: (e) => onSearchOptionChange({ wholeWords: e.target.checked })
874
+ type: "button",
875
+ className: "epub-reader__btn",
876
+ onClick: () => onToggleDarkMode(!darkMode),
877
+ "aria-pressed": darkMode,
878
+ "aria-label": darkMode ? "\u6697\u9ED1\u6A21\u5F0F\uFF1A\u5F00\uFF0C\u70B9\u51FB\u5207\u6362\u5230\u4EAE\u8272" : "\u6697\u9ED1\u6A21\u5F0F\uFF1A\u5173\uFF0C\u70B9\u51FB\u5207\u6362\u5230\u6697\u9ED1",
879
+ title: darkMode ? "\u5207\u6362\u5230\u4EAE\u8272" : "\u5207\u6362\u5230\u6697\u9ED1",
880
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: darkMode ? "sun" : "moon" })
1150
881
  }
1151
- ),
1152
- "\u5168\u8BCD\u5339\u914D"
1153
- ] }),
1154
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
1155
- /* @__PURE__ */ jsxRuntime.jsx(
1156
- "input",
1157
- {
1158
- type: "checkbox",
1159
- checked: Boolean(search.options.matchDiacritics),
1160
- onChange: (e) => onSearchOptionChange({ matchDiacritics: e.target.checked })
1161
- }
1162
- ),
1163
- "\u533A\u5206\u53D8\u97F3"
1164
- ] })
1165
- ] }),
1166
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__meta", children: [
1167
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1168
- "\u8FDB\u5EA6 ",
1169
- search.progressPercent,
1170
- "%"
1171
- ] }),
1172
- search.searching ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
1173
- search.searching ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
1174
- ] }),
1175
- search.results.length ? /* @__PURE__ */ jsxRuntime.jsx(SearchResultList, { results: search.results, onSelect: onSearchResultSelect }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
1176
- ] }) : null,
1177
- activePanel === "progress" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1178
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__meta", children: [
1179
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5C31\u7EEA" }),
1180
- sectionLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: sectionLabel }) : null
1181
- ] }),
1182
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mprogress", children: [
1183
- /* @__PURE__ */ jsxRuntime.jsx(
1184
- "input",
1185
- {
1186
- className: "epub-reader__range",
1187
- type: "range",
1188
- min: 0,
1189
- max: 100,
1190
- step: 1,
1191
- value: displayedPercent,
1192
- onChange: (e) => {
1193
- onSeekStart();
1194
- onSeekChange(Number(e.target.value));
1195
- },
1196
- onPointerUp: (e) => {
1197
- const v = Number(e.target.value);
1198
- onSeekEnd(v);
1199
- },
1200
- onKeyUp: (e) => {
1201
- if (e.key !== "Enter") return;
1202
- const v = Number(e.target.value);
1203
- onSeekCommit(v);
1204
- }
1205
- }
1206
- ),
1207
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mprogress-percent", children: [
1208
- displayedPercent,
1209
- "%"
1210
- ] })
882
+ )
883
+ ] }) : null
1211
884
  ] })
1212
- ] }) : null,
1213
- activePanel === "theme" ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onToggleDarkMode(!darkMode), children: darkMode ? "\u5207\u6362\u5230\u4EAE\u8272" : "\u5207\u6362\u5230\u6697\u9ED1" }) : null,
1214
- activePanel === "font" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mfont", children: [
1215
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize - 10), children: "A-" }),
1216
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__font", children: [
1217
- fontSize,
1218
- "%"
1219
- ] }),
1220
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize + 10), children: "A+" })
1221
- ] }) : null
1222
- ] })
1223
- ] })
885
+ ]
886
+ }
887
+ )
1224
888
  ] });
1225
889
  };
1226
890
  var MOBILE_MAX_WIDTH = 768;
@@ -1271,6 +935,7 @@ var EBookReader = react.forwardRef(function EBookReader2({
1271
935
  fileUrl,
1272
936
  className,
1273
937
  style,
938
+ mobileToolbarRight,
1274
939
  defaultFontSize = 100,
1275
940
  fontSize: controlledFontSize,
1276
941
  onFontSizeChange,
@@ -1294,6 +959,7 @@ var EBookReader = react.forwardRef(function EBookReader2({
1294
959
  const [progressInfo, setProgressInfo] = react.useState(null);
1295
960
  const [isSeeking, setIsSeeking] = react.useState(false);
1296
961
  const [seekPercent, setSeekPercent] = react.useState(0);
962
+ const [downloadLoading, setDownloadLoading] = react.useState(null);
1297
963
  const [uncontrolledFontSize, setUncontrolledFontSize] = react.useState(defaultFontSize);
1298
964
  const [uncontrolledDarkMode, setUncontrolledDarkMode] = react.useState(defaultDarkMode);
1299
965
  const fontSize = controlledFontSize ?? uncontrolledFontSize;
@@ -1311,6 +977,7 @@ var EBookReader = react.forwardRef(function EBookReader2({
1311
977
  const layoutRef = react.useRef(layout);
1312
978
  const boundDocsRef = react.useRef(/* @__PURE__ */ new WeakSet());
1313
979
  const gestureRef = react.useRef({ startX: 0, startY: 0, startAt: 0, tracking: false, moved: false, actionTaken: false });
980
+ const isDraggingRef = react.useRef(false);
1314
981
  const percentage = react.useMemo(() => Math.round((progressInfo?.fraction ?? 0) * 100), [progressInfo]);
1315
982
  const displayedPercent = isSeeking ? seekPercent : percentage;
1316
983
  const sectionLabel = progressInfo?.tocItem?.label ?? "";
@@ -1500,6 +1167,9 @@ var EBookReader = react.forwardRef(function EBookReader2({
1500
1167
  onError: (e) => onError?.(e),
1501
1168
  onProgress: (info) => {
1502
1169
  setProgressInfo(info);
1170
+ if (!isDraggingRef.current) {
1171
+ setIsSeeking(false);
1172
+ }
1503
1173
  onProgress?.(info);
1504
1174
  },
1505
1175
  onToc: (items) => setToc(items),
@@ -1538,18 +1208,28 @@ var EBookReader = react.forwardRef(function EBookReader2({
1538
1208
  if (!nextUrl) return;
1539
1209
  const controller = new AbortController();
1540
1210
  prepareOpen();
1211
+ setDownloadLoading("download");
1212
+ let active = true;
1541
1213
  void (async () => {
1542
1214
  try {
1543
1215
  const downloaded = await downloadEpubAsFile(nextUrl, controller.signal);
1216
+ if (!active) return;
1217
+ setDownloadLoading("open");
1544
1218
  await handleOpenFile(downloaded);
1545
1219
  } catch (e) {
1546
1220
  if (e?.name === "AbortError") return;
1547
1221
  setStatus("error");
1548
1222
  setErrorText(e?.message ? String(e.message) : "\u4E0B\u8F7D\u5931\u8D25");
1549
1223
  onError?.(e);
1224
+ } finally {
1225
+ if (active) setDownloadLoading(null);
1550
1226
  }
1551
1227
  })();
1552
- return () => controller.abort();
1228
+ return () => {
1229
+ active = false;
1230
+ setDownloadLoading(null);
1231
+ controller.abort();
1232
+ };
1553
1233
  }, [file, fileUrl, handleOpenFile, onError, prepareOpen]);
1554
1234
  react.useEffect(() => {
1555
1235
  readerRef.current?.setDarkMode(darkMode);
@@ -1586,10 +1266,13 @@ var EBookReader = react.forwardRef(function EBookReader2({
1586
1266
  }),
1587
1267
  []
1588
1268
  );
1589
- const handleSeekStart = react.useCallback(() => setIsSeeking(true), []);
1269
+ const handleSeekStart = react.useCallback(() => {
1270
+ setIsSeeking(true);
1271
+ isDraggingRef.current = true;
1272
+ }, []);
1590
1273
  const handleSeekChange = react.useCallback((v) => setSeekPercent(v), []);
1591
1274
  const handleSeekEnd = react.useCallback((v) => {
1592
- setIsSeeking(false);
1275
+ isDraggingRef.current = false;
1593
1276
  readerRef.current?.goToFraction(v / 100);
1594
1277
  }, []);
1595
1278
  const handleTocSelect = react.useCallback((href) => {
@@ -1607,8 +1290,13 @@ var EBookReader = react.forwardRef(function EBookReader2({
1607
1290
  "data-theme": darkMode ? "dark" : "light",
1608
1291
  "data-layout": layout,
1609
1292
  tabIndex: 0,
1293
+ "aria-busy": downloadLoading != null,
1610
1294
  children: [
1611
1295
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__viewer", ref: viewerHostRef }),
1296
+ downloadLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__loading", role: "status", "aria-live": "polite", children: [
1297
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__spinner", "aria-hidden": "true" }),
1298
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__loading-text", children: downloadLoading === "download" ? "\u52A0\u8F7D\u4E2D\u2026" : "\u6E32\u67D3\u4E2D\u2026" })
1299
+ ] }) : null,
1612
1300
  layout === "mobile" ? /* @__PURE__ */ jsxRuntime.jsx(
1613
1301
  MobileUI,
1614
1302
  {
@@ -1616,6 +1304,7 @@ var EBookReader = react.forwardRef(function EBookReader2({
1616
1304
  activePanel: mobilePanel,
1617
1305
  onTogglePanel: toggleMobilePanel,
1618
1306
  onClosePanel: closeMobileSheet,
1307
+ toolbarRight: mobileToolbarRight,
1619
1308
  toc,
1620
1309
  search,
1621
1310
  status,
@@ -1696,9 +1385,6 @@ var EBookReader = react.forwardRef(function EBookReader2({
1696
1385
  );
1697
1386
  });
1698
1387
 
1699
- // src/react/index.ts
1700
- ensureEpubReaderStyle();
1701
-
1702
1388
  exports.EBookReader = EBookReader;
1703
1389
  //# sourceMappingURL=react.cjs.map
1704
1390
  //# sourceMappingURL=react.cjs.map