@libraz/mejiro-react 0.9.0 → 0.9.1
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/index.d.ts +313 -123
- package/dist/index.js +1556 -1201
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -61,7 +61,9 @@ function MejiroChapterNav({
|
|
|
61
61
|
const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
|
|
62
62
|
const preview = ch.paragraphs.find((p) => !p.headingLevel && p.text.trim())?.text;
|
|
63
63
|
const chapterKey = `${title}-${ch.paragraphs.map((p) => p.text).join("|").slice(0, 120)}`;
|
|
64
|
-
const headings = ch.paragraphs.
|
|
64
|
+
const headings = ch.paragraphs.flatMap(
|
|
65
|
+
(p, paragraph) => p.headingLevel && p.text.trim() && p.text.trim() !== title ? [{ paragraph, text: p.text }] : []
|
|
66
|
+
).slice(0, 3);
|
|
65
67
|
return /* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
|
|
66
68
|
"button",
|
|
67
69
|
{
|
|
@@ -74,7 +76,7 @@ function MejiroChapterNav({
|
|
|
74
76
|
/* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
|
|
75
77
|
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
|
|
76
78
|
preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
|
|
77
|
-
headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.
|
|
79
|
+
headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.paragraph)) })
|
|
78
80
|
] })
|
|
79
81
|
]
|
|
80
82
|
}
|
|
@@ -91,6 +93,17 @@ function MejiroChapterNav({
|
|
|
91
93
|
// src/MejiroDropZone.tsx
|
|
92
94
|
import { useRef, useState } from "react";
|
|
93
95
|
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
96
|
+
function matchesAccept(accept, file) {
|
|
97
|
+
const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter(Boolean);
|
|
98
|
+
if (tokens.length === 0) return true;
|
|
99
|
+
const name = file.name.toLowerCase();
|
|
100
|
+
const type = file.type.toLowerCase();
|
|
101
|
+
return tokens.some((token) => {
|
|
102
|
+
if (token.startsWith(".")) return name.endsWith(token);
|
|
103
|
+
if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
|
|
104
|
+
return type === token;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
94
107
|
function MejiroDropZone({
|
|
95
108
|
accept = ".epub",
|
|
96
109
|
validateFile,
|
|
@@ -104,7 +117,7 @@ function MejiroDropZone({
|
|
|
104
117
|
const [dragover, setDragover] = useState(false);
|
|
105
118
|
const isValid = (file) => {
|
|
106
119
|
if (validateFile) return validateFile(file);
|
|
107
|
-
return file
|
|
120
|
+
return matchesAccept(accept, file);
|
|
108
121
|
};
|
|
109
122
|
const rootClass = ["mejiro-reader-drop-zone", dragover ? "is-dragover" : "", className ?? ""].filter(Boolean).join(" ");
|
|
110
123
|
return /* @__PURE__ */ jsxs2(
|
|
@@ -160,7 +173,30 @@ import {
|
|
|
160
173
|
EditableEpub,
|
|
161
174
|
exportEditableEpub
|
|
162
175
|
} from "@libraz/mejiro/epub";
|
|
163
|
-
import { useCallback as useCallback7, useEffect as
|
|
176
|
+
import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef11, useState as useState9 } from "react";
|
|
177
|
+
|
|
178
|
+
// src/errors.ts
|
|
179
|
+
import { assertEpubInputSize } from "@libraz/mejiro/epub";
|
|
180
|
+
function toError(cause) {
|
|
181
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
182
|
+
}
|
|
183
|
+
async function fetchEpubBuffer(url, init) {
|
|
184
|
+
const res = init ? await fetch(url, init) : await fetch(url);
|
|
185
|
+
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
186
|
+
return res.arrayBuffer();
|
|
187
|
+
}
|
|
188
|
+
async function readEpubFile(file, limits) {
|
|
189
|
+
assertEpubInputSize(file.size, limits);
|
|
190
|
+
return file.arrayBuffer();
|
|
191
|
+
}
|
|
192
|
+
async function withErrorReporting(task, report) {
|
|
193
|
+
try {
|
|
194
|
+
return await task();
|
|
195
|
+
} catch (cause) {
|
|
196
|
+
report(toError(cause));
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
164
200
|
|
|
165
201
|
// src/MejiroReader.tsx
|
|
166
202
|
import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
|
|
@@ -169,12 +205,12 @@ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
|
|
|
169
205
|
import {
|
|
170
206
|
forwardRef,
|
|
171
207
|
useCallback as useCallback6,
|
|
172
|
-
useEffect as
|
|
208
|
+
useEffect as useEffect8,
|
|
173
209
|
useImperativeHandle,
|
|
174
210
|
useLayoutEffect as useLayoutEffect4,
|
|
175
211
|
useMemo as useMemo4,
|
|
176
|
-
useRef as
|
|
177
|
-
useState as
|
|
212
|
+
useRef as useRef10,
|
|
213
|
+
useState as useState8
|
|
178
214
|
} from "react";
|
|
179
215
|
|
|
180
216
|
// src/MejiroPageIndicator.tsx
|
|
@@ -190,14 +226,59 @@ function MejiroPageIndicator({ current, total }) {
|
|
|
190
226
|
// src/MejiroScrollView.tsx
|
|
191
227
|
import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
|
|
192
228
|
import {
|
|
193
|
-
useEffect,
|
|
229
|
+
useEffect as useEffect2,
|
|
194
230
|
useLayoutEffect,
|
|
195
|
-
useMemo as
|
|
196
|
-
useRef as
|
|
231
|
+
useMemo as useMemo3,
|
|
232
|
+
useRef as useRef3
|
|
197
233
|
} from "react";
|
|
198
234
|
|
|
235
|
+
// src/MejiroImageOverlay.tsx
|
|
236
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
237
|
+
function MejiroImageOverlay({
|
|
238
|
+
rect,
|
|
239
|
+
label,
|
|
240
|
+
onOverlayPointerDown,
|
|
241
|
+
onResizePointerDown,
|
|
242
|
+
onClose
|
|
243
|
+
}) {
|
|
244
|
+
const messages = useI18n();
|
|
245
|
+
const resolvedLabel = label ?? messages.imageButton;
|
|
246
|
+
return /* @__PURE__ */ jsxs4(
|
|
247
|
+
"div",
|
|
248
|
+
{
|
|
249
|
+
className: "mejiro-reader-image-overlay",
|
|
250
|
+
style: {
|
|
251
|
+
left: rect.x,
|
|
252
|
+
top: rect.y,
|
|
253
|
+
width: rect.w,
|
|
254
|
+
height: rect.h
|
|
255
|
+
},
|
|
256
|
+
onPointerDown: onOverlayPointerDown,
|
|
257
|
+
children: [
|
|
258
|
+
/* @__PURE__ */ jsxs4("div", { className: "mejiro-reader-image-overlay-label", children: [
|
|
259
|
+
/* @__PURE__ */ jsx4("div", { className: "mejiro-reader-image-overlay-icon" }),
|
|
260
|
+
/* @__PURE__ */ jsx4("span", { children: resolvedLabel })
|
|
261
|
+
] }),
|
|
262
|
+
/* @__PURE__ */ jsx4("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
|
|
263
|
+
/* @__PURE__ */ jsx4(
|
|
264
|
+
"button",
|
|
265
|
+
{
|
|
266
|
+
type: "button",
|
|
267
|
+
"aria-label": messages.imageRemoveButton,
|
|
268
|
+
title: messages.imageRemoveButton,
|
|
269
|
+
className: "mejiro-reader-image-overlay-close",
|
|
270
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
271
|
+
onClick: () => onClose?.()
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
199
279
|
// src/MejiroPageView.tsx
|
|
200
280
|
import { normalizeFontFamily } from "@libraz/mejiro/browser";
|
|
281
|
+
import { paragraphClassName as paragraphClassName2 } from "@libraz/mejiro/render";
|
|
201
282
|
|
|
202
283
|
// src/MejiroPage.tsx
|
|
203
284
|
import { paragraphClassName } from "@libraz/mejiro/render";
|
|
@@ -207,12 +288,12 @@ import {
|
|
|
207
288
|
segmentToInlineNode
|
|
208
289
|
} from "@libraz/mejiro/render";
|
|
209
290
|
import { createElement, Fragment as Fragment2 } from "react";
|
|
210
|
-
import { jsx as
|
|
291
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
211
292
|
function renderSegment(segment, key) {
|
|
212
293
|
return renderInlineNode(segmentToInlineNode(segment), key);
|
|
213
294
|
}
|
|
214
295
|
function renderInlineNode(node, key) {
|
|
215
|
-
if (node.type === "text") return /* @__PURE__ */
|
|
296
|
+
if (node.type === "text") return /* @__PURE__ */ jsx5(Fragment2, { children: node.text }, key);
|
|
216
297
|
return createElement(
|
|
217
298
|
node.tag,
|
|
218
299
|
{
|
|
@@ -226,33 +307,36 @@ function renderInlineNode(node, key) {
|
|
|
226
307
|
}
|
|
227
308
|
|
|
228
309
|
// src/MejiroPage.tsx
|
|
229
|
-
import { jsx as
|
|
310
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
230
311
|
function renderLine(line, lineIndex) {
|
|
231
312
|
const nodes = [];
|
|
232
313
|
if (lineIndex > 0) {
|
|
233
|
-
nodes.push(/* @__PURE__ */
|
|
314
|
+
nodes.push(/* @__PURE__ */ jsx6("br", {}, `br-${lineIndex}`));
|
|
234
315
|
}
|
|
235
316
|
for (let i = 0; i < line.segments.length; i++) {
|
|
236
317
|
nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
|
|
237
318
|
}
|
|
238
319
|
return nodes;
|
|
239
320
|
}
|
|
321
|
+
function paragraphScaleStyle(scale) {
|
|
322
|
+
return scale == null ? void 0 : { "--mejiro-paragraph-scale": scale };
|
|
323
|
+
}
|
|
240
324
|
function MejiroPage({ page, className, style }) {
|
|
241
325
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
242
|
-
return /* @__PURE__ */
|
|
326
|
+
return /* @__PURE__ */ jsx6("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
|
|
243
327
|
const paraClass = paragraphClassName(
|
|
244
328
|
paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
|
|
245
329
|
paragraph.headingLevel
|
|
246
330
|
);
|
|
247
331
|
return (
|
|
248
332
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
249
|
-
/* @__PURE__ */
|
|
333
|
+
/* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
250
334
|
);
|
|
251
335
|
}) });
|
|
252
336
|
}
|
|
253
337
|
|
|
254
338
|
// src/MejiroPageView.tsx
|
|
255
|
-
import { jsx as
|
|
339
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
256
340
|
function MejiroPageView({
|
|
257
341
|
result,
|
|
258
342
|
fontFamily,
|
|
@@ -264,33 +348,307 @@ function MejiroPageView({
|
|
|
264
348
|
if (result.hasImages || slotMode) {
|
|
265
349
|
const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
|
|
266
350
|
const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
|
|
267
|
-
return /* @__PURE__ */
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
351
|
+
return /* @__PURE__ */ jsx7(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
className: rootClass,
|
|
355
|
+
style: { position: "relative", fontFamily: fontFamilyCss, ...style },
|
|
356
|
+
children: result.lines.map((line, i) => {
|
|
357
|
+
const slot = result.slots[i];
|
|
358
|
+
if (!slot || slot.height <= 0) return null;
|
|
359
|
+
const colStyle = {
|
|
360
|
+
position: "absolute",
|
|
361
|
+
writingMode: "vertical-rl",
|
|
362
|
+
overflow: "hidden",
|
|
363
|
+
margin: 0,
|
|
364
|
+
right: slot.xPos,
|
|
365
|
+
top: slot.yStart,
|
|
366
|
+
height: slot.height,
|
|
367
|
+
fontSize: line.fontSize,
|
|
368
|
+
lineHeight: lineSpacing
|
|
369
|
+
};
|
|
370
|
+
return /* @__PURE__ */ jsx7(
|
|
371
|
+
"div",
|
|
372
|
+
{
|
|
373
|
+
className: paragraphClassName2(line.kind, line.headingLevel),
|
|
374
|
+
style: colStyle,
|
|
375
|
+
children: line.segments.map((seg, si) => renderSegment(seg, `${i}-${si}`))
|
|
376
|
+
},
|
|
377
|
+
i
|
|
378
|
+
);
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return /* @__PURE__ */ jsx7(MejiroPage, { page: result.page, className, style });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/MejiroSelectionLayer.tsx
|
|
387
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
388
|
+
function MejiroSelectionLayer({
|
|
389
|
+
rects,
|
|
390
|
+
side,
|
|
391
|
+
className,
|
|
392
|
+
style,
|
|
393
|
+
rectClassName,
|
|
394
|
+
rectStyle
|
|
395
|
+
}) {
|
|
396
|
+
const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
|
|
397
|
+
const rectClass = rectClassName ?? "mejiro-selection-rect";
|
|
398
|
+
return /* @__PURE__ */ jsx8(
|
|
399
|
+
"div",
|
|
400
|
+
{
|
|
401
|
+
className: wrapperClass,
|
|
402
|
+
style: {
|
|
271
403
|
position: "absolute",
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
404
|
+
inset: 0,
|
|
405
|
+
pointerEvents: "none",
|
|
406
|
+
...style
|
|
407
|
+
},
|
|
408
|
+
children: rects.map((r, i) => {
|
|
409
|
+
if (r.side !== side) return null;
|
|
410
|
+
const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
|
|
411
|
+
return /* @__PURE__ */ jsx8(
|
|
412
|
+
"div",
|
|
413
|
+
{
|
|
414
|
+
className: rectClass,
|
|
415
|
+
style: {
|
|
416
|
+
position: "absolute",
|
|
417
|
+
width: r.width,
|
|
418
|
+
height: r.height,
|
|
419
|
+
...posStyle,
|
|
420
|
+
...r.color != null ? { backgroundColor: r.color } : null,
|
|
421
|
+
...rectStyle
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
i
|
|
425
|
+
);
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/useMultiImageOverlay.ts
|
|
432
|
+
import { createOverlayDragSession, isPrimaryPointerPress } from "@libraz/mejiro/browser";
|
|
433
|
+
import {
|
|
434
|
+
useCallback,
|
|
435
|
+
useEffect,
|
|
436
|
+
useMemo as useMemo2,
|
|
437
|
+
useRef as useRef2,
|
|
438
|
+
useState as useState2
|
|
439
|
+
} from "react";
|
|
440
|
+
var nextId = 0;
|
|
441
|
+
function applyImages(layout, images, spreads, margin) {
|
|
442
|
+
for (const si of spreads) {
|
|
443
|
+
const bookImages = (images.get(si) ?? []).map((it) => ({
|
|
444
|
+
x: it.rect.x,
|
|
445
|
+
y: it.rect.y,
|
|
446
|
+
w: it.rect.w,
|
|
447
|
+
h: it.rect.h,
|
|
448
|
+
margin
|
|
449
|
+
}));
|
|
450
|
+
layout.setImages(si, bookImages);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function overlayPageSide(rect) {
|
|
454
|
+
return rect.x + rect.w / 2 < 0 ? "left" : "right";
|
|
455
|
+
}
|
|
456
|
+
function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
457
|
+
const defW = options.defaultWidth ?? 120;
|
|
458
|
+
const defH = options.defaultHeight ?? 160;
|
|
459
|
+
const defX = options.defaultX ?? 80;
|
|
460
|
+
const defY = options.defaultY ?? 100;
|
|
461
|
+
const margin = options.margin;
|
|
462
|
+
const [images, setImages] = useState2(() => /* @__PURE__ */ new Map());
|
|
463
|
+
const imagesRef = useRef2(images);
|
|
464
|
+
imagesRef.current = images;
|
|
465
|
+
const layoutRef = useRef2(null);
|
|
466
|
+
layoutRef.current = layout;
|
|
467
|
+
const spreadIdxRef = useRef2(spreadIdx);
|
|
468
|
+
spreadIdxRef.current = spreadIdx;
|
|
469
|
+
const onUpdateRef = useRef2(options.onUpdate);
|
|
470
|
+
onUpdateRef.current = options.onUpdate;
|
|
471
|
+
const activeDragCleanupsRef = useRef2(/* @__PURE__ */ new Set());
|
|
472
|
+
useEffect(
|
|
473
|
+
() => () => {
|
|
474
|
+
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
475
|
+
activeDragCleanupsRef.current.clear();
|
|
476
|
+
},
|
|
477
|
+
[]
|
|
478
|
+
);
|
|
479
|
+
const syncSpreads = useCallback(
|
|
480
|
+
(spreads) => {
|
|
481
|
+
const lo = layoutRef.current;
|
|
482
|
+
if (!lo) return;
|
|
483
|
+
applyImages(lo, imagesRef.current, spreads, margin);
|
|
484
|
+
for (const si of spreads) onUpdateRef.current?.(lo.getSpread(si));
|
|
485
|
+
},
|
|
486
|
+
[margin]
|
|
487
|
+
);
|
|
488
|
+
const appliedRef = useRef2({
|
|
489
|
+
layout: null,
|
|
490
|
+
margin
|
|
491
|
+
});
|
|
492
|
+
if (layout && (appliedRef.current.layout !== layout || appliedRef.current.margin !== margin)) {
|
|
493
|
+
appliedRef.current = { layout, margin };
|
|
494
|
+
applyImages(layout, imagesRef.current, [...imagesRef.current.keys()], margin);
|
|
495
|
+
}
|
|
496
|
+
const commit = useCallback(
|
|
497
|
+
(next, affectedSpread) => {
|
|
498
|
+
imagesRef.current = next;
|
|
499
|
+
setImages(next);
|
|
500
|
+
syncSpreads([affectedSpread]);
|
|
501
|
+
},
|
|
502
|
+
[syncSpreads]
|
|
503
|
+
);
|
|
504
|
+
const findById = useCallback(
|
|
505
|
+
(id) => {
|
|
506
|
+
for (const [s, list] of imagesRef.current) {
|
|
507
|
+
const i = list.findIndex((it) => it.id === id);
|
|
508
|
+
if (i >= 0) return { spread: s, index: i, item: list[i] };
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
},
|
|
512
|
+
[]
|
|
513
|
+
);
|
|
514
|
+
const addImage = useCallback(
|
|
515
|
+
(overrides) => {
|
|
516
|
+
const si = spreadIdxRef.current;
|
|
517
|
+
const list = imagesRef.current.get(si) ?? [];
|
|
518
|
+
const last = list[list.length - 1];
|
|
519
|
+
const baseX = last ? last.rect.x - 100 : defX;
|
|
520
|
+
const baseY = last ? last.rect.y + 40 : defY;
|
|
521
|
+
nextId += 1;
|
|
522
|
+
const item = {
|
|
523
|
+
id: `mejiro-img-${nextId}`,
|
|
524
|
+
rect: {
|
|
525
|
+
x: overrides?.x ?? baseX,
|
|
526
|
+
y: overrides?.y ?? baseY,
|
|
527
|
+
w: overrides?.w ?? defW,
|
|
528
|
+
h: overrides?.h ?? defH
|
|
529
|
+
}
|
|
282
530
|
};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
531
|
+
const next = new Map(imagesRef.current);
|
|
532
|
+
next.set(si, [...list, item]);
|
|
533
|
+
commit(next, si);
|
|
534
|
+
return item;
|
|
535
|
+
},
|
|
536
|
+
[defX, defY, defW, defH, commit]
|
|
537
|
+
);
|
|
538
|
+
const removeImage = useCallback(
|
|
539
|
+
(id) => {
|
|
540
|
+
const found = findById(id);
|
|
541
|
+
if (!found) return;
|
|
542
|
+
const next = new Map(imagesRef.current);
|
|
543
|
+
const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
|
|
544
|
+
if (list.length === 0) next.delete(found.spread);
|
|
545
|
+
else next.set(found.spread, list);
|
|
546
|
+
commit(next, found.spread);
|
|
547
|
+
},
|
|
548
|
+
[findById, commit]
|
|
549
|
+
);
|
|
550
|
+
const updateImage = useCallback(
|
|
551
|
+
(id, rect) => {
|
|
552
|
+
const found = findById(id);
|
|
553
|
+
if (!found) return;
|
|
554
|
+
const next = new Map(imagesRef.current);
|
|
555
|
+
const list = (next.get(found.spread) ?? []).map(
|
|
556
|
+
(it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
|
|
286
557
|
);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
558
|
+
next.set(found.spread, list);
|
|
559
|
+
commit(next, found.spread);
|
|
560
|
+
},
|
|
561
|
+
[findById, commit]
|
|
562
|
+
);
|
|
563
|
+
const clearImages = useCallback(
|
|
564
|
+
(target) => {
|
|
565
|
+
if (target == null) {
|
|
566
|
+
const old = imagesRef.current;
|
|
567
|
+
imagesRef.current = /* @__PURE__ */ new Map();
|
|
568
|
+
setImages(/* @__PURE__ */ new Map());
|
|
569
|
+
layoutRef.current?.clearImages();
|
|
570
|
+
if (layoutRef.current) {
|
|
571
|
+
for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
const next = new Map(imagesRef.current);
|
|
575
|
+
next.delete(target);
|
|
576
|
+
commit(next, target);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
[commit]
|
|
580
|
+
);
|
|
581
|
+
const onOverlayPointerDown = useCallback(
|
|
582
|
+
(id, e) => {
|
|
583
|
+
if (!isPrimaryPointerPress(e)) return;
|
|
584
|
+
e.preventDefault();
|
|
585
|
+
const found = findById(id);
|
|
586
|
+
if (!found) return;
|
|
587
|
+
const target = e.currentTarget;
|
|
588
|
+
createOverlayDragSession({
|
|
589
|
+
mode: "move",
|
|
590
|
+
rect: found.item.rect,
|
|
591
|
+
startX: e.clientX,
|
|
592
|
+
startY: e.clientY,
|
|
593
|
+
pointerId: e.nativeEvent.pointerId,
|
|
594
|
+
captureElement: target,
|
|
595
|
+
activeElement: target,
|
|
596
|
+
dragClass: "is-dragging",
|
|
597
|
+
registry: activeDragCleanupsRef.current,
|
|
598
|
+
onChange: (rect) => updateImage(id, rect)
|
|
599
|
+
});
|
|
600
|
+
},
|
|
601
|
+
[findById, updateImage]
|
|
602
|
+
);
|
|
603
|
+
const onResizePointerDown = useCallback(
|
|
604
|
+
(id, e) => {
|
|
605
|
+
if (!isPrimaryPointerPress(e)) return;
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
e.stopPropagation();
|
|
608
|
+
const found = findById(id);
|
|
609
|
+
if (!found) return;
|
|
610
|
+
const target = e.currentTarget;
|
|
611
|
+
createOverlayDragSession({
|
|
612
|
+
mode: "resize",
|
|
613
|
+
rect: found.item.rect,
|
|
614
|
+
startX: e.clientX,
|
|
615
|
+
startY: e.clientY,
|
|
616
|
+
pointerId: e.nativeEvent.pointerId,
|
|
617
|
+
captureElement: target,
|
|
618
|
+
activeElement: target.parentElement,
|
|
619
|
+
dragClass: "is-dragging",
|
|
620
|
+
registry: activeDragCleanupsRef.current,
|
|
621
|
+
onChange: (rect) => updateImage(id, rect)
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
[findById, updateImage]
|
|
625
|
+
);
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
if (!layout) return;
|
|
628
|
+
for (const si of imagesRef.current.keys()) onUpdateRef.current?.(layout.getSpread(si));
|
|
629
|
+
}, [layout, margin]);
|
|
630
|
+
const imagesBySpread = useMemo2(() => new Map(images), [images, margin]);
|
|
631
|
+
const currentImages = useMemo2(
|
|
632
|
+
() => [...imagesBySpread.get(spreadIdx) ?? []],
|
|
633
|
+
[imagesBySpread, spreadIdx]
|
|
634
|
+
);
|
|
635
|
+
const hasImages = useMemo2(() => [...images.values()].some((list) => list.length > 0), [images]);
|
|
636
|
+
return {
|
|
637
|
+
imagesBySpread,
|
|
638
|
+
currentImages,
|
|
639
|
+
hasImages,
|
|
640
|
+
addImage,
|
|
641
|
+
removeImage,
|
|
642
|
+
updateImage,
|
|
643
|
+
clearImages,
|
|
644
|
+
onOverlayPointerDown,
|
|
645
|
+
onResizePointerDown
|
|
646
|
+
};
|
|
290
647
|
}
|
|
291
648
|
|
|
292
649
|
// src/MejiroScrollView.tsx
|
|
293
|
-
import { jsx as
|
|
650
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
651
|
+
var NO_IMAGES = [];
|
|
294
652
|
function MejiroScrollView({
|
|
295
653
|
layout,
|
|
296
654
|
pageWidth,
|
|
@@ -302,23 +660,49 @@ function MejiroScrollView({
|
|
|
302
660
|
slotMode,
|
|
303
661
|
onVisiblePageChange,
|
|
304
662
|
scrollToPage,
|
|
305
|
-
pageGap = 24
|
|
663
|
+
pageGap = 24,
|
|
664
|
+
selectionRects,
|
|
665
|
+
spreadIdx,
|
|
666
|
+
images = NO_IMAGES,
|
|
667
|
+
onImagePointerDown,
|
|
668
|
+
onImageResizePointerDown,
|
|
669
|
+
onImageClose
|
|
306
670
|
}) {
|
|
307
|
-
const containerRef =
|
|
308
|
-
const pageRefs =
|
|
309
|
-
const onVisiblePageChangeRef =
|
|
671
|
+
const containerRef = useRef3(null);
|
|
672
|
+
const pageRefs = useRef3([]);
|
|
673
|
+
const onVisiblePageChangeRef = useRef3(onVisiblePageChange);
|
|
310
674
|
onVisiblePageChangeRef.current = onVisiblePageChange;
|
|
311
|
-
const programmaticScrollRef =
|
|
312
|
-
const
|
|
675
|
+
const programmaticScrollRef = useRef3(false);
|
|
676
|
+
const userPageRef = useRef3(null);
|
|
677
|
+
const rightPage = spreadIdx != null ? spreadIdx * 2 : -1;
|
|
678
|
+
const leftPage = spreadIdx != null ? spreadIdx * 2 + 1 : -1;
|
|
679
|
+
const rightImages = useMemo3(
|
|
680
|
+
() => images.filter((item) => overlayPageSide(item.rect) === "right"),
|
|
681
|
+
[images]
|
|
682
|
+
);
|
|
683
|
+
const leftImages = useMemo3(
|
|
684
|
+
() => images.filter((item) => overlayPageSide(item.rect) === "left"),
|
|
685
|
+
[images]
|
|
686
|
+
);
|
|
687
|
+
const pages = useMemo3(() => {
|
|
313
688
|
const total = layout.totalPages;
|
|
314
689
|
return Array.from({ length: total }, (_, i) => layout.getPage(i));
|
|
315
|
-
}, [layout]);
|
|
690
|
+
}, [layout, images]);
|
|
316
691
|
const pageCount = pages.length;
|
|
692
|
+
const rectsByPage = useMemo3(() => {
|
|
693
|
+
const byPage = [];
|
|
694
|
+
for (const rect of selectionRects ?? []) {
|
|
695
|
+
const list = byPage[rect.pageIdx] ?? [];
|
|
696
|
+
list.push(rect);
|
|
697
|
+
byPage[rect.pageIdx] = list;
|
|
698
|
+
}
|
|
699
|
+
return byPage;
|
|
700
|
+
}, [selectionRects]);
|
|
317
701
|
const contentStyle = { height: contentHeight };
|
|
318
702
|
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
|
|
319
703
|
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
320
704
|
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
321
|
-
|
|
705
|
+
useEffect2(() => {
|
|
322
706
|
const container = containerRef.current;
|
|
323
707
|
if (!container) return;
|
|
324
708
|
if (pageCount === 0) return;
|
|
@@ -341,10 +725,9 @@ function MejiroScrollView({
|
|
|
341
725
|
}
|
|
342
726
|
}
|
|
343
727
|
if (mostVisibleIdx >= 0) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
728
|
+
const source = programmaticScrollRef.current ? "programmatic" : "user";
|
|
729
|
+
if (source === "user") userPageRef.current = mostVisibleIdx;
|
|
730
|
+
onVisiblePageChangeRef.current?.(mostVisibleIdx, source);
|
|
348
731
|
}
|
|
349
732
|
},
|
|
350
733
|
{ root: container, threshold: [0.25, 0.5, 0.75] }
|
|
@@ -357,8 +740,10 @@ function MejiroScrollView({
|
|
|
357
740
|
useLayoutEffect(() => {
|
|
358
741
|
if (scrollToPage == null) return;
|
|
359
742
|
if (pageCount === 0) return;
|
|
743
|
+
if (scrollToPage === userPageRef.current) return;
|
|
360
744
|
const el = pageRefs.current[scrollToPage];
|
|
361
745
|
if (!(el && containerRef.current)) return;
|
|
746
|
+
userPageRef.current = null;
|
|
362
747
|
programmaticScrollRef.current = true;
|
|
363
748
|
containerRef.current.scrollTo({
|
|
364
749
|
top: el.offsetTop,
|
|
@@ -369,12 +754,12 @@ function MejiroScrollView({
|
|
|
369
754
|
}, 0);
|
|
370
755
|
return () => clearTimeout(timer);
|
|
371
756
|
}, [scrollToPage, pageCount]);
|
|
372
|
-
return /* @__PURE__ */
|
|
757
|
+
return /* @__PURE__ */ jsx9("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx9(
|
|
373
758
|
"div",
|
|
374
759
|
{
|
|
375
760
|
className: "mejiro-reader-scroll-track",
|
|
376
761
|
style: { display: "flex", flexDirection: "column", gap: pageGap },
|
|
377
|
-
children: pages.map((result, i) => /* @__PURE__ */
|
|
762
|
+
children: pages.map((result, i) => /* @__PURE__ */ jsxs5(
|
|
378
763
|
"div",
|
|
379
764
|
{
|
|
380
765
|
ref: (el) => {
|
|
@@ -382,24 +767,58 @@ function MejiroScrollView({
|
|
|
382
767
|
},
|
|
383
768
|
"data-page-idx": i,
|
|
384
769
|
className: "mejiro-reader-page",
|
|
385
|
-
style: {
|
|
770
|
+
style: {
|
|
771
|
+
width: pageWidth,
|
|
772
|
+
height: pageHeight,
|
|
773
|
+
flexShrink: 0,
|
|
774
|
+
...i === rightPage && rightImages.length > 0 || i === leftPage && leftImages.length > 0 ? { overflow: "visible" } : null
|
|
775
|
+
},
|
|
386
776
|
children: [
|
|
387
|
-
/* @__PURE__ */
|
|
388
|
-
/* @__PURE__ */
|
|
389
|
-
/* @__PURE__ */
|
|
390
|
-
/* @__PURE__ */
|
|
777
|
+
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-rule" }),
|
|
778
|
+
/* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-header", children: [
|
|
779
|
+
/* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-title" }),
|
|
780
|
+
/* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
|
|
391
781
|
] }),
|
|
392
|
-
/* @__PURE__ */
|
|
393
|
-
|
|
782
|
+
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
|
|
783
|
+
/* @__PURE__ */ jsx9(
|
|
784
|
+
MejiroPageView,
|
|
785
|
+
{
|
|
786
|
+
result,
|
|
787
|
+
slotMode,
|
|
788
|
+
fontFamily,
|
|
789
|
+
lineSpacing,
|
|
790
|
+
className: "mejiro-reader-page-content",
|
|
791
|
+
style: contentStyle
|
|
792
|
+
}
|
|
793
|
+
),
|
|
794
|
+
rectsByPage[i] && /* @__PURE__ */ jsx9(
|
|
795
|
+
MejiroSelectionLayer,
|
|
796
|
+
{
|
|
797
|
+
rects: rectsByPage[i],
|
|
798
|
+
side: i % 2 === 0 ? "right" : "left"
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
] }) }),
|
|
802
|
+
i === rightPage && rightImages.map((item) => /* @__PURE__ */ jsx9(
|
|
803
|
+
MejiroImageOverlay,
|
|
394
804
|
{
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
805
|
+
rect: item.rect,
|
|
806
|
+
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
807
|
+
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
808
|
+
onClose: () => onImageClose?.(item.id)
|
|
809
|
+
},
|
|
810
|
+
item.id
|
|
811
|
+
)),
|
|
812
|
+
i === leftPage && leftImages.map((item) => /* @__PURE__ */ jsx9(
|
|
813
|
+
MejiroImageOverlay,
|
|
814
|
+
{
|
|
815
|
+
rect: { ...item.rect, x: item.rect.x + pageWidth },
|
|
816
|
+
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
817
|
+
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
818
|
+
onClose: () => onImageClose?.(item.id)
|
|
819
|
+
},
|
|
820
|
+
item.id
|
|
821
|
+
))
|
|
403
822
|
]
|
|
404
823
|
},
|
|
405
824
|
i
|
|
@@ -410,7 +829,8 @@ function MejiroScrollView({
|
|
|
410
829
|
|
|
411
830
|
// src/MejiroSettingsPanel.tsx
|
|
412
831
|
import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
|
|
413
|
-
import {
|
|
832
|
+
import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
|
|
833
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
414
834
|
var DEFAULT_FONTS = [
|
|
415
835
|
{ value: "serif", label: "System Serif" },
|
|
416
836
|
{ value: "sans-serif", label: "System Sans" }
|
|
@@ -427,6 +847,48 @@ function parseClampedInput(value, min, max, fallback) {
|
|
|
427
847
|
if (value.trim() === "") return fallback;
|
|
428
848
|
return clampNumber(Number(value), min, max, fallback);
|
|
429
849
|
}
|
|
850
|
+
function CommittedNumberInput({
|
|
851
|
+
id,
|
|
852
|
+
className,
|
|
853
|
+
value,
|
|
854
|
+
min,
|
|
855
|
+
max,
|
|
856
|
+
step,
|
|
857
|
+
onCommit
|
|
858
|
+
}) {
|
|
859
|
+
const [draft, setDraft] = useState3(String(value));
|
|
860
|
+
const [input, setInput] = useState3(null);
|
|
861
|
+
const latest = useRef4({ value, min, max, onCommit });
|
|
862
|
+
latest.current = { value, min, max, onCommit };
|
|
863
|
+
useEffect3(() => {
|
|
864
|
+
setDraft(String(value));
|
|
865
|
+
}, [value]);
|
|
866
|
+
useEffect3(() => {
|
|
867
|
+
if (!input) return;
|
|
868
|
+
const commit = () => {
|
|
869
|
+
const { value: current, min: lo, max: hi, onCommit: emit } = latest.current;
|
|
870
|
+
const next = parseClampedInput(input.value, lo, hi, current);
|
|
871
|
+
setDraft(String(next));
|
|
872
|
+
emit(next);
|
|
873
|
+
};
|
|
874
|
+
input.addEventListener("change", commit);
|
|
875
|
+
return () => input.removeEventListener("change", commit);
|
|
876
|
+
}, [input]);
|
|
877
|
+
return /* @__PURE__ */ jsx10(
|
|
878
|
+
"input",
|
|
879
|
+
{
|
|
880
|
+
ref: setInput,
|
|
881
|
+
id,
|
|
882
|
+
className,
|
|
883
|
+
type: "number",
|
|
884
|
+
value: draft,
|
|
885
|
+
min,
|
|
886
|
+
max,
|
|
887
|
+
step,
|
|
888
|
+
onChange: (e) => setDraft(e.target.value)
|
|
889
|
+
}
|
|
890
|
+
);
|
|
891
|
+
}
|
|
430
892
|
function MejiroSettingsPanel({
|
|
431
893
|
open,
|
|
432
894
|
settings,
|
|
@@ -439,13 +901,13 @@ function MejiroSettingsPanel({
|
|
|
439
901
|
const patch = (next) => onChange({ ...settings, ...next });
|
|
440
902
|
const currentFont = normalizeFontFamily3(settings.fontFamily);
|
|
441
903
|
const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
|
|
442
|
-
return /* @__PURE__ */
|
|
443
|
-
/* @__PURE__ */
|
|
444
|
-
/* @__PURE__ */
|
|
445
|
-
/* @__PURE__ */
|
|
446
|
-
/* @__PURE__ */
|
|
447
|
-
/* @__PURE__ */
|
|
448
|
-
/* @__PURE__ */
|
|
904
|
+
return /* @__PURE__ */ jsx10("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-content", children: [
|
|
905
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
|
|
906
|
+
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
|
|
907
|
+
/* @__PURE__ */ jsx10("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx10("select", { value: currentFont, onChange: (e) => patch({ fontFamily: e.target.value }), children: fontOptions.map((f) => /* @__PURE__ */ jsx10("option", { value: f.value, children: f.label }, f.value)) }) }),
|
|
908
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
909
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
|
|
910
|
+
/* @__PURE__ */ jsx10(
|
|
449
911
|
"button",
|
|
450
912
|
{
|
|
451
913
|
type: "button",
|
|
@@ -462,25 +924,17 @@ function MejiroSettingsPanel({
|
|
|
462
924
|
children: "A\u2212"
|
|
463
925
|
}
|
|
464
926
|
),
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
-
|
|
927
|
+
/* @__PURE__ */ jsx10(
|
|
928
|
+
CommittedNumberInput,
|
|
467
929
|
{
|
|
468
930
|
id: "mejiro-reader-font-size",
|
|
469
|
-
type: "number",
|
|
470
931
|
value: settings.fontSize,
|
|
471
932
|
min: minFontSize,
|
|
472
933
|
max: maxFontSize,
|
|
473
|
-
|
|
474
|
-
fontSize: parseClampedInput(
|
|
475
|
-
e.target.value,
|
|
476
|
-
minFontSize,
|
|
477
|
-
maxFontSize,
|
|
478
|
-
settings.fontSize
|
|
479
|
-
)
|
|
480
|
-
})
|
|
934
|
+
onCommit: (fontSize) => patch({ fontSize })
|
|
481
935
|
}
|
|
482
936
|
),
|
|
483
|
-
/* @__PURE__ */
|
|
937
|
+
/* @__PURE__ */ jsx10(
|
|
484
938
|
"button",
|
|
485
939
|
{
|
|
486
940
|
type: "button",
|
|
@@ -499,58 +953,50 @@ function MejiroSettingsPanel({
|
|
|
499
953
|
)
|
|
500
954
|
] })
|
|
501
955
|
] }),
|
|
502
|
-
/* @__PURE__ */
|
|
503
|
-
/* @__PURE__ */
|
|
504
|
-
/* @__PURE__ */
|
|
505
|
-
/* @__PURE__ */
|
|
506
|
-
/* @__PURE__ */
|
|
956
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
|
|
957
|
+
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
|
|
958
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
959
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
|
|
960
|
+
/* @__PURE__ */ jsxs6(
|
|
507
961
|
"select",
|
|
508
962
|
{
|
|
509
963
|
id: "mejiro-reader-kinsoku",
|
|
510
964
|
value: settings.mode ?? "strict",
|
|
511
965
|
onChange: (e) => patch({ mode: e.target.value }),
|
|
512
966
|
children: [
|
|
513
|
-
/* @__PURE__ */
|
|
514
|
-
/* @__PURE__ */
|
|
967
|
+
/* @__PURE__ */ jsx10("option", { value: "strict", children: messages.settingsStrict }),
|
|
968
|
+
/* @__PURE__ */ jsx10("option", { value: "loose", children: messages.settingsLoose })
|
|
515
969
|
]
|
|
516
970
|
}
|
|
517
971
|
)
|
|
518
972
|
] }),
|
|
519
|
-
/* @__PURE__ */
|
|
520
|
-
/* @__PURE__ */
|
|
521
|
-
/* @__PURE__ */
|
|
973
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
974
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
|
|
975
|
+
/* @__PURE__ */ jsxs6(
|
|
522
976
|
"select",
|
|
523
977
|
{
|
|
524
978
|
id: "mejiro-reader-hanging",
|
|
525
979
|
value: String(settings.enableHanging ?? true),
|
|
526
980
|
onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
|
|
527
981
|
children: [
|
|
528
|
-
/* @__PURE__ */
|
|
529
|
-
/* @__PURE__ */
|
|
982
|
+
/* @__PURE__ */ jsx10("option", { value: "true", children: messages.toggleOn }),
|
|
983
|
+
/* @__PURE__ */ jsx10("option", { value: "false", children: messages.toggleOff })
|
|
530
984
|
]
|
|
531
985
|
}
|
|
532
986
|
)
|
|
533
987
|
] }),
|
|
534
|
-
/* @__PURE__ */
|
|
535
|
-
/* @__PURE__ */
|
|
536
|
-
/* @__PURE__ */
|
|
537
|
-
|
|
988
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
989
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
|
|
990
|
+
/* @__PURE__ */ jsx10(
|
|
991
|
+
CommittedNumberInput,
|
|
538
992
|
{
|
|
539
993
|
id: "mejiro-reader-line-spacing",
|
|
540
994
|
className: "mejiro-reader-control--wide",
|
|
541
|
-
type: "number",
|
|
542
995
|
value: settings.lineSpacing ?? 1.8,
|
|
543
996
|
min: 1,
|
|
544
997
|
max: 3,
|
|
545
998
|
step: 0.1,
|
|
546
|
-
|
|
547
|
-
lineSpacing: parseClampedInput(
|
|
548
|
-
e.target.value,
|
|
549
|
-
1,
|
|
550
|
-
3,
|
|
551
|
-
settings.lineSpacing ?? 1.8
|
|
552
|
-
)
|
|
553
|
-
})
|
|
999
|
+
onCommit: (lineSpacing) => patch({ lineSpacing })
|
|
554
1000
|
}
|
|
555
1001
|
)
|
|
556
1002
|
] })
|
|
@@ -559,111 +1005,23 @@ function MejiroSettingsPanel({
|
|
|
559
1005
|
}
|
|
560
1006
|
|
|
561
1007
|
// src/MejiroSpread.tsx
|
|
562
|
-
import { normalizeFontFamily as normalizeFontFamily4 } from "@libraz/mejiro/browser";
|
|
563
1008
|
import {
|
|
564
|
-
|
|
1009
|
+
isPrimaryPointerPress as isPrimaryPointerPress2,
|
|
1010
|
+
normalizeFontFamily as normalizeFontFamily4
|
|
1011
|
+
} from "@libraz/mejiro/browser";
|
|
1012
|
+
import {
|
|
1013
|
+
useRef as useRef5
|
|
565
1014
|
} from "react";
|
|
566
|
-
|
|
567
|
-
// src/MejiroImageOverlay.tsx
|
|
568
|
-
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
569
|
-
function MejiroImageOverlay({
|
|
570
|
-
rect,
|
|
571
|
-
label,
|
|
572
|
-
onOverlayPointerDown,
|
|
573
|
-
onResizePointerDown,
|
|
574
|
-
onClose
|
|
575
|
-
}) {
|
|
576
|
-
const messages = useI18n();
|
|
577
|
-
const resolvedLabel = label ?? messages.imageButton;
|
|
578
|
-
return /* @__PURE__ */ jsxs6(
|
|
579
|
-
"div",
|
|
580
|
-
{
|
|
581
|
-
className: "mejiro-reader-image-overlay",
|
|
582
|
-
style: {
|
|
583
|
-
left: rect.x,
|
|
584
|
-
top: rect.y,
|
|
585
|
-
width: rect.w,
|
|
586
|
-
height: rect.h
|
|
587
|
-
},
|
|
588
|
-
onPointerDown: onOverlayPointerDown,
|
|
589
|
-
children: [
|
|
590
|
-
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-image-overlay-label", children: [
|
|
591
|
-
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-icon" }),
|
|
592
|
-
/* @__PURE__ */ jsx9("span", { children: resolvedLabel })
|
|
593
|
-
] }),
|
|
594
|
-
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
|
|
595
|
-
/* @__PURE__ */ jsx9(
|
|
596
|
-
"button",
|
|
597
|
-
{
|
|
598
|
-
type: "button",
|
|
599
|
-
"aria-label": messages.imageRemoveButton,
|
|
600
|
-
title: messages.imageRemoveButton,
|
|
601
|
-
className: "mejiro-reader-image-overlay-close",
|
|
602
|
-
onPointerDown: (e) => {
|
|
603
|
-
e.stopPropagation();
|
|
604
|
-
e.preventDefault();
|
|
605
|
-
onClose?.();
|
|
606
|
-
},
|
|
607
|
-
onKeyDown: (e) => {
|
|
608
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
609
|
-
e.stopPropagation();
|
|
610
|
-
e.preventDefault();
|
|
611
|
-
onClose?.();
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
)
|
|
615
|
-
]
|
|
616
|
-
}
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// src/MejiroSelectionLayer.tsx
|
|
621
|
-
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
622
|
-
function MejiroSelectionLayer({
|
|
623
|
-
rects,
|
|
624
|
-
side,
|
|
625
|
-
className,
|
|
626
|
-
style,
|
|
627
|
-
rectClassName,
|
|
628
|
-
rectStyle
|
|
629
|
-
}) {
|
|
630
|
-
const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
|
|
631
|
-
const rectClass = rectClassName ?? "mejiro-selection-rect";
|
|
632
|
-
return /* @__PURE__ */ jsx10(
|
|
633
|
-
"div",
|
|
634
|
-
{
|
|
635
|
-
className: wrapperClass,
|
|
636
|
-
style: {
|
|
637
|
-
position: "absolute",
|
|
638
|
-
inset: 0,
|
|
639
|
-
pointerEvents: "none",
|
|
640
|
-
...style
|
|
641
|
-
},
|
|
642
|
-
children: rects.map((r, i) => {
|
|
643
|
-
if (r.side !== side) return null;
|
|
644
|
-
const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
|
|
645
|
-
return /* @__PURE__ */ jsx10(
|
|
646
|
-
"div",
|
|
647
|
-
{
|
|
648
|
-
className: rectClass,
|
|
649
|
-
style: {
|
|
650
|
-
position: "absolute",
|
|
651
|
-
width: r.width,
|
|
652
|
-
height: r.height,
|
|
653
|
-
...posStyle,
|
|
654
|
-
...r.color != null ? { backgroundColor: r.color } : null,
|
|
655
|
-
...rectStyle
|
|
656
|
-
}
|
|
657
|
-
},
|
|
658
|
-
i
|
|
659
|
-
);
|
|
660
|
-
})
|
|
661
|
-
}
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// src/MejiroSpread.tsx
|
|
666
1015
|
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1016
|
+
var OVERLAY_SELECTOR = ".mejiro-reader-image-overlay";
|
|
1017
|
+
var CONTENT_SELECTOR = ".mejiro-reader-page-content";
|
|
1018
|
+
function contentAtPoint(root, x, y) {
|
|
1019
|
+
for (const content of root.querySelectorAll(CONTENT_SELECTOR)) {
|
|
1020
|
+
const r = content.getBoundingClientRect();
|
|
1021
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return content;
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
667
1025
|
function defaultHeader(data) {
|
|
668
1026
|
return /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-page-header", children: [
|
|
669
1027
|
/* @__PURE__ */ jsx11("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
|
|
@@ -695,20 +1053,21 @@ function MejiroSpread({
|
|
|
695
1053
|
selectionRects,
|
|
696
1054
|
onSelectionChange,
|
|
697
1055
|
singlePage = false,
|
|
1056
|
+
singleSide = "right",
|
|
698
1057
|
onSwipe,
|
|
699
1058
|
onSurfaceTap
|
|
700
1059
|
}) {
|
|
701
1060
|
const messages = useI18n();
|
|
702
|
-
const
|
|
703
|
-
const selectionStartRef = useRef3(null);
|
|
1061
|
+
const selectionRef = useRef5(null);
|
|
704
1062
|
const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
|
|
705
|
-
const gestureRef =
|
|
1063
|
+
const gestureRef = useRef5(null);
|
|
706
1064
|
const gestureEnabled = onSwipe != null || onSurfaceTap != null;
|
|
707
1065
|
const SWIPE_THRESHOLD = 40;
|
|
708
1066
|
const TAP_MOVE_THRESHOLD = 8;
|
|
709
1067
|
function handleGesturePointerDown(e) {
|
|
710
1068
|
if (!gestureEnabled) return;
|
|
711
|
-
if (e
|
|
1069
|
+
if (!isPrimaryPointerPress2(e)) return;
|
|
1070
|
+
if (e.target?.closest(OVERLAY_SELECTOR)) return;
|
|
712
1071
|
gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
|
|
713
1072
|
}
|
|
714
1073
|
function handleGesturePointerUp(e) {
|
|
@@ -726,15 +1085,13 @@ function MejiroSpread({
|
|
|
726
1085
|
}
|
|
727
1086
|
if (ax < TAP_MOVE_THRESHOLD && ay < TAP_MOVE_THRESHOLD) {
|
|
728
1087
|
const target = e.target;
|
|
729
|
-
if (target?.closest(
|
|
1088
|
+
if (target?.closest(`button, a, ${OVERLAY_SELECTOR}`)) return;
|
|
730
1089
|
onSurfaceTap?.();
|
|
731
1090
|
}
|
|
732
1091
|
}
|
|
733
1092
|
function resolvePointer(e) {
|
|
734
1093
|
if (!anchorAtCoord) return null;
|
|
735
|
-
const
|
|
736
|
-
if (!target) return null;
|
|
737
|
-
const content = target.closest(".mejiro-reader-page-content");
|
|
1094
|
+
const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
|
|
738
1095
|
if (!content) return null;
|
|
739
1096
|
const pageEl = content.closest(".mejiro-reader-page");
|
|
740
1097
|
const isRight = pageEl?.classList.contains("mejiro-reader-page--right") ?? true;
|
|
@@ -745,26 +1102,28 @@ function MejiroSpread({
|
|
|
745
1102
|
return anchorAtCoord(spreadX, offsetY);
|
|
746
1103
|
}
|
|
747
1104
|
function handlePointerDown(e) {
|
|
748
|
-
if (!selectionEnabled) return;
|
|
1105
|
+
if (!selectionEnabled || selectionRef.current) return;
|
|
1106
|
+
if (!e.target?.closest(CONTENT_SELECTOR)) return;
|
|
749
1107
|
const anchor = resolvePointer(e);
|
|
750
1108
|
if (!anchor) return;
|
|
751
1109
|
e.preventDefault();
|
|
752
|
-
|
|
1110
|
+
selectionRef.current = { start: anchor, pointerId: e.pointerId };
|
|
753
1111
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
754
1112
|
onSelectionChange?.(null);
|
|
755
1113
|
}
|
|
756
1114
|
function handlePointerMove(e) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (!start) return;
|
|
1115
|
+
const session = selectionRef.current;
|
|
1116
|
+
if (!session || e.pointerId !== session.pointerId) return;
|
|
760
1117
|
const end = resolvePointer(e);
|
|
761
1118
|
if (!end) return;
|
|
1119
|
+
const { start } = session;
|
|
762
1120
|
if (end.paragraph === start.paragraph && end.charIndex === start.charIndex) return;
|
|
763
1121
|
onSelectionChange?.({ start, end });
|
|
764
1122
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
|
|
1123
|
+
function endSelection(e) {
|
|
1124
|
+
const session = selectionRef.current;
|
|
1125
|
+
if (!session || e.pointerId !== session.pointerId) return;
|
|
1126
|
+
selectionRef.current = null;
|
|
768
1127
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
769
1128
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
770
1129
|
}
|
|
@@ -774,8 +1133,12 @@ function MejiroSpread({
|
|
|
774
1133
|
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
775
1134
|
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
776
1135
|
const visibleRects = selectionRects && spreadIdx != null ? selectionRects.filter((r) => r.spreadIdx === spreadIdx) : selectionRects;
|
|
1136
|
+
const imageSide = singlePage ? singleSide : "right";
|
|
1137
|
+
const hostedImages = singlePage ? images.filter((item) => overlayPageSide(item.rect) === singleSide) : images;
|
|
1138
|
+
const hasImages = hostedImages.length > 0;
|
|
777
1139
|
const renderPage = (side) => {
|
|
778
1140
|
const isRight = side === "right";
|
|
1141
|
+
const hostsImages = side === imageSide;
|
|
779
1142
|
const result = isRight ? spread.right : spread.left;
|
|
780
1143
|
const header = isRight ? rightHeader : leftHeader;
|
|
781
1144
|
const pageKey = `${side}-${header.pageNumber ?? "blank"}`;
|
|
@@ -783,7 +1146,7 @@ function MejiroSpread({
|
|
|
783
1146
|
width: pageWidth,
|
|
784
1147
|
height: pageHeight
|
|
785
1148
|
};
|
|
786
|
-
if (
|
|
1149
|
+
if (hostsImages && hasImages) pageStyle.overflow = "visible";
|
|
787
1150
|
return /* @__PURE__ */ jsxs7(
|
|
788
1151
|
"div",
|
|
789
1152
|
{
|
|
@@ -807,10 +1170,10 @@ function MejiroSpread({
|
|
|
807
1170
|
),
|
|
808
1171
|
visibleRects && visibleRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: visibleRects, side })
|
|
809
1172
|
] }) }),
|
|
810
|
-
|
|
1173
|
+
hostsImages && hostedImages.map((item) => /* @__PURE__ */ jsx11(
|
|
811
1174
|
MejiroImageOverlay,
|
|
812
1175
|
{
|
|
813
|
-
rect: item.rect,
|
|
1176
|
+
rect: isRight ? item.rect : { ...item.rect, x: item.rect.x + pageWidth },
|
|
814
1177
|
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
815
1178
|
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
816
1179
|
onClose: () => onImageClose?.(item.id)
|
|
@@ -826,12 +1189,13 @@ function MejiroSpread({
|
|
|
826
1189
|
if (gestureEnabled) handleGesturePointerDown(e);
|
|
827
1190
|
if (selectionEnabled) handlePointerDown(e);
|
|
828
1191
|
}
|
|
829
|
-
function combinedPointerMove(e) {
|
|
830
|
-
if (selectionEnabled) handlePointerMove(e);
|
|
831
|
-
}
|
|
832
1192
|
function combinedPointerUp(e) {
|
|
833
1193
|
if (gestureEnabled) handleGesturePointerUp(e);
|
|
834
|
-
|
|
1194
|
+
endSelection(e);
|
|
1195
|
+
}
|
|
1196
|
+
function combinedPointerCancel(e) {
|
|
1197
|
+
gestureRef.current = null;
|
|
1198
|
+
endSelection(e);
|
|
835
1199
|
}
|
|
836
1200
|
const useCombined = selectionEnabled || gestureEnabled;
|
|
837
1201
|
return /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs7(
|
|
@@ -839,11 +1203,12 @@ function MejiroSpread({
|
|
|
839
1203
|
{
|
|
840
1204
|
className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
|
|
841
1205
|
onPointerDown: useCombined ? combinedPointerDown : void 0,
|
|
842
|
-
onPointerMove: useCombined ?
|
|
1206
|
+
onPointerMove: useCombined ? handlePointerMove : void 0,
|
|
843
1207
|
onPointerUp: useCombined ? combinedPointerUp : void 0,
|
|
844
|
-
onPointerCancel: useCombined ?
|
|
1208
|
+
onPointerCancel: useCombined ? combinedPointerCancel : void 0,
|
|
1209
|
+
onLostPointerCapture: selectionEnabled ? endSelection : void 0,
|
|
845
1210
|
children: [
|
|
846
|
-
renderPage("right"),
|
|
1211
|
+
renderPage(singlePage ? singleSide : "right"),
|
|
847
1212
|
!singlePage && renderPage("left"),
|
|
848
1213
|
/* @__PURE__ */ jsx11(
|
|
849
1214
|
"button",
|
|
@@ -902,29 +1267,127 @@ function MejiroStats({
|
|
|
902
1267
|
return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
|
|
903
1268
|
}
|
|
904
1269
|
|
|
1270
|
+
// src/persistence.ts
|
|
1271
|
+
function resolveDefaultStorage() {
|
|
1272
|
+
try {
|
|
1273
|
+
return globalThis.localStorage ?? null;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function readStorage(storage, key, parse, fallback) {
|
|
1279
|
+
if (!storage) return fallback;
|
|
1280
|
+
try {
|
|
1281
|
+
return parse(storage.getItem(key));
|
|
1282
|
+
} catch {
|
|
1283
|
+
return fallback;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function removeStorage(storage, key) {
|
|
1287
|
+
if (!storage) return;
|
|
1288
|
+
try {
|
|
1289
|
+
storage.removeItem(key);
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function uniqueChapterId(existing) {
|
|
1294
|
+
const taken = new Set(existing.map((chapter) => chapter.id));
|
|
1295
|
+
const stamp = Date.now();
|
|
1296
|
+
let n = existing.length;
|
|
1297
|
+
while (taken.has(`chapter-${stamp}-${n}`)) n++;
|
|
1298
|
+
return `chapter-${stamp}-${n}`;
|
|
1299
|
+
}
|
|
1300
|
+
function mergeDefined(base, patch) {
|
|
1301
|
+
const next = { ...base };
|
|
1302
|
+
for (const key of Object.keys(patch)) {
|
|
1303
|
+
const value = patch[key];
|
|
1304
|
+
if (value !== void 0) next[key] = value;
|
|
1305
|
+
}
|
|
1306
|
+
return next;
|
|
1307
|
+
}
|
|
1308
|
+
function createPendingWrite() {
|
|
1309
|
+
let timer = null;
|
|
1310
|
+
let pending = null;
|
|
1311
|
+
function cancel() {
|
|
1312
|
+
if (timer) {
|
|
1313
|
+
clearTimeout(timer);
|
|
1314
|
+
timer = null;
|
|
1315
|
+
}
|
|
1316
|
+
pending = null;
|
|
1317
|
+
}
|
|
1318
|
+
function flush() {
|
|
1319
|
+
const write = pending;
|
|
1320
|
+
cancel();
|
|
1321
|
+
if (!write) return;
|
|
1322
|
+
try {
|
|
1323
|
+
write();
|
|
1324
|
+
} catch {
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
schedule(write, delayMs) {
|
|
1329
|
+
cancel();
|
|
1330
|
+
pending = write;
|
|
1331
|
+
timer = setTimeout(flush, delayMs);
|
|
1332
|
+
},
|
|
1333
|
+
flush,
|
|
1334
|
+
cancel
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function flushOnPageHide(flush) {
|
|
1338
|
+
if (typeof window === "undefined") return () => {
|
|
1339
|
+
};
|
|
1340
|
+
const handler = () => flush();
|
|
1341
|
+
window.addEventListener("beforeunload", handler);
|
|
1342
|
+
window.addEventListener("pagehide", handler);
|
|
1343
|
+
return () => {
|
|
1344
|
+
window.removeEventListener("beforeunload", handler);
|
|
1345
|
+
window.removeEventListener("pagehide", handler);
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function snapshotChapters(chapters) {
|
|
1349
|
+
return chapters.map(({ id, title, body }) => ({ id, title, body }));
|
|
1350
|
+
}
|
|
1351
|
+
function createDraftChangeTracker(chapters, key) {
|
|
1352
|
+
let lastChapters = snapshotChapters(chapters);
|
|
1353
|
+
let lastKey = key;
|
|
1354
|
+
return {
|
|
1355
|
+
changed(nextChapters, nextKey) {
|
|
1356
|
+
const same = nextKey === lastKey && nextChapters.length === lastChapters.length && nextChapters.every((chapter, i) => {
|
|
1357
|
+
const prev = lastChapters[i];
|
|
1358
|
+
return chapter.id === prev.id && chapter.title === prev.title && chapter.body === prev.body;
|
|
1359
|
+
});
|
|
1360
|
+
if (same) return false;
|
|
1361
|
+
lastChapters = snapshotChapters(nextChapters);
|
|
1362
|
+
lastKey = nextKey;
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
905
1368
|
// src/useChapterLayout.ts
|
|
906
1369
|
import {
|
|
907
|
-
useCallback,
|
|
908
|
-
useEffect as
|
|
1370
|
+
useCallback as useCallback2,
|
|
1371
|
+
useEffect as useEffect4,
|
|
909
1372
|
useLayoutEffect as useLayoutEffect2,
|
|
910
|
-
useRef as
|
|
911
|
-
useState as
|
|
1373
|
+
useRef as useRef6,
|
|
1374
|
+
useState as useState4
|
|
912
1375
|
} from "react";
|
|
913
1376
|
function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
914
1377
|
const enableResize = options.enableResize ?? true;
|
|
915
1378
|
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
916
|
-
const [layout, setLayout] =
|
|
917
|
-
const [pageWidth, setPageWidth] =
|
|
918
|
-
const [pageHeight, setPageHeight] =
|
|
919
|
-
const [contentHeight, setContentHeight] =
|
|
920
|
-
const [elapsedMs, setElapsedMs] =
|
|
921
|
-
const layoutRef =
|
|
922
|
-
const requestIdRef =
|
|
923
|
-
const pendingRestore =
|
|
1379
|
+
const [layout, setLayout] = useState4(null);
|
|
1380
|
+
const [pageWidth, setPageWidth] = useState4(0);
|
|
1381
|
+
const [pageHeight, setPageHeight] = useState4(0);
|
|
1382
|
+
const [contentHeight, setContentHeight] = useState4(0);
|
|
1383
|
+
const [elapsedMs, setElapsedMs] = useState4(0);
|
|
1384
|
+
const layoutRef = useRef6(null);
|
|
1385
|
+
const requestIdRef = useRef6(0);
|
|
1386
|
+
const pendingRestore = useRef6(null);
|
|
924
1387
|
layoutRef.current = layout;
|
|
925
|
-
const optionsRef =
|
|
1388
|
+
const optionsRef = useRef6(options);
|
|
926
1389
|
optionsRef.current = options;
|
|
927
|
-
const recompute =
|
|
1390
|
+
const recompute = useCallback2(
|
|
928
1391
|
async (opts = {}) => {
|
|
929
1392
|
const blank = opts.blank ?? true;
|
|
930
1393
|
const o = optionsRef.current;
|
|
@@ -963,13 +1426,20 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
963
1426
|
setContentHeight(0);
|
|
964
1427
|
setElapsedMs(0);
|
|
965
1428
|
}, [recompute]);
|
|
966
|
-
|
|
1429
|
+
useEffect4(() => {
|
|
967
1430
|
void recompute();
|
|
968
1431
|
return () => {
|
|
969
1432
|
requestIdRef.current++;
|
|
970
1433
|
};
|
|
971
1434
|
}, [recompute]);
|
|
972
|
-
|
|
1435
|
+
useLayoutEffect2(() => {
|
|
1436
|
+
const anchor = pendingRestore.current;
|
|
1437
|
+
const restore = optionsRef.current.restorePosition;
|
|
1438
|
+
if (!(layout && anchor && restore)) return;
|
|
1439
|
+
pendingRestore.current = null;
|
|
1440
|
+
restore(layout, anchor);
|
|
1441
|
+
}, [layout]);
|
|
1442
|
+
useEffect4(() => {
|
|
973
1443
|
if (!enableResize) return;
|
|
974
1444
|
const el = surface.current;
|
|
975
1445
|
if (!el) return;
|
|
@@ -1024,86 +1494,35 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1024
1494
|
|
|
1025
1495
|
// src/useEpub.ts
|
|
1026
1496
|
import { parseEpub } from "@libraz/mejiro/epub";
|
|
1027
|
-
import { useCallback as
|
|
1497
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef7, useState as useState5 } from "react";
|
|
1028
1498
|
function useEpub(options = {}) {
|
|
1029
|
-
const [epub, setEpub] =
|
|
1030
|
-
const [loading, setLoading] =
|
|
1031
|
-
const [error, setError] =
|
|
1032
|
-
const requestIdRef =
|
|
1033
|
-
const onLoadRef =
|
|
1034
|
-
onLoadRef.current = options.onLoad;
|
|
1035
|
-
const onErrorRef =
|
|
1036
|
-
onErrorRef.current = options.onError;
|
|
1037
|
-
const fetchOptionsRef =
|
|
1038
|
-
fetchOptionsRef.current = options.fetchOptions;
|
|
1039
|
-
const fetchEpubRef =
|
|
1040
|
-
fetchEpubRef.current = options.fetchEpub;
|
|
1041
|
-
const limitsRef =
|
|
1042
|
-
limitsRef.current = options.limits;
|
|
1043
|
-
const
|
|
1044
|
-
const requestId = ++requestIdRef.current;
|
|
1045
|
-
setLoading(true);
|
|
1046
|
-
setError(null);
|
|
1047
|
-
try {
|
|
1048
|
-
const book = await parseEpub(buffer, { limits: limitsRef.current });
|
|
1049
|
-
if (requestId !== requestIdRef.current) return null;
|
|
1050
|
-
setEpub(book);
|
|
1051
|
-
onLoadRef.current?.(book);
|
|
1052
|
-
return book;
|
|
1053
|
-
} catch (err) {
|
|
1054
|
-
if (requestId === requestIdRef.current) {
|
|
1055
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1056
|
-
setError(nextError);
|
|
1057
|
-
onErrorRef.current?.(nextError);
|
|
1058
|
-
}
|
|
1059
|
-
return null;
|
|
1060
|
-
} finally {
|
|
1061
|
-
if (requestId === requestIdRef.current) setLoading(false);
|
|
1062
|
-
}
|
|
1063
|
-
}, []);
|
|
1064
|
-
const loadFile = useCallback2(async (file) => {
|
|
1065
|
-
const requestId = ++requestIdRef.current;
|
|
1066
|
-
setLoading(true);
|
|
1067
|
-
setError(null);
|
|
1068
|
-
try {
|
|
1069
|
-
const book = await parseEpub(await file.arrayBuffer(), { limits: limitsRef.current });
|
|
1070
|
-
if (requestId !== requestIdRef.current) return null;
|
|
1071
|
-
setEpub(book);
|
|
1072
|
-
onLoadRef.current?.(book);
|
|
1073
|
-
return book;
|
|
1074
|
-
} catch (err) {
|
|
1075
|
-
if (requestId === requestIdRef.current) {
|
|
1076
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1077
|
-
setError(nextError);
|
|
1078
|
-
onErrorRef.current?.(nextError);
|
|
1079
|
-
}
|
|
1080
|
-
return null;
|
|
1081
|
-
} finally {
|
|
1082
|
-
if (requestId === requestIdRef.current) setLoading(false);
|
|
1083
|
-
}
|
|
1084
|
-
}, []);
|
|
1085
|
-
const loadUrl = useCallback2(async (url) => {
|
|
1499
|
+
const [epub, setEpub] = useState5(null);
|
|
1500
|
+
const [loading, setLoading] = useState5(false);
|
|
1501
|
+
const [error, setError] = useState5(null);
|
|
1502
|
+
const requestIdRef = useRef7(0);
|
|
1503
|
+
const onLoadRef = useRef7(options.onLoad);
|
|
1504
|
+
onLoadRef.current = options.onLoad;
|
|
1505
|
+
const onErrorRef = useRef7(options.onError);
|
|
1506
|
+
onErrorRef.current = options.onError;
|
|
1507
|
+
const fetchOptionsRef = useRef7(options.fetchOptions);
|
|
1508
|
+
fetchOptionsRef.current = options.fetchOptions;
|
|
1509
|
+
const fetchEpubRef = useRef7(options.fetchEpub);
|
|
1510
|
+
fetchEpubRef.current = options.fetchEpub;
|
|
1511
|
+
const limitsRef = useRef7(options.limits);
|
|
1512
|
+
limitsRef.current = options.limits;
|
|
1513
|
+
const load = useCallback3(async (read) => {
|
|
1086
1514
|
const requestId = ++requestIdRef.current;
|
|
1087
1515
|
setLoading(true);
|
|
1088
1516
|
setError(null);
|
|
1089
1517
|
try {
|
|
1090
|
-
|
|
1091
|
-
if (fetchEpubRef.current) {
|
|
1092
|
-
buffer = await fetchEpubRef.current(url);
|
|
1093
|
-
} else {
|
|
1094
|
-
const init = fetchOptionsRef.current;
|
|
1095
|
-
const res = init ? await fetch(url, init) : await fetch(url);
|
|
1096
|
-
if (!res.ok) return null;
|
|
1097
|
-
buffer = await res.arrayBuffer();
|
|
1098
|
-
}
|
|
1099
|
-
const book = await parseEpub(buffer, { limits: limitsRef.current });
|
|
1518
|
+
const book = await parseEpub(await read(), { limits: limitsRef.current });
|
|
1100
1519
|
if (requestId !== requestIdRef.current) return null;
|
|
1101
1520
|
setEpub(book);
|
|
1102
1521
|
onLoadRef.current?.(book);
|
|
1103
1522
|
return book;
|
|
1104
1523
|
} catch (err) {
|
|
1105
1524
|
if (requestId === requestIdRef.current) {
|
|
1106
|
-
const nextError =
|
|
1525
|
+
const nextError = toError(err);
|
|
1107
1526
|
setError(nextError);
|
|
1108
1527
|
onErrorRef.current?.(nextError);
|
|
1109
1528
|
}
|
|
@@ -1112,11 +1531,22 @@ function useEpub(options = {}) {
|
|
|
1112
1531
|
if (requestId === requestIdRef.current) setLoading(false);
|
|
1113
1532
|
}
|
|
1114
1533
|
}, []);
|
|
1534
|
+
const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
|
|
1535
|
+
const loadFile = useCallback3(
|
|
1536
|
+
(file) => load(() => readEpubFile(file, limitsRef.current)),
|
|
1537
|
+
[load]
|
|
1538
|
+
);
|
|
1539
|
+
const loadUrl = useCallback3(
|
|
1540
|
+
(url) => load(
|
|
1541
|
+
() => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
|
|
1542
|
+
),
|
|
1543
|
+
[load]
|
|
1544
|
+
);
|
|
1115
1545
|
const defaultUrl = options.defaultUrl;
|
|
1116
|
-
|
|
1546
|
+
useEffect5(() => {
|
|
1117
1547
|
if (defaultUrl) void loadUrl(defaultUrl);
|
|
1118
1548
|
}, [defaultUrl, loadUrl]);
|
|
1119
|
-
const replaceEpub =
|
|
1549
|
+
const replaceEpub = useCallback3((book) => {
|
|
1120
1550
|
requestIdRef.current++;
|
|
1121
1551
|
setLoading(false);
|
|
1122
1552
|
setError(null);
|
|
@@ -1127,15 +1557,15 @@ function useEpub(options = {}) {
|
|
|
1127
1557
|
|
|
1128
1558
|
// src/useMejiroBook.ts
|
|
1129
1559
|
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1130
|
-
import { useCallback as
|
|
1560
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
|
|
1131
1561
|
function useMejiroBook(initial, options = {}) {
|
|
1132
|
-
const bookRef =
|
|
1562
|
+
const bookRef = useRef8(null);
|
|
1133
1563
|
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1134
|
-
const [snapshot, setLocal] =
|
|
1135
|
-
const optionsRef =
|
|
1564
|
+
const [snapshot, setLocal] = useState6(initial);
|
|
1565
|
+
const optionsRef = useRef8(options);
|
|
1136
1566
|
optionsRef.current = options;
|
|
1137
|
-
const pendingRef =
|
|
1138
|
-
const flush =
|
|
1567
|
+
const pendingRef = useRef8(null);
|
|
1568
|
+
const flush = useCallback4(() => {
|
|
1139
1569
|
const pending = pendingRef.current;
|
|
1140
1570
|
if (!pending) return;
|
|
1141
1571
|
pendingRef.current = null;
|
|
@@ -1147,7 +1577,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1147
1577
|
for (const waiter of waiters) waiter.resolve();
|
|
1148
1578
|
},
|
|
1149
1579
|
(err) => {
|
|
1150
|
-
const error =
|
|
1580
|
+
const error = toError(err);
|
|
1151
1581
|
const handler = optionsRef.current.onError;
|
|
1152
1582
|
if (handler) {
|
|
1153
1583
|
handler(error);
|
|
@@ -1158,12 +1588,12 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1158
1588
|
}
|
|
1159
1589
|
);
|
|
1160
1590
|
}, []);
|
|
1161
|
-
const setOptions =
|
|
1591
|
+
const setOptions = useCallback4(
|
|
1162
1592
|
(next) => {
|
|
1163
|
-
setLocal((prev) => (
|
|
1593
|
+
setLocal((prev) => mergeDefined(prev, next));
|
|
1164
1594
|
const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
|
|
1165
1595
|
pendingRef.current = pending;
|
|
1166
|
-
|
|
1596
|
+
pending.patch = mergeDefined(pending.patch, next);
|
|
1167
1597
|
const settled = new Promise((resolve, reject) => {
|
|
1168
1598
|
pending.waiters.push({ resolve, reject });
|
|
1169
1599
|
});
|
|
@@ -1182,8 +1612,9 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1182
1612
|
},
|
|
1183
1613
|
[flush]
|
|
1184
1614
|
);
|
|
1185
|
-
|
|
1615
|
+
useEffect6(
|
|
1186
1616
|
() => () => {
|
|
1617
|
+
bookRef.current?.dispose();
|
|
1187
1618
|
const pending = pendingRef.current;
|
|
1188
1619
|
if (!pending) return;
|
|
1189
1620
|
pendingRef.current = null;
|
|
@@ -1195,205 +1626,8 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1195
1626
|
return { book: bookRef.current, options: snapshot, setOptions };
|
|
1196
1627
|
}
|
|
1197
1628
|
|
|
1198
|
-
// src/useMultiImageOverlay.ts
|
|
1199
|
-
import { createOverlayDragSession } from "@libraz/mejiro/browser";
|
|
1200
|
-
import {
|
|
1201
|
-
useCallback as useCallback4,
|
|
1202
|
-
useEffect as useEffect5,
|
|
1203
|
-
useMemo as useMemo3,
|
|
1204
|
-
useRef as useRef7,
|
|
1205
|
-
useState as useState5
|
|
1206
|
-
} from "react";
|
|
1207
|
-
var nextId = 0;
|
|
1208
|
-
function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
1209
|
-
const defW = options.defaultWidth ?? 120;
|
|
1210
|
-
const defH = options.defaultHeight ?? 160;
|
|
1211
|
-
const defX = options.defaultX ?? 80;
|
|
1212
|
-
const defY = options.defaultY ?? 100;
|
|
1213
|
-
const margin = options.margin;
|
|
1214
|
-
const [images, setImages] = useState5(() => /* @__PURE__ */ new Map());
|
|
1215
|
-
const imagesRef = useRef7(images);
|
|
1216
|
-
imagesRef.current = images;
|
|
1217
|
-
const layoutRef = useRef7(null);
|
|
1218
|
-
layoutRef.current = layout;
|
|
1219
|
-
const spreadIdxRef = useRef7(spreadIdx);
|
|
1220
|
-
spreadIdxRef.current = spreadIdx;
|
|
1221
|
-
const onUpdateRef = useRef7(options.onUpdate);
|
|
1222
|
-
onUpdateRef.current = options.onUpdate;
|
|
1223
|
-
const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1224
|
-
useEffect5(
|
|
1225
|
-
() => () => {
|
|
1226
|
-
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
1227
|
-
activeDragCleanupsRef.current.clear();
|
|
1228
|
-
},
|
|
1229
|
-
[]
|
|
1230
|
-
);
|
|
1231
|
-
const syncSpread = useCallback4(
|
|
1232
|
-
(si) => {
|
|
1233
|
-
const lo = layoutRef.current;
|
|
1234
|
-
if (!lo) return;
|
|
1235
|
-
const items = imagesRef.current.get(si) ?? [];
|
|
1236
|
-
const bookImages = items.map((it) => ({
|
|
1237
|
-
x: it.rect.x,
|
|
1238
|
-
y: it.rect.y,
|
|
1239
|
-
w: it.rect.w,
|
|
1240
|
-
h: it.rect.h,
|
|
1241
|
-
margin
|
|
1242
|
-
}));
|
|
1243
|
-
lo.setImages(si, bookImages);
|
|
1244
|
-
onUpdateRef.current?.(lo.getSpread(si));
|
|
1245
|
-
},
|
|
1246
|
-
[margin]
|
|
1247
|
-
);
|
|
1248
|
-
const commit = useCallback4(
|
|
1249
|
-
(next, affectedSpread) => {
|
|
1250
|
-
imagesRef.current = next;
|
|
1251
|
-
setImages(next);
|
|
1252
|
-
syncSpread(affectedSpread);
|
|
1253
|
-
},
|
|
1254
|
-
[syncSpread]
|
|
1255
|
-
);
|
|
1256
|
-
const findById = useCallback4(
|
|
1257
|
-
(id) => {
|
|
1258
|
-
for (const [s, list] of imagesRef.current) {
|
|
1259
|
-
const i = list.findIndex((it) => it.id === id);
|
|
1260
|
-
if (i >= 0) return { spread: s, index: i, item: list[i] };
|
|
1261
|
-
}
|
|
1262
|
-
return null;
|
|
1263
|
-
},
|
|
1264
|
-
[]
|
|
1265
|
-
);
|
|
1266
|
-
const addImage = useCallback4(
|
|
1267
|
-
(overrides) => {
|
|
1268
|
-
const si = spreadIdxRef.current;
|
|
1269
|
-
const list = imagesRef.current.get(si) ?? [];
|
|
1270
|
-
const last = list[list.length - 1];
|
|
1271
|
-
const baseX = last ? last.rect.x - 100 : defX;
|
|
1272
|
-
const baseY = last ? last.rect.y + 40 : defY;
|
|
1273
|
-
nextId += 1;
|
|
1274
|
-
const item = {
|
|
1275
|
-
id: `mejiro-img-${nextId}`,
|
|
1276
|
-
rect: {
|
|
1277
|
-
x: overrides?.x ?? baseX,
|
|
1278
|
-
y: overrides?.y ?? baseY,
|
|
1279
|
-
w: overrides?.w ?? defW,
|
|
1280
|
-
h: overrides?.h ?? defH
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
const next = new Map(imagesRef.current);
|
|
1284
|
-
next.set(si, [...list, item]);
|
|
1285
|
-
commit(next, si);
|
|
1286
|
-
return item;
|
|
1287
|
-
},
|
|
1288
|
-
[defX, defY, defW, defH, commit]
|
|
1289
|
-
);
|
|
1290
|
-
const removeImage = useCallback4(
|
|
1291
|
-
(id) => {
|
|
1292
|
-
const found = findById(id);
|
|
1293
|
-
if (!found) return;
|
|
1294
|
-
const next = new Map(imagesRef.current);
|
|
1295
|
-
const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
|
|
1296
|
-
if (list.length === 0) next.delete(found.spread);
|
|
1297
|
-
else next.set(found.spread, list);
|
|
1298
|
-
commit(next, found.spread);
|
|
1299
|
-
},
|
|
1300
|
-
[findById, commit]
|
|
1301
|
-
);
|
|
1302
|
-
const updateImage = useCallback4(
|
|
1303
|
-
(id, rect) => {
|
|
1304
|
-
const found = findById(id);
|
|
1305
|
-
if (!found) return;
|
|
1306
|
-
const next = new Map(imagesRef.current);
|
|
1307
|
-
const list = (next.get(found.spread) ?? []).map(
|
|
1308
|
-
(it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
|
|
1309
|
-
);
|
|
1310
|
-
next.set(found.spread, list);
|
|
1311
|
-
commit(next, found.spread);
|
|
1312
|
-
},
|
|
1313
|
-
[findById, commit]
|
|
1314
|
-
);
|
|
1315
|
-
const clearImages = useCallback4(
|
|
1316
|
-
(target) => {
|
|
1317
|
-
if (target == null) {
|
|
1318
|
-
const old = imagesRef.current;
|
|
1319
|
-
imagesRef.current = /* @__PURE__ */ new Map();
|
|
1320
|
-
setImages(/* @__PURE__ */ new Map());
|
|
1321
|
-
layoutRef.current?.clearImages();
|
|
1322
|
-
if (layoutRef.current) {
|
|
1323
|
-
for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
|
|
1324
|
-
}
|
|
1325
|
-
} else {
|
|
1326
|
-
const next = new Map(imagesRef.current);
|
|
1327
|
-
next.delete(target);
|
|
1328
|
-
commit(next, target);
|
|
1329
|
-
}
|
|
1330
|
-
},
|
|
1331
|
-
[commit]
|
|
1332
|
-
);
|
|
1333
|
-
const onOverlayPointerDown = useCallback4(
|
|
1334
|
-
(id, e) => {
|
|
1335
|
-
e.preventDefault();
|
|
1336
|
-
const found = findById(id);
|
|
1337
|
-
if (!found) return;
|
|
1338
|
-
const target = e.currentTarget;
|
|
1339
|
-
createOverlayDragSession({
|
|
1340
|
-
mode: "move",
|
|
1341
|
-
rect: found.item.rect,
|
|
1342
|
-
startX: e.clientX,
|
|
1343
|
-
startY: e.clientY,
|
|
1344
|
-
pointerId: e.nativeEvent.pointerId,
|
|
1345
|
-
captureElement: target,
|
|
1346
|
-
activeElement: target,
|
|
1347
|
-
dragClass: "is-dragging",
|
|
1348
|
-
registry: activeDragCleanupsRef.current,
|
|
1349
|
-
onChange: (rect) => updateImage(id, rect)
|
|
1350
|
-
});
|
|
1351
|
-
},
|
|
1352
|
-
[findById, updateImage]
|
|
1353
|
-
);
|
|
1354
|
-
const onResizePointerDown = useCallback4(
|
|
1355
|
-
(id, e) => {
|
|
1356
|
-
e.preventDefault();
|
|
1357
|
-
e.stopPropagation();
|
|
1358
|
-
const found = findById(id);
|
|
1359
|
-
if (!found) return;
|
|
1360
|
-
const target = e.currentTarget;
|
|
1361
|
-
createOverlayDragSession({
|
|
1362
|
-
mode: "resize",
|
|
1363
|
-
rect: found.item.rect,
|
|
1364
|
-
startX: e.clientX,
|
|
1365
|
-
startY: e.clientY,
|
|
1366
|
-
pointerId: e.nativeEvent.pointerId,
|
|
1367
|
-
captureElement: target,
|
|
1368
|
-
activeElement: target.parentElement,
|
|
1369
|
-
dragClass: "is-dragging",
|
|
1370
|
-
registry: activeDragCleanupsRef.current,
|
|
1371
|
-
onChange: (rect) => updateImage(id, rect)
|
|
1372
|
-
});
|
|
1373
|
-
},
|
|
1374
|
-
[findById, updateImage]
|
|
1375
|
-
);
|
|
1376
|
-
useEffect5(() => {
|
|
1377
|
-
if (!layout) return;
|
|
1378
|
-
for (const si of imagesRef.current.keys()) syncSpread(si);
|
|
1379
|
-
}, [layout, syncSpread]);
|
|
1380
|
-
const currentImages = useMemo3(() => images.get(spreadIdx) ?? [], [images, spreadIdx]);
|
|
1381
|
-
const hasImages = useMemo3(() => [...images.values()].some((list) => list.length > 0), [images]);
|
|
1382
|
-
return {
|
|
1383
|
-
imagesBySpread: images,
|
|
1384
|
-
currentImages,
|
|
1385
|
-
hasImages,
|
|
1386
|
-
addImage,
|
|
1387
|
-
removeImage,
|
|
1388
|
-
updateImage,
|
|
1389
|
-
clearImages,
|
|
1390
|
-
onOverlayPointerDown,
|
|
1391
|
-
onResizePointerDown
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
1629
|
// src/useSpread.ts
|
|
1396
|
-
import { useCallback as useCallback5, useEffect as
|
|
1630
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef9, useState as useState7 } from "react";
|
|
1397
1631
|
function ignoreNavigationKey(e) {
|
|
1398
1632
|
if (e.defaultPrevented) return true;
|
|
1399
1633
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
|
|
@@ -1403,29 +1637,57 @@ function ignoreNavigationKey(e) {
|
|
|
1403
1637
|
const tag = target.tagName;
|
|
1404
1638
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
1405
1639
|
}
|
|
1640
|
+
function navigationCount(totalPages, single) {
|
|
1641
|
+
return Math.max(1, single ? totalPages : Math.ceil(totalPages / 2));
|
|
1642
|
+
}
|
|
1643
|
+
function firstPageOf(index, single) {
|
|
1644
|
+
return single ? index : index * 2;
|
|
1645
|
+
}
|
|
1646
|
+
function indexOfPageIn(pageIdx, single) {
|
|
1647
|
+
return single ? pageIdx : Math.floor(pageIdx / 2);
|
|
1648
|
+
}
|
|
1649
|
+
function firstTextAnchorFrom(layout, page) {
|
|
1650
|
+
for (let p = page; p < layout.totalPages; p++) {
|
|
1651
|
+
const anchor = layout.anchorAt(Math.floor(p / 2), p % 2 === 0 ? "right" : "left");
|
|
1652
|
+
if (anchor) return anchor;
|
|
1653
|
+
}
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1406
1656
|
function useSpread(layout, options = {}) {
|
|
1407
1657
|
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1408
1658
|
const turnDuration = options.turnDuration ?? 180;
|
|
1409
|
-
const
|
|
1659
|
+
const single = options.single ?? false;
|
|
1660
|
+
const onChangeRef = useRef9(options.onChange);
|
|
1410
1661
|
onChangeRef.current = options.onChange;
|
|
1411
|
-
const [spreadIdx, setSpreadIdx] =
|
|
1412
|
-
const [
|
|
1413
|
-
|
|
1414
|
-
|
|
1662
|
+
const [spreadIdx, setSpreadIdx] = useState7(0);
|
|
1663
|
+
const [indexSingle, setIndexSingle] = useState7(single);
|
|
1664
|
+
if (indexSingle !== single) {
|
|
1665
|
+
setIndexSingle(single);
|
|
1666
|
+
setSpreadIdx(indexOfPageIn(firstPageOf(spreadIdx, indexSingle), single));
|
|
1667
|
+
}
|
|
1668
|
+
const [spread, setSpread] = useState7(null);
|
|
1669
|
+
const [turning, setTurning] = useState7(false);
|
|
1670
|
+
const layoutRef = useRef9(null);
|
|
1415
1671
|
layoutRef.current = layout;
|
|
1416
|
-
const spreadIdxRef =
|
|
1672
|
+
const spreadIdxRef = useRef9(0);
|
|
1417
1673
|
spreadIdxRef.current = spreadIdx;
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
const
|
|
1421
|
-
const
|
|
1422
|
-
const
|
|
1674
|
+
const singleRef = useRef9(single);
|
|
1675
|
+
singleRef.current = single;
|
|
1676
|
+
const turnTimerRef = useRef9(null);
|
|
1677
|
+
const layoutGenerationRef = useRef9(0);
|
|
1678
|
+
const lastEmittedRef = useRef9(null);
|
|
1679
|
+
const totalPages = spread?.totalPages ?? 0;
|
|
1680
|
+
const totalSpreads = navigationCount(totalPages, single);
|
|
1681
|
+
const firstPage = firstPageOf(spreadIdx, single);
|
|
1682
|
+
const layoutSpreadIdx = Math.floor(firstPage / 2);
|
|
1683
|
+
const singleSide = single ? firstPage % 2 === 0 ? "right" : "left" : null;
|
|
1423
1684
|
const refresh = useCallback5(() => {
|
|
1424
1685
|
if (!layoutRef.current) {
|
|
1425
1686
|
setSpread(null);
|
|
1426
1687
|
return;
|
|
1427
1688
|
}
|
|
1428
|
-
|
|
1689
|
+
const page = firstPageOf(spreadIdxRef.current, singleRef.current);
|
|
1690
|
+
setSpread(layoutRef.current.getSpread(Math.floor(page / 2)));
|
|
1429
1691
|
}, []);
|
|
1430
1692
|
useLayoutEffect3(() => {
|
|
1431
1693
|
layoutGenerationRef.current++;
|
|
@@ -1441,9 +1703,9 @@ function useSpread(layout, options = {}) {
|
|
|
1441
1703
|
}
|
|
1442
1704
|
setSpread(layout.getSpread(0));
|
|
1443
1705
|
}, [layout]);
|
|
1444
|
-
|
|
1706
|
+
useEffect7(() => {
|
|
1445
1707
|
if (!layout) return;
|
|
1446
|
-
setSpread(layout.getSpread(
|
|
1708
|
+
setSpread(layout.getSpread(layoutSpreadIdx));
|
|
1447
1709
|
const last = lastEmittedRef.current;
|
|
1448
1710
|
if (!last) {
|
|
1449
1711
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
@@ -1453,11 +1715,12 @@ function useSpread(layout, options = {}) {
|
|
|
1453
1715
|
onChangeRef.current?.(spreadIdx);
|
|
1454
1716
|
}
|
|
1455
1717
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
1456
|
-
}, [layout, spreadIdx]);
|
|
1718
|
+
}, [layout, spreadIdx, layoutSpreadIdx]);
|
|
1457
1719
|
const goTo = useCallback5(
|
|
1458
1720
|
(index) => {
|
|
1459
1721
|
if (!layoutRef.current) return;
|
|
1460
|
-
const
|
|
1722
|
+
const targetSingle = singleRef.current;
|
|
1723
|
+
const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
|
|
1461
1724
|
const target = Math.max(0, Math.min(max, index));
|
|
1462
1725
|
if (target === spreadIdxRef.current) return;
|
|
1463
1726
|
if (turnDuration > 0) {
|
|
@@ -1466,7 +1729,7 @@ function useSpread(layout, options = {}) {
|
|
|
1466
1729
|
setTurning(true);
|
|
1467
1730
|
turnTimerRef.current = setTimeout(() => {
|
|
1468
1731
|
if (generation !== layoutGenerationRef.current) return;
|
|
1469
|
-
setSpreadIdx(target);
|
|
1732
|
+
setSpreadIdx(indexOfPageIn(firstPageOf(target, targetSingle), singleRef.current));
|
|
1470
1733
|
setTurning(false);
|
|
1471
1734
|
turnTimerRef.current = null;
|
|
1472
1735
|
}, turnDuration);
|
|
@@ -1483,12 +1746,20 @@ function useSpread(layout, options = {}) {
|
|
|
1483
1746
|
turnTimerRef.current = null;
|
|
1484
1747
|
}
|
|
1485
1748
|
setTurning(false);
|
|
1486
|
-
const max =
|
|
1749
|
+
const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
|
|
1487
1750
|
setSpreadIdx(Math.max(0, Math.min(max, index)));
|
|
1488
1751
|
}, []);
|
|
1752
|
+
const indexOfPage = useCallback5((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
|
|
1753
|
+
const anchorAt = useCallback5(
|
|
1754
|
+
(index) => {
|
|
1755
|
+
if (!layout || index < 0 || index >= navigationCount(layout.totalPages, single)) return null;
|
|
1756
|
+
return firstTextAnchorFrom(layout, firstPageOf(index, single));
|
|
1757
|
+
},
|
|
1758
|
+
[layout, single]
|
|
1759
|
+
);
|
|
1489
1760
|
const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
|
|
1490
1761
|
const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
|
|
1491
|
-
|
|
1762
|
+
useEffect7(() => {
|
|
1492
1763
|
if (!enableKeyboard) return;
|
|
1493
1764
|
const onKey = (e) => {
|
|
1494
1765
|
if (!layoutRef.current) return;
|
|
@@ -1499,7 +1770,7 @@ function useSpread(layout, options = {}) {
|
|
|
1499
1770
|
window.addEventListener("keydown", onKey);
|
|
1500
1771
|
return () => window.removeEventListener("keydown", onKey);
|
|
1501
1772
|
}, [enableKeyboard, next, prev]);
|
|
1502
|
-
|
|
1773
|
+
useEffect7(
|
|
1503
1774
|
() => () => {
|
|
1504
1775
|
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1505
1776
|
},
|
|
@@ -1508,6 +1779,9 @@ function useSpread(layout, options = {}) {
|
|
|
1508
1779
|
return {
|
|
1509
1780
|
spreadIdx,
|
|
1510
1781
|
spread,
|
|
1782
|
+
firstPage,
|
|
1783
|
+
layoutSpreadIdx,
|
|
1784
|
+
singleSide,
|
|
1511
1785
|
totalPages,
|
|
1512
1786
|
totalSpreads,
|
|
1513
1787
|
turning,
|
|
@@ -1515,6 +1789,8 @@ function useSpread(layout, options = {}) {
|
|
|
1515
1789
|
prev,
|
|
1516
1790
|
goTo,
|
|
1517
1791
|
setSpread: setSpreadIndex,
|
|
1792
|
+
indexOfPage,
|
|
1793
|
+
anchorAt,
|
|
1518
1794
|
refresh
|
|
1519
1795
|
};
|
|
1520
1796
|
}
|
|
@@ -1522,6 +1798,52 @@ function useSpread(layout, options = {}) {
|
|
|
1522
1798
|
// src/MejiroReader.tsx
|
|
1523
1799
|
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1524
1800
|
var OPTIONS_DEBOUNCE_MS = 60;
|
|
1801
|
+
var IMAGE_DEFAULT_X = 80;
|
|
1802
|
+
function singlePageImagePlacement(side, pageWidth) {
|
|
1803
|
+
if (side == null) return void 0;
|
|
1804
|
+
return { x: side === "left" ? IMAGE_DEFAULT_X - pageWidth : IMAGE_DEFAULT_X };
|
|
1805
|
+
}
|
|
1806
|
+
var MANUSCRIPT_SOURCE = /* @__PURE__ */ Symbol("manuscript");
|
|
1807
|
+
function manuscriptKey(manuscript) {
|
|
1808
|
+
if (manuscript === void 0) return void 0;
|
|
1809
|
+
return JSON.stringify(manuscript.map(({ id, title, body }) => ({ id, title, body })));
|
|
1810
|
+
}
|
|
1811
|
+
function reflowOptionsKey(o) {
|
|
1812
|
+
return JSON.stringify([
|
|
1813
|
+
o.fontFamily,
|
|
1814
|
+
o.fontSize,
|
|
1815
|
+
o.lineSpacing,
|
|
1816
|
+
o.mode,
|
|
1817
|
+
o.enableHanging,
|
|
1818
|
+
o.headingScale,
|
|
1819
|
+
o.headingStyles
|
|
1820
|
+
]);
|
|
1821
|
+
}
|
|
1822
|
+
function scrollTargetPage(spread, userPage) {
|
|
1823
|
+
return userPage != null && spread.indexOfPage(userPage) === spread.spreadIdx ? userPage : spread.firstPage;
|
|
1824
|
+
}
|
|
1825
|
+
function createLifecycleTracker(sink) {
|
|
1826
|
+
let last = null;
|
|
1827
|
+
let dwell = null;
|
|
1828
|
+
return (chapter, spreadIdx, view, controlledSpreadIdx) => {
|
|
1829
|
+
if (!view) return;
|
|
1830
|
+
const totalSpreads = view.total;
|
|
1831
|
+
if (controlledSpreadIdx != null) {
|
|
1832
|
+
const target = Math.max(0, Math.min(totalSpreads - 1, controlledSpreadIdx));
|
|
1833
|
+
if (spreadIdx !== target) return;
|
|
1834
|
+
}
|
|
1835
|
+
if (last && last.chapter === chapter && last.spreadIdx === spreadIdx) return;
|
|
1836
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1837
|
+
const previous = last;
|
|
1838
|
+
if (previous && dwell) sink.pageRead(dwell.anchor, now - dwell.ts);
|
|
1839
|
+
const inCh = view.anchor;
|
|
1840
|
+
dwell = inCh ? { anchor: { chapter, ...inCh }, ts: now } : null;
|
|
1841
|
+
last = { chapter, spreadIdx };
|
|
1842
|
+
if (!previous) return;
|
|
1843
|
+
sink.spreadChanged(chapter, spreadIdx);
|
|
1844
|
+
if (spreadIdx === totalSpreads - 1) sink.chapterFinished(chapter);
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1525
1847
|
function MejiroReaderInner(props, ref) {
|
|
1526
1848
|
const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
|
|
1527
1849
|
const dialectProp = "dialect" in props ? props.dialect : void 0;
|
|
@@ -1568,15 +1890,18 @@ function MejiroReaderInner(props, ref) {
|
|
|
1568
1890
|
onPageRead,
|
|
1569
1891
|
onChapterCompleted
|
|
1570
1892
|
} = props;
|
|
1571
|
-
const surfaceRef =
|
|
1572
|
-
const fileRef =
|
|
1573
|
-
const [settingsOpen, setSettingsOpen] =
|
|
1574
|
-
const [chapterState, setChapterState] =
|
|
1893
|
+
const surfaceRef = useRef10(null);
|
|
1894
|
+
const fileRef = useRef10(null);
|
|
1895
|
+
const [settingsOpen, setSettingsOpen] = useState8(false);
|
|
1896
|
+
const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
|
|
1575
1897
|
const chapterIsUncontrolled = chapterProp == null;
|
|
1576
1898
|
const chapter = chapterProp ?? chapterState;
|
|
1577
|
-
const [chromeHidden, setChromeHidden] =
|
|
1578
|
-
const [
|
|
1579
|
-
|
|
1899
|
+
const [chromeHidden, setChromeHidden] = useState8(false);
|
|
1900
|
+
const [userScroll, setUserScroll] = useState8(
|
|
1901
|
+
null
|
|
1902
|
+
);
|
|
1903
|
+
const [autoSingle, setAutoSingle] = useState8(false);
|
|
1904
|
+
useEffect8(() => {
|
|
1580
1905
|
if (!settingsOpen) return;
|
|
1581
1906
|
const onKey = (e2) => {
|
|
1582
1907
|
if (e2.key === "Escape") setSettingsOpen(false);
|
|
@@ -1584,7 +1909,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1584
1909
|
window.addEventListener("keydown", onKey);
|
|
1585
1910
|
return () => window.removeEventListener("keydown", onKey);
|
|
1586
1911
|
}, [settingsOpen]);
|
|
1587
|
-
|
|
1912
|
+
useEffect8(() => {
|
|
1588
1913
|
if (spreadMode !== "auto") return;
|
|
1589
1914
|
const surface = surfaceRef.current;
|
|
1590
1915
|
if (!surface) return;
|
|
@@ -1601,16 +1926,19 @@ function MejiroReaderInner(props, ref) {
|
|
|
1601
1926
|
const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
|
|
1602
1927
|
const resolvedGeometry = useMemo4(() => {
|
|
1603
1928
|
const columns = effectiveSingle ? 1 : 2;
|
|
1604
|
-
|
|
1605
|
-
return {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1929
|
+
const geometry = pageGeometryProp ?? {};
|
|
1930
|
+
if (fit !== "width") return mergeDefined({ columns }, geometry);
|
|
1931
|
+
return mergeDefined(
|
|
1932
|
+
{
|
|
1933
|
+
columns,
|
|
1934
|
+
gutterOffset: 0,
|
|
1935
|
+
headerOffset: 0,
|
|
1936
|
+
maxHeight: Number.POSITIVE_INFINITY,
|
|
1937
|
+
minWidth: 0,
|
|
1938
|
+
minHeight: 0
|
|
1939
|
+
},
|
|
1940
|
+
geometry
|
|
1941
|
+
);
|
|
1614
1942
|
}, [fit, pageGeometryProp, effectiveSingle]);
|
|
1615
1943
|
const surfaceAspect = useMemo4(() => {
|
|
1616
1944
|
const columns = effectiveSingle ? 1 : 2;
|
|
@@ -1618,10 +1946,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
1618
1946
|
return `${columns} / ${aspect}`;
|
|
1619
1947
|
}, [effectiveSingle, resolvedGeometry]);
|
|
1620
1948
|
const resolvedOptions = useMemo4(
|
|
1621
|
-
() => (
|
|
1949
|
+
() => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
|
|
1622
1950
|
[optionsProp]
|
|
1623
1951
|
);
|
|
1624
|
-
const onErrorRef =
|
|
1952
|
+
const onErrorRef = useRef10(onError);
|
|
1625
1953
|
onErrorRef.current = onError;
|
|
1626
1954
|
const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
|
|
1627
1955
|
const {
|
|
@@ -1633,18 +1961,20 @@ function MejiroReaderInner(props, ref) {
|
|
|
1633
1961
|
onError: reportError
|
|
1634
1962
|
});
|
|
1635
1963
|
const optionsPropKey = JSON.stringify(resolvedOptions);
|
|
1636
|
-
const syncedOptionsPropKeyRef =
|
|
1637
|
-
const resolvedOptionsRef =
|
|
1964
|
+
const syncedOptionsPropKeyRef = useRef10(optionsPropKey);
|
|
1965
|
+
const resolvedOptionsRef = useRef10(resolvedOptions);
|
|
1638
1966
|
resolvedOptionsRef.current = resolvedOptions;
|
|
1639
|
-
|
|
1967
|
+
useEffect8(() => {
|
|
1640
1968
|
if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
|
|
1641
1969
|
syncedOptionsPropKeyRef.current = optionsPropKey;
|
|
1642
1970
|
void setOptions(resolvedOptionsRef.current);
|
|
1643
1971
|
}, [optionsPropKey, setOptions]);
|
|
1972
|
+
const manuscriptSourceKey = manuscriptKey(manuscriptProp);
|
|
1644
1973
|
const synthesizedEpub = useMemo4(() => {
|
|
1645
|
-
if (
|
|
1646
|
-
|
|
1647
|
-
|
|
1974
|
+
if (manuscriptSourceKey === void 0) return null;
|
|
1975
|
+
const chapters = JSON.parse(manuscriptSourceKey);
|
|
1976
|
+
return manuscriptToEpubBook(chapters, { dialect: dialectProp });
|
|
1977
|
+
}, [manuscriptSourceKey, dialectProp]);
|
|
1648
1978
|
const epubCtx = useEpub({
|
|
1649
1979
|
// `epub` / `manuscript` take precedence: skip the URL fetch when a parsed
|
|
1650
1980
|
// book (or a synthesized manuscript book) is supplied.
|
|
@@ -1654,59 +1984,74 @@ function MejiroReaderInner(props, ref) {
|
|
|
1654
1984
|
fetchEpub: fetchEpubFn,
|
|
1655
1985
|
onLoad: (b) => {
|
|
1656
1986
|
if (chapterProp == null) setChapterState(0);
|
|
1987
|
+
clearImagesRef.current();
|
|
1657
1988
|
onLoad?.(b);
|
|
1658
1989
|
}
|
|
1659
1990
|
});
|
|
1660
1991
|
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
1661
1992
|
const isManuscriptSource = manuscriptProp !== void 0;
|
|
1662
1993
|
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
1663
|
-
|
|
1994
|
+
useEffect8(() => {
|
|
1664
1995
|
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
1665
1996
|
}, [epubCtx.error]);
|
|
1666
|
-
const pageGeometryRef =
|
|
1997
|
+
const pageGeometryRef = useRef10(resolvedGeometry);
|
|
1667
1998
|
pageGeometryRef.current = resolvedGeometry;
|
|
1668
|
-
const
|
|
1669
|
-
const spreadIdxPropRef =
|
|
1999
|
+
const spreadAnchorRef = useRef10(() => null);
|
|
2000
|
+
const spreadIdxPropRef = useRef10(spreadIdxProp);
|
|
1670
2001
|
spreadIdxPropRef.current = spreadIdxProp;
|
|
2002
|
+
const visibleAnchorRef = useRef10(
|
|
2003
|
+
null
|
|
2004
|
+
);
|
|
1671
2005
|
const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
|
|
1672
2006
|
pageGeometry: () => pageGeometryRef.current,
|
|
1673
2007
|
// Preserve the reading position across a reflow re-layout (size / option
|
|
1674
2008
|
// changes), but only in uncontrolled mode — when `spreadIdx` is controlled
|
|
1675
2009
|
// the host owns the position and the controlled-restore effect handles it.
|
|
1676
|
-
capturePosition: (layout2) =>
|
|
2010
|
+
capturePosition: (layout2) => {
|
|
2011
|
+
if (spreadIdxPropRef.current != null) return null;
|
|
2012
|
+
const visible = visibleAnchorRef.current;
|
|
2013
|
+
if (visible?.layout === layout2) return visible.anchor;
|
|
2014
|
+
return spreadAnchorRef.current();
|
|
2015
|
+
}
|
|
1677
2016
|
});
|
|
1678
|
-
const spreadChangedRef =
|
|
2017
|
+
const spreadChangedRef = useRef10(void 0);
|
|
1679
2018
|
spreadChangedRef.current = (i) => {
|
|
1680
2019
|
onSpreadChange?.(i);
|
|
1681
2020
|
onSpreadIdxChange?.(i);
|
|
1682
2021
|
};
|
|
1683
2022
|
const spreadCtx = useSpread(layoutCtx.layout, {
|
|
1684
2023
|
enableKeyboard,
|
|
2024
|
+
single: effectiveSingle,
|
|
1685
2025
|
onChange: (i) => spreadChangedRef.current?.(i)
|
|
1686
2026
|
});
|
|
1687
|
-
|
|
1688
|
-
const
|
|
2027
|
+
spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
2028
|
+
const indexOfPageRef = useRef10(spreadCtx.indexOfPage);
|
|
2029
|
+
indexOfPageRef.current = spreadCtx.indexOfPage;
|
|
2030
|
+
const anchorSingleRef = useRef10(effectiveSingle);
|
|
2031
|
+
useLayoutEffect4(() => {
|
|
2032
|
+
const l = layoutCtx.layout;
|
|
2033
|
+
const flipped = anchorSingleRef.current !== effectiveSingle;
|
|
2034
|
+
anchorSingleRef.current = effectiveSingle;
|
|
2035
|
+
if (flipped && l && visibleAnchorRef.current?.layout === l) return;
|
|
2036
|
+
visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2037
|
+
}, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
|
|
2038
|
+
const setSpreadRef = useRef10(spreadCtx.setSpread);
|
|
1689
2039
|
setSpreadRef.current = spreadCtx.setSpread;
|
|
1690
2040
|
useLayoutEffect4(() => {
|
|
1691
2041
|
const anchor = layoutCtx.pendingRestore.current;
|
|
1692
2042
|
if (!(anchor && layoutCtx.layout)) return;
|
|
1693
2043
|
layoutCtx.pendingRestore.current = null;
|
|
1694
2044
|
const loc = layoutCtx.layout.locateAnchor(anchor);
|
|
1695
|
-
setSpreadRef.current(loc
|
|
2045
|
+
setSpreadRef.current(loc ? indexOfPageRef.current(loc.pageIdx) : 0);
|
|
2046
|
+
visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
|
|
1696
2047
|
}, [layoutCtx.layout]);
|
|
1697
|
-
const optionsKey =
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
bookOptions.lineSpacing,
|
|
1701
|
-
bookOptions.mode,
|
|
1702
|
-
bookOptions.enableHanging
|
|
1703
|
-
].join("|");
|
|
1704
|
-
const optionsKeyRef = useRef9(optionsKey);
|
|
1705
|
-
const bookOptionsRef = useRef9(bookOptions);
|
|
2048
|
+
const optionsKey = reflowOptionsKey(bookOptions);
|
|
2049
|
+
const optionsKeyRef = useRef10(optionsKey);
|
|
2050
|
+
const bookOptionsRef = useRef10(bookOptions);
|
|
1706
2051
|
bookOptionsRef.current = bookOptions;
|
|
1707
|
-
const recomputeRef =
|
|
2052
|
+
const recomputeRef = useRef10(layoutCtx.recompute);
|
|
1708
2053
|
recomputeRef.current = layoutCtx.recompute;
|
|
1709
|
-
|
|
2054
|
+
useEffect8(() => {
|
|
1710
2055
|
if (optionsKeyRef.current === optionsKey) return;
|
|
1711
2056
|
optionsKeyRef.current = optionsKey;
|
|
1712
2057
|
const timer = setTimeout(() => {
|
|
@@ -1715,20 +2060,21 @@ function MejiroReaderInner(props, ref) {
|
|
|
1715
2060
|
await setOptions({ ...bookOptionsRef.current });
|
|
1716
2061
|
await recomputeRef.current({ blank: false });
|
|
1717
2062
|
} catch (err) {
|
|
1718
|
-
reportError(
|
|
2063
|
+
reportError(toError(err));
|
|
1719
2064
|
}
|
|
1720
2065
|
})();
|
|
1721
2066
|
}, OPTIONS_DEBOUNCE_MS);
|
|
1722
2067
|
return () => clearTimeout(timer);
|
|
1723
2068
|
}, [optionsKey, setOptions, reportError]);
|
|
1724
2069
|
const geometryKey = JSON.stringify(resolvedGeometry ?? null);
|
|
1725
|
-
const geometryKeyRef =
|
|
1726
|
-
|
|
2070
|
+
const geometryKeyRef = useRef10(geometryKey);
|
|
2071
|
+
useEffect8(() => {
|
|
1727
2072
|
if (geometryKeyRef.current === geometryKey) return;
|
|
1728
2073
|
geometryKeyRef.current = geometryKey;
|
|
1729
2074
|
void recomputeRef.current({ blank: false });
|
|
1730
2075
|
}, [geometryKey]);
|
|
1731
|
-
const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.
|
|
2076
|
+
const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.layoutSpreadIdx, {
|
|
2077
|
+
defaultX: IMAGE_DEFAULT_X,
|
|
1732
2078
|
onUpdate: () => spreadCtx.refresh()
|
|
1733
2079
|
});
|
|
1734
2080
|
const annotationRects = useMemo4(() => {
|
|
@@ -1743,22 +2089,31 @@ function MejiroReaderInner(props, ref) {
|
|
|
1743
2089
|
for (const rect of rects) result.push({ ...rect, color: annotation.color });
|
|
1744
2090
|
}
|
|
1745
2091
|
return result;
|
|
1746
|
-
}, [annotations, layoutCtx.layout, chapter]);
|
|
1747
|
-
const clearImagesRef =
|
|
2092
|
+
}, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
|
|
2093
|
+
const clearImagesRef = useRef10(imageCtx.clearImages);
|
|
1748
2094
|
clearImagesRef.current = imageCtx.clearImages;
|
|
1749
|
-
const onLoadRef =
|
|
2095
|
+
const onLoadRef = useRef10(onLoad);
|
|
1750
2096
|
onLoadRef.current = onLoad;
|
|
1751
|
-
|
|
2097
|
+
const eRef = useRef10(e);
|
|
2098
|
+
eRef.current = e;
|
|
2099
|
+
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
|
|
2100
|
+
useEffect8(() => {
|
|
1752
2101
|
if (!controlled) return;
|
|
1753
2102
|
if (chapterIsUncontrolled) setChapterState(0);
|
|
1754
2103
|
clearImagesRef.current();
|
|
1755
2104
|
book.clearCache();
|
|
1756
|
-
if (
|
|
1757
|
-
}, [controlled,
|
|
1758
|
-
const
|
|
2105
|
+
if (eRef.current) onLoadRef.current?.(eRef.current);
|
|
2106
|
+
}, [controlled, sourceKey, book, chapterIsUncontrolled]);
|
|
2107
|
+
const imagesChapterRef = useRef10(chapter);
|
|
2108
|
+
useEffect8(() => {
|
|
2109
|
+
if (imagesChapterRef.current === chapter) return;
|
|
2110
|
+
imagesChapterRef.current = chapter;
|
|
2111
|
+
clearImagesRef.current();
|
|
2112
|
+
}, [chapter]);
|
|
2113
|
+
const spreadGoToRef = useRef10(spreadCtx.goTo);
|
|
1759
2114
|
spreadGoToRef.current = spreadCtx.goTo;
|
|
1760
|
-
const appliedSpreadIdxPropRef =
|
|
1761
|
-
|
|
2115
|
+
const appliedSpreadIdxPropRef = useRef10(spreadIdxProp);
|
|
2116
|
+
useEffect8(() => {
|
|
1762
2117
|
if (spreadIdxProp == null) {
|
|
1763
2118
|
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
1764
2119
|
return;
|
|
@@ -1782,7 +2137,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1782
2137
|
},
|
|
1783
2138
|
[chapter, chapterProp, onChapterChange]
|
|
1784
2139
|
);
|
|
1785
|
-
const listenersRef =
|
|
2140
|
+
const listenersRef = useRef10(/* @__PURE__ */ new Map());
|
|
1786
2141
|
const emit = useCallback6(
|
|
1787
2142
|
(event, payload) => {
|
|
1788
2143
|
const set = listenersRef.current.get(event);
|
|
@@ -1791,8 +2146,15 @@ function MejiroReaderInner(props, ref) {
|
|
|
1791
2146
|
},
|
|
1792
2147
|
[]
|
|
1793
2148
|
);
|
|
1794
|
-
const pendingAnchorRef =
|
|
1795
|
-
const
|
|
2149
|
+
const pendingAnchorRef = useRef10(null);
|
|
2150
|
+
const layoutOwnerRef = useRef10({
|
|
2151
|
+
layout: null,
|
|
2152
|
+
chapter
|
|
2153
|
+
});
|
|
2154
|
+
if (layoutOwnerRef.current.layout !== layoutCtx.layout) {
|
|
2155
|
+
layoutOwnerRef.current = { layout: layoutCtx.layout, chapter };
|
|
2156
|
+
}
|
|
2157
|
+
const layout = layoutOwnerRef.current.chapter === chapter ? layoutCtx.layout : null;
|
|
1796
2158
|
const tryApplyPendingAnchor = useCallback6(() => {
|
|
1797
2159
|
const pending = pendingAnchorRef.current;
|
|
1798
2160
|
if (!pending) return;
|
|
@@ -1804,48 +2166,45 @@ function MejiroReaderInner(props, ref) {
|
|
|
1804
2166
|
});
|
|
1805
2167
|
if (!loc) return;
|
|
1806
2168
|
pendingAnchorRef.current = null;
|
|
1807
|
-
spreadGoToRef.current(loc.
|
|
2169
|
+
spreadGoToRef.current(indexOfPageRef.current(loc.pageIdx));
|
|
1808
2170
|
pending.resolve();
|
|
1809
2171
|
}, [chapter, layout]);
|
|
1810
|
-
|
|
2172
|
+
useEffect8(() => {
|
|
1811
2173
|
tryApplyPendingAnchor();
|
|
1812
2174
|
}, [tryApplyPendingAnchor]);
|
|
1813
|
-
|
|
2175
|
+
useEffect8(
|
|
1814
2176
|
() => () => {
|
|
1815
2177
|
pendingAnchorRef.current?.resolve();
|
|
1816
2178
|
pendingAnchorRef.current = null;
|
|
1817
2179
|
},
|
|
1818
2180
|
[]
|
|
1819
2181
|
);
|
|
1820
|
-
const
|
|
1821
|
-
const dwellRef = useRef9(null);
|
|
1822
|
-
const onPageReadRef = useRef9(onPageRead);
|
|
2182
|
+
const onPageReadRef = useRef10(onPageRead);
|
|
1823
2183
|
onPageReadRef.current = onPageRead;
|
|
1824
|
-
const onChapterCompletedRef =
|
|
2184
|
+
const onChapterCompletedRef = useRef10(onChapterCompleted);
|
|
1825
2185
|
onChapterCompletedRef.current = onChapterCompleted;
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
useEffect7(() => {
|
|
2186
|
+
const trackLifecycleRef = useRef10(null);
|
|
2187
|
+
trackLifecycleRef.current ??= createLifecycleTracker({
|
|
2188
|
+
spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
|
|
2189
|
+
chapterFinished: (ch) => {
|
|
2190
|
+
emit("chapterFinished", { chapter: ch });
|
|
2191
|
+
onChapterCompletedRef.current?.(ch);
|
|
2192
|
+
},
|
|
2193
|
+
pageRead: (anchor, dwellMs) => onPageReadRef.current?.(anchor, dwellMs)
|
|
2194
|
+
});
|
|
2195
|
+
useEffect8(() => {
|
|
2196
|
+
const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2197
|
+
trackLifecycleRef.current?.(chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
|
|
2198
|
+
}, [
|
|
2199
|
+
chapter,
|
|
2200
|
+
spreadCtx.spreadIdx,
|
|
2201
|
+
spreadCtx.totalSpreads,
|
|
2202
|
+
spreadCtx.anchorAt,
|
|
2203
|
+
layout,
|
|
2204
|
+
spreadIdxProp
|
|
2205
|
+
]);
|
|
2206
|
+
const prevTurningRef = useRef10(false);
|
|
2207
|
+
useEffect8(() => {
|
|
1849
2208
|
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
1850
2209
|
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
1851
2210
|
} else if (!spreadCtx.turning && prevTurningRef.current) {
|
|
@@ -1874,23 +2233,14 @@ function MejiroReaderInner(props, ref) {
|
|
|
1874
2233
|
}),
|
|
1875
2234
|
getAnchor: () => {
|
|
1876
2235
|
if (!layout) return null;
|
|
1877
|
-
const inCh =
|
|
2236
|
+
const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
1878
2237
|
return inCh ? { chapter, ...inCh } : null;
|
|
1879
2238
|
},
|
|
1880
2239
|
getVisibleRange: () => {
|
|
1881
2240
|
if (!layout) return null;
|
|
1882
|
-
const start =
|
|
2241
|
+
const start = spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
1883
2242
|
if (!start) return null;
|
|
1884
|
-
const
|
|
1885
|
-
let end;
|
|
1886
|
-
if (next) {
|
|
1887
|
-
end = next;
|
|
1888
|
-
} else {
|
|
1889
|
-
const ch = e?.chapters[chapter];
|
|
1890
|
-
const lastP = (ch?.paragraphs.length ?? 1) - 1;
|
|
1891
|
-
const lastText = ch?.paragraphs[lastP]?.text ?? "";
|
|
1892
|
-
end = { paragraph: Math.max(0, lastP), charIndex: [...lastText].length };
|
|
1893
|
-
}
|
|
2243
|
+
const end = spreadCtx.anchorAt(spreadCtx.spreadIdx + 1) ?? layout.endAnchor() ?? start;
|
|
1894
2244
|
return {
|
|
1895
2245
|
start: { chapter, ...start },
|
|
1896
2246
|
end: { chapter, ...end }
|
|
@@ -1915,10 +2265,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
1915
2265
|
spreadCtx.spreadIdx,
|
|
1916
2266
|
spreadCtx.totalPages,
|
|
1917
2267
|
spreadCtx.totalSpreads,
|
|
2268
|
+
spreadCtx.anchorAt,
|
|
1918
2269
|
onChapter,
|
|
1919
2270
|
chapter,
|
|
1920
2271
|
layout,
|
|
1921
|
-
e,
|
|
1922
2272
|
setOptions,
|
|
1923
2273
|
tryApplyPendingAnchor
|
|
1924
2274
|
]
|
|
@@ -1970,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1970
2320
|
MejiroStats,
|
|
1971
2321
|
{
|
|
1972
2322
|
chapter: e?.chapters[chapter] ?? null,
|
|
1973
|
-
totalPages:
|
|
2323
|
+
totalPages: spreadCtx.totalPages,
|
|
1974
2324
|
elapsedMs: layoutCtx.elapsedMs,
|
|
1975
2325
|
fontLabel
|
|
1976
2326
|
}
|
|
@@ -1989,7 +2339,12 @@ function MejiroReaderInner(props, ref) {
|
|
|
1989
2339
|
{
|
|
1990
2340
|
type: "button",
|
|
1991
2341
|
className: `mejiro-reader-btn${imageCtx.hasImages ? " is-active" : ""}`,
|
|
1992
|
-
onClick: () => imageCtx.addImage(
|
|
2342
|
+
onClick: () => imageCtx.addImage(
|
|
2343
|
+
singlePageImagePlacement(
|
|
2344
|
+
mode === "paginated" ? spreadCtx.singleSide : null,
|
|
2345
|
+
layoutCtx.pageWidth
|
|
2346
|
+
)
|
|
2347
|
+
),
|
|
1993
2348
|
children: resolvedMessages.imageButton
|
|
1994
2349
|
}
|
|
1995
2350
|
),
|
|
@@ -2007,7 +2362,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2007
2362
|
)
|
|
2008
2363
|
] })
|
|
2009
2364
|
] }) : null;
|
|
2010
|
-
const currentSpread = spreadCtx.
|
|
2365
|
+
const currentSpread = spreadCtx.layoutSpreadIdx;
|
|
2011
2366
|
const rightPage = currentSpread * 2 + 1;
|
|
2012
2367
|
const leftPage = currentSpread * 2 + 2;
|
|
2013
2368
|
const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
|
|
@@ -2051,18 +2406,29 @@ function MejiroReaderInner(props, ref) {
|
|
|
2051
2406
|
fontFamily: bookOptions.fontFamily,
|
|
2052
2407
|
fontSize: bookOptions.fontSize,
|
|
2053
2408
|
lineSpacing: bookOptions.lineSpacing,
|
|
2054
|
-
scrollToPage:
|
|
2409
|
+
scrollToPage: scrollTargetPage(
|
|
2410
|
+
spreadCtx,
|
|
2411
|
+
userScroll?.layout === layoutCtx.layout ? userScroll.page : null
|
|
2412
|
+
),
|
|
2055
2413
|
onVisiblePageChange: (pageIdx, source) => {
|
|
2056
2414
|
if (source === "programmatic") return;
|
|
2057
|
-
|
|
2415
|
+
if (layoutCtx.layout) setUserScroll({ layout: layoutCtx.layout, page: pageIdx });
|
|
2416
|
+
const target = spreadCtx.indexOfPage(pageIdx);
|
|
2058
2417
|
if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
|
|
2059
|
-
}
|
|
2418
|
+
},
|
|
2419
|
+
selectionRects: annotationRects.length ? annotationRects : void 0,
|
|
2420
|
+
spreadIdx: spreadCtx.layoutSpreadIdx,
|
|
2421
|
+
images: imageCtx.currentImages,
|
|
2422
|
+
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
2423
|
+
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
2424
|
+
onImageClose: imageCtx.removeImage
|
|
2060
2425
|
}
|
|
2061
2426
|
),
|
|
2062
2427
|
layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
|
|
2063
2428
|
MejiroSpread,
|
|
2064
2429
|
{
|
|
2065
2430
|
singlePage: effectiveSingle,
|
|
2431
|
+
singleSide: spreadCtx.singleSide ?? void 0,
|
|
2066
2432
|
onSwipe: (dir) => dir === "next" ? spreadCtx.next() : spreadCtx.prev(),
|
|
2067
2433
|
onSurfaceTap: enableSurfaceTap ? () => setChromeHidden((v) => !v) : void 0,
|
|
2068
2434
|
spread: spreadCtx.spread,
|
|
@@ -2094,7 +2460,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2094
2460
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
2095
2461
|
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
2096
2462
|
onImageClose: imageCtx.removeImage,
|
|
2097
|
-
spreadIdx: spreadCtx.
|
|
2463
|
+
spreadIdx: spreadCtx.layoutSpreadIdx,
|
|
2098
2464
|
selectionRects: annotationRects.length ? annotationRects : void 0
|
|
2099
2465
|
},
|
|
2100
2466
|
`${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
|
|
@@ -2178,21 +2544,22 @@ function MejiroEditor({
|
|
|
2178
2544
|
onError
|
|
2179
2545
|
}) {
|
|
2180
2546
|
const messages = useI18n();
|
|
2181
|
-
const imageInputRef =
|
|
2182
|
-
const textareaRef =
|
|
2183
|
-
const
|
|
2184
|
-
const [
|
|
2185
|
-
const [
|
|
2186
|
-
const [
|
|
2187
|
-
const [
|
|
2188
|
-
const [
|
|
2189
|
-
const [
|
|
2190
|
-
const [
|
|
2191
|
-
const [
|
|
2192
|
-
const
|
|
2193
|
-
const
|
|
2194
|
-
const
|
|
2195
|
-
const
|
|
2547
|
+
const imageInputRef = useRef11(null);
|
|
2548
|
+
const textareaRef = useRef11(null);
|
|
2549
|
+
const readerRef = useRef11(null);
|
|
2550
|
+
const [editor, setEditor] = useState9(null);
|
|
2551
|
+
const [loading, setLoading] = useState9(false);
|
|
2552
|
+
const [error, setError] = useState9(null);
|
|
2553
|
+
const [revision, setRevision] = useState9(0);
|
|
2554
|
+
const [selection, setSelection] = useState9({ chapter: 0, paragraph: 0 });
|
|
2555
|
+
const [text, setText] = useState9("");
|
|
2556
|
+
const [rubyStart, setRubyStart] = useState9(0);
|
|
2557
|
+
const [rubyEnd, setRubyEnd] = useState9(1);
|
|
2558
|
+
const [rubyText, setRubyText] = useState9("");
|
|
2559
|
+
const loadRequestIdRef = useRef11(0);
|
|
2560
|
+
const onLoadRef = useRef11(onLoad);
|
|
2561
|
+
const onErrorRef = useRef11(onError);
|
|
2562
|
+
const limitsRef = useRef11(limits);
|
|
2196
2563
|
limitsRef.current = limits;
|
|
2197
2564
|
const book = editor?.book ?? null;
|
|
2198
2565
|
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
@@ -2202,48 +2569,49 @@ function MejiroEditor({
|
|
|
2202
2569
|
void revision;
|
|
2203
2570
|
return book ? cloneEditableEpubBook(book) : null;
|
|
2204
2571
|
}, [book, revision]);
|
|
2205
|
-
|
|
2572
|
+
useEffect9(() => {
|
|
2206
2573
|
onLoadRef.current = onLoad;
|
|
2207
2574
|
}, [onLoad]);
|
|
2208
|
-
|
|
2575
|
+
useEffect9(() => {
|
|
2209
2576
|
onErrorRef.current = onError;
|
|
2210
2577
|
}, [onError]);
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
try {
|
|
2215
|
-
const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
|
|
2216
|
-
if (requestId !== loadRequestIdRef.current) return;
|
|
2217
|
-
setEditor(next);
|
|
2218
|
-
setSelection({ chapter: 0, paragraph: 0 });
|
|
2219
|
-
setRevision((value) => value + 1);
|
|
2220
|
-
onLoadRef.current?.(next);
|
|
2221
|
-
} catch (err) {
|
|
2222
|
-
if (requestId === loadRequestIdRef.current) {
|
|
2223
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2224
|
-
setError(nextError);
|
|
2225
|
-
onErrorRef.current?.(nextError);
|
|
2226
|
-
}
|
|
2227
|
-
} finally {
|
|
2228
|
-
if (requestId === loadRequestIdRef.current) setLoading(false);
|
|
2229
|
-
}
|
|
2578
|
+
const reportError = useCallback7((nextError) => {
|
|
2579
|
+
setError(nextError);
|
|
2580
|
+
onErrorRef.current?.(nextError);
|
|
2230
2581
|
}, []);
|
|
2582
|
+
const loadBufferForRequest = useCallback7(
|
|
2583
|
+
async (buffer, requestId) => {
|
|
2584
|
+
setLoading(true);
|
|
2585
|
+
setError(null);
|
|
2586
|
+
try {
|
|
2587
|
+
const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
|
|
2588
|
+
if (requestId !== loadRequestIdRef.current) return;
|
|
2589
|
+
setEditor(next);
|
|
2590
|
+
setSelection({ chapter: 0, paragraph: 0 });
|
|
2591
|
+
setRevision((value) => value + 1);
|
|
2592
|
+
onLoadRef.current?.(next);
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
if (requestId === loadRequestIdRef.current) reportError(toError(err));
|
|
2595
|
+
} finally {
|
|
2596
|
+
if (requestId === loadRequestIdRef.current) setLoading(false);
|
|
2597
|
+
}
|
|
2598
|
+
},
|
|
2599
|
+
[reportError]
|
|
2600
|
+
);
|
|
2231
2601
|
async function loadFile(file) {
|
|
2232
2602
|
const requestId = ++loadRequestIdRef.current;
|
|
2233
2603
|
setLoading(true);
|
|
2234
2604
|
setError(null);
|
|
2235
2605
|
try {
|
|
2236
|
-
await loadBufferForRequest(await file.
|
|
2606
|
+
await loadBufferForRequest(await readEpubFile(file, limitsRef.current), requestId);
|
|
2237
2607
|
} catch (err) {
|
|
2238
2608
|
if (requestId === loadRequestIdRef.current) {
|
|
2239
|
-
|
|
2240
|
-
setError(nextError);
|
|
2241
|
-
onErrorRef.current?.(nextError);
|
|
2609
|
+
reportError(toError(err));
|
|
2242
2610
|
setLoading(false);
|
|
2243
2611
|
}
|
|
2244
2612
|
}
|
|
2245
2613
|
}
|
|
2246
|
-
|
|
2614
|
+
useEffect9(() => {
|
|
2247
2615
|
if (!epubUrl) return;
|
|
2248
2616
|
let cancelled = false;
|
|
2249
2617
|
const requestId = ++loadRequestIdRef.current;
|
|
@@ -2251,17 +2619,11 @@ function MejiroEditor({
|
|
|
2251
2619
|
setLoading(true);
|
|
2252
2620
|
setError(null);
|
|
2253
2621
|
try {
|
|
2254
|
-
const
|
|
2255
|
-
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
2256
|
-
const buffer = await res.arrayBuffer();
|
|
2622
|
+
const buffer = await fetchEpubBuffer(epubUrl);
|
|
2257
2623
|
if (cancelled || requestId !== loadRequestIdRef.current) return;
|
|
2258
2624
|
await loadBufferForRequest(buffer, requestId);
|
|
2259
2625
|
} catch (err) {
|
|
2260
|
-
if (!cancelled && requestId === loadRequestIdRef.current)
|
|
2261
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2262
|
-
setError(nextError);
|
|
2263
|
-
onErrorRef.current?.(nextError);
|
|
2264
|
-
}
|
|
2626
|
+
if (!cancelled && requestId === loadRequestIdRef.current) reportError(toError(err));
|
|
2265
2627
|
} finally {
|
|
2266
2628
|
if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
|
|
2267
2629
|
}
|
|
@@ -2269,13 +2631,32 @@ function MejiroEditor({
|
|
|
2269
2631
|
return () => {
|
|
2270
2632
|
cancelled = true;
|
|
2271
2633
|
};
|
|
2272
|
-
}, [epubUrl, loadBufferForRequest]);
|
|
2273
|
-
|
|
2274
|
-
|
|
2634
|
+
}, [epubUrl, loadBufferForRequest, reportError]);
|
|
2635
|
+
const syncedRef = useRef11(
|
|
2636
|
+
null
|
|
2637
|
+
);
|
|
2638
|
+
useEffect9(() => {
|
|
2639
|
+
const nextText = paragraph?.text ?? "";
|
|
2640
|
+
const target = `${selection.chapter}:${selection.paragraph}`;
|
|
2641
|
+
const prev = syncedRef.current;
|
|
2642
|
+
syncedRef.current = { editor, target, text: nextText };
|
|
2643
|
+
if (prev && prev.editor === editor && prev.target === target) {
|
|
2644
|
+
setText((current) => current === prev.text ? nextText : current);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
setText(nextText);
|
|
2275
2648
|
setRubyStart(0);
|
|
2276
|
-
setRubyEnd(Math.min(1, [...
|
|
2649
|
+
setRubyEnd(Math.min(1, [...nextText].length));
|
|
2277
2650
|
setRubyText("");
|
|
2278
|
-
}, [paragraph]);
|
|
2651
|
+
}, [editor, selection.chapter, selection.paragraph, paragraph]);
|
|
2652
|
+
useEffect9(() => {
|
|
2653
|
+
if (!previewBook) return;
|
|
2654
|
+
void readerRef.current?.goToAnchor({
|
|
2655
|
+
chapter: selection.chapter,
|
|
2656
|
+
paragraph: selection.paragraph,
|
|
2657
|
+
charIndex: 0
|
|
2658
|
+
});
|
|
2659
|
+
}, [previewBook, selection.chapter, selection.paragraph]);
|
|
2279
2660
|
function selectParagraph(chapterIndex, paragraphIndex) {
|
|
2280
2661
|
if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
|
|
2281
2662
|
if (textDirty) applyText();
|
|
@@ -2322,39 +2703,49 @@ function MejiroEditor({
|
|
|
2322
2703
|
setRevision((value) => value + 1);
|
|
2323
2704
|
}
|
|
2324
2705
|
async function addImage(file) {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
data
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2706
|
+
await withErrorReporting(async () => {
|
|
2707
|
+
if (!(editor && chapter)) return;
|
|
2708
|
+
const chapterIndex = selection.chapter;
|
|
2709
|
+
const afterBlockId = paragraphBlockId(chapter, selection.paragraph);
|
|
2710
|
+
const data = await file.arrayBuffer();
|
|
2711
|
+
editor.addImage(chapterIndex, {
|
|
2712
|
+
filename: file.name,
|
|
2713
|
+
mediaType: file.type || "application/octet-stream",
|
|
2714
|
+
data,
|
|
2715
|
+
alt: file.name,
|
|
2716
|
+
afterBlockId
|
|
2717
|
+
});
|
|
2718
|
+
setRevision((value) => value + 1);
|
|
2719
|
+
}, reportError);
|
|
2334
2720
|
}
|
|
2335
2721
|
async function exportEpub() {
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2722
|
+
await withErrorReporting(async () => {
|
|
2723
|
+
if (!editor) return;
|
|
2724
|
+
const watermark = exportPolicy?.watermark;
|
|
2725
|
+
const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
|
|
2726
|
+
let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
|
|
2727
|
+
if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
|
|
2728
|
+
const decision = await onBeforeExport?.(buffer);
|
|
2729
|
+
onExport?.(buffer);
|
|
2730
|
+
const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
|
|
2731
|
+
if (!allowDownload) return;
|
|
2732
|
+
const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
|
|
2733
|
+
const a = document.createElement("a");
|
|
2734
|
+
a.href = url;
|
|
2735
|
+
a.download = `${editor.title || "edited"}.epub`;
|
|
2736
|
+
a.click();
|
|
2737
|
+
URL.revokeObjectURL(url);
|
|
2738
|
+
}, reportError);
|
|
2351
2739
|
}
|
|
2352
2740
|
return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
|
|
2353
2741
|
/* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
|
|
2354
2742
|
previewBook ? /* @__PURE__ */ jsx14(
|
|
2355
2743
|
MejiroReader,
|
|
2356
2744
|
{
|
|
2745
|
+
ref: readerRef,
|
|
2357
2746
|
epub: previewBook,
|
|
2747
|
+
chapter: selection.chapter,
|
|
2748
|
+
onChapterChange: (index) => selectParagraph(index, 0),
|
|
2358
2749
|
fonts,
|
|
2359
2750
|
subtitle: messages.editorPreviewSubtitle,
|
|
2360
2751
|
chapterNavMode: "panel",
|
|
@@ -2472,7 +2863,7 @@ function paragraphBlockId(chapter, paragraphIndex) {
|
|
|
2472
2863
|
|
|
2473
2864
|
// src/MejiroManuscriptEditor.tsx
|
|
2474
2865
|
import { EpubProject } from "@libraz/mejiro/epub";
|
|
2475
|
-
import { useCallback as useCallback9, useEffect as
|
|
2866
|
+
import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef14, useState as useState12 } from "react";
|
|
2476
2867
|
|
|
2477
2868
|
// src/MejiroNotationHighlighter.tsx
|
|
2478
2869
|
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
@@ -2480,8 +2871,8 @@ import {
|
|
|
2480
2871
|
forwardRef as forwardRef2,
|
|
2481
2872
|
useImperativeHandle as useImperativeHandle2,
|
|
2482
2873
|
useMemo as useMemo6,
|
|
2483
|
-
useRef as
|
|
2484
|
-
useState as
|
|
2874
|
+
useRef as useRef12,
|
|
2875
|
+
useState as useState10
|
|
2485
2876
|
} from "react";
|
|
2486
2877
|
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2487
2878
|
var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
|
|
@@ -2497,11 +2888,11 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
2497
2888
|
onCompositionEnd,
|
|
2498
2889
|
...rest
|
|
2499
2890
|
}, ref) {
|
|
2500
|
-
const textareaRef =
|
|
2501
|
-
const overlayRef =
|
|
2891
|
+
const textareaRef = useRef12(null);
|
|
2892
|
+
const overlayRef = useRef12(null);
|
|
2502
2893
|
useImperativeHandle2(ref, () => textareaRef.current, []);
|
|
2503
|
-
const [isComposing, setIsComposing] =
|
|
2504
|
-
const lastSegmentsRef =
|
|
2894
|
+
const [isComposing, setIsComposing] = useState10(false);
|
|
2895
|
+
const lastSegmentsRef = useRef12(null);
|
|
2505
2896
|
const segments = useMemo6(() => {
|
|
2506
2897
|
if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
|
|
2507
2898
|
const next = buildSegments(value, dialect);
|
|
@@ -2590,10 +2981,11 @@ function buildSegments(text, dialect) {
|
|
|
2590
2981
|
}
|
|
2591
2982
|
|
|
2592
2983
|
// src/useManuscriptDraft.ts
|
|
2593
|
-
import { useCallback as useCallback8, useEffect as
|
|
2984
|
+
import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
|
|
2594
2985
|
var DEFAULT_DELAY = 800;
|
|
2595
|
-
function defaultChapter(
|
|
2596
|
-
|
|
2986
|
+
function defaultChapter(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
|
|
2987
|
+
const index = existing.length;
|
|
2988
|
+
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
2597
2989
|
}
|
|
2598
2990
|
function useManuscriptDraft(options = {}) {
|
|
2599
2991
|
const {
|
|
@@ -2604,28 +2996,30 @@ function useManuscriptDraft(options = {}) {
|
|
|
2604
2996
|
defaultChapterTitle,
|
|
2605
2997
|
defaultChapterBody
|
|
2606
2998
|
} = options;
|
|
2607
|
-
const titleForRef =
|
|
2999
|
+
const titleForRef = useRef13(defaultChapterTitle);
|
|
2608
3000
|
titleForRef.current = defaultChapterTitle;
|
|
2609
|
-
const bodyForRef =
|
|
3001
|
+
const bodyForRef = useRef13(defaultChapterBody);
|
|
2610
3002
|
bodyForRef.current = defaultChapterBody;
|
|
2611
|
-
const [chapters, setChaptersState] =
|
|
2612
|
-
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter(
|
|
3003
|
+
const [chapters, setChaptersState] = useState11(
|
|
3004
|
+
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
|
|
2613
3005
|
);
|
|
2614
|
-
const [selected, setSelectedState] =
|
|
2615
|
-
const [autosaveError, setAutosaveError] =
|
|
2616
|
-
const saveRef =
|
|
3006
|
+
const [selected, setSelectedState] = useState11(0);
|
|
3007
|
+
const [autosaveError, setAutosaveError] = useState11(null);
|
|
3008
|
+
const saveRef = useRef13(onAutosave);
|
|
2617
3009
|
saveRef.current = onAutosave;
|
|
2618
|
-
const payloadRef =
|
|
3010
|
+
const payloadRef = useRef13(autosavePayload);
|
|
2619
3011
|
payloadRef.current = autosavePayload;
|
|
2620
|
-
const chaptersRef =
|
|
3012
|
+
const chaptersRef = useRef13(chapters);
|
|
2621
3013
|
chaptersRef.current = chapters;
|
|
2622
|
-
const selectedRef =
|
|
3014
|
+
const selectedRef = useRef13(selected);
|
|
2623
3015
|
selectedRef.current = selected;
|
|
2624
|
-
const
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
const
|
|
2628
|
-
const
|
|
3016
|
+
const trackerRef = useRef13(null);
|
|
3017
|
+
if (!trackerRef.current)
|
|
3018
|
+
trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
|
|
3019
|
+
const dirtyRef = useRef13(false);
|
|
3020
|
+
const mountedRef = useRef13(true);
|
|
3021
|
+
const revisionRef = useRef13(0);
|
|
3022
|
+
const inFlightRevisionRef = useRef13(-1);
|
|
2629
3023
|
const flushAutosave = useCallback8(() => {
|
|
2630
3024
|
const callback = saveRef.current;
|
|
2631
3025
|
if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
|
|
@@ -2633,38 +3027,36 @@ function useManuscriptDraft(options = {}) {
|
|
|
2633
3027
|
}
|
|
2634
3028
|
const revision = revisionRef.current;
|
|
2635
3029
|
inFlightRevisionRef.current = revision;
|
|
2636
|
-
const
|
|
3030
|
+
const plain = snapshotChapters(chaptersRef.current);
|
|
3031
|
+
const payload = payloadRef.current ? payloadRef.current(plain) : plain;
|
|
2637
3032
|
void Promise.resolve(callback(payload)).then(() => {
|
|
2638
3033
|
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
2639
3034
|
}).catch((err) => {
|
|
2640
3035
|
if (!mountedRef.current) return;
|
|
2641
|
-
setAutosaveError(
|
|
3036
|
+
setAutosaveError(toError(err));
|
|
2642
3037
|
}).finally(() => {
|
|
2643
3038
|
if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
|
|
2644
3039
|
});
|
|
2645
3040
|
}, []);
|
|
2646
|
-
|
|
2647
|
-
if (
|
|
2648
|
-
|
|
2649
|
-
|
|
3041
|
+
useEffect10(() => {
|
|
3042
|
+
if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
|
|
3043
|
+
dirtyRef.current = true;
|
|
3044
|
+
revisionRef.current += 1;
|
|
3045
|
+
setAutosaveError(null);
|
|
2650
3046
|
}
|
|
2651
|
-
if (!saveRef.current) return void 0;
|
|
2652
|
-
dirtyRef.current = true;
|
|
2653
|
-
revisionRef.current += 1;
|
|
2654
|
-
setAutosaveError(null);
|
|
3047
|
+
if (!(saveRef.current && dirtyRef.current)) return void 0;
|
|
2655
3048
|
const timer = setTimeout(() => {
|
|
2656
3049
|
flushAutosave();
|
|
2657
3050
|
}, autosaveDelay);
|
|
2658
3051
|
return () => clearTimeout(timer);
|
|
2659
3052
|
}, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
|
|
2660
|
-
|
|
3053
|
+
useEffect10(() => {
|
|
2661
3054
|
mountedRef.current = true;
|
|
2662
|
-
const
|
|
2663
|
-
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
3055
|
+
const detachPageHide = flushOnPageHide(flushAutosave);
|
|
2664
3056
|
return () => {
|
|
2665
3057
|
flushAutosave();
|
|
2666
3058
|
mountedRef.current = false;
|
|
2667
|
-
|
|
3059
|
+
detachPageHide();
|
|
2668
3060
|
};
|
|
2669
3061
|
}, [flushAutosave]);
|
|
2670
3062
|
const commit = useCallback8((nextChapters, nextSelected) => {
|
|
@@ -2680,7 +3072,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
2680
3072
|
}, []);
|
|
2681
3073
|
const setChapters = useCallback8(
|
|
2682
3074
|
(next) => {
|
|
2683
|
-
const normalized = next.length ? next : [defaultChapter(
|
|
3075
|
+
const normalized = next.length ? next : [defaultChapter([], titleForRef.current, bodyForRef.current)];
|
|
2684
3076
|
const prev = selectedRef.current;
|
|
2685
3077
|
const selectedId = chaptersRef.current[prev]?.id;
|
|
2686
3078
|
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
@@ -2701,7 +3093,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
2701
3093
|
const addChapter = useCallback8(
|
|
2702
3094
|
(chapter = {}) => {
|
|
2703
3095
|
const current = chaptersRef.current;
|
|
2704
|
-
const generated = defaultChapter(current
|
|
3096
|
+
const generated = defaultChapter(current, titleForRef.current, bodyForRef.current);
|
|
2705
3097
|
const next = [
|
|
2706
3098
|
...current,
|
|
2707
3099
|
{
|
|
@@ -2815,24 +3207,24 @@ function MejiroManuscriptEditor({
|
|
|
2815
3207
|
panelSide = "right"
|
|
2816
3208
|
}) {
|
|
2817
3209
|
const messages = useI18n();
|
|
2818
|
-
const coverInputRef =
|
|
2819
|
-
const bodyTextareaRef =
|
|
3210
|
+
const coverInputRef = useRef14(null);
|
|
3211
|
+
const bodyTextareaRef = useRef14(null);
|
|
2820
3212
|
const titleControlled = onTitleChange !== void 0;
|
|
2821
3213
|
const authorControlled = onAuthorChange !== void 0;
|
|
2822
3214
|
const coverControlled = onCoverChange !== void 0;
|
|
2823
|
-
const [titleState, setTitleState] =
|
|
2824
|
-
const [authorState, setAuthorState] =
|
|
2825
|
-
const [coverState, setCoverState] =
|
|
3215
|
+
const [titleState, setTitleState] = useState12(titleProp ?? messages.manuscriptDefaultTitle);
|
|
3216
|
+
const [authorState, setAuthorState] = useState12(authorProp ?? "");
|
|
3217
|
+
const [coverState, setCoverState] = useState12(coverProp ?? null);
|
|
2826
3218
|
const title = titleControlled ? titleProp ?? "" : titleState;
|
|
2827
3219
|
const author = authorControlled ? authorProp ?? "" : authorState;
|
|
2828
3220
|
const cover = coverControlled ? coverProp ?? null : coverState;
|
|
2829
|
-
|
|
3221
|
+
useEffect11(() => {
|
|
2830
3222
|
if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
|
|
2831
3223
|
}, [titleControlled, titleProp]);
|
|
2832
|
-
|
|
3224
|
+
useEffect11(() => {
|
|
2833
3225
|
if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
|
|
2834
3226
|
}, [authorControlled, authorProp]);
|
|
2835
|
-
|
|
3227
|
+
useEffect11(() => {
|
|
2836
3228
|
if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
|
|
2837
3229
|
}, [coverControlled, coverProp]);
|
|
2838
3230
|
const setTitle = useCallback9(
|
|
@@ -2874,8 +3266,8 @@ function MejiroManuscriptEditor({
|
|
|
2874
3266
|
reorderChapters
|
|
2875
3267
|
} = draft;
|
|
2876
3268
|
const current = chapters[selected] ?? chapters[0];
|
|
2877
|
-
const [draggingIndex, setDraggingIndex] =
|
|
2878
|
-
const [exportError, setExportError] =
|
|
3269
|
+
const [draggingIndex, setDraggingIndex] = useState12(null);
|
|
3270
|
+
const [exportError, setExportError] = useState12(null);
|
|
2879
3271
|
function wrapSelection(open, close) {
|
|
2880
3272
|
const el = bodyTextareaRef.current;
|
|
2881
3273
|
if (!el) return;
|
|
@@ -2893,57 +3285,69 @@ function MejiroManuscriptEditor({
|
|
|
2893
3285
|
bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
|
|
2894
3286
|
});
|
|
2895
3287
|
}
|
|
3288
|
+
const [identifier] = useState12(() => `urn:uuid:${crypto.randomUUID()}`);
|
|
2896
3289
|
const exportEpub = useCallback9(async () => {
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
if (cover) {
|
|
2908
|
-
project.setCover({
|
|
2909
|
-
href: coverAssetHref(cover),
|
|
2910
|
-
mediaType: cover.type || void 0,
|
|
2911
|
-
data: await cover.arrayBuffer()
|
|
3290
|
+
await withErrorReporting(
|
|
3291
|
+
async () => {
|
|
3292
|
+
const project = EpubProject.fromManuscript({
|
|
3293
|
+
metadata: { title, author: author || void 0, identifier },
|
|
3294
|
+
dialect,
|
|
3295
|
+
chapters: chapters.map((chapter) => ({
|
|
3296
|
+
id: chapter.id,
|
|
3297
|
+
title: chapter.title || messages.untitled,
|
|
3298
|
+
body: chapter.body
|
|
3299
|
+
}))
|
|
2912
3300
|
});
|
|
3301
|
+
if (cover) {
|
|
3302
|
+
project.setCover({
|
|
3303
|
+
href: coverAssetHref(cover),
|
|
3304
|
+
mediaType: cover.type || void 0,
|
|
3305
|
+
data: await cover.arrayBuffer()
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
3309
|
+
setExportError(null);
|
|
3310
|
+
onExport?.(buffer);
|
|
3311
|
+
downloadEpub(buffer, title);
|
|
3312
|
+
},
|
|
3313
|
+
(error) => {
|
|
3314
|
+
setExportError(error);
|
|
3315
|
+
onError?.(error);
|
|
2913
3316
|
}
|
|
2914
|
-
|
|
2915
|
-
setExportError(null);
|
|
2916
|
-
onExport?.(buffer);
|
|
2917
|
-
downloadEpub(buffer, title);
|
|
2918
|
-
} catch (cause) {
|
|
2919
|
-
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2920
|
-
setExportError(error);
|
|
2921
|
-
onError?.(error);
|
|
2922
|
-
}
|
|
3317
|
+
);
|
|
2923
3318
|
}, [
|
|
2924
3319
|
assetResolver,
|
|
2925
3320
|
author,
|
|
2926
3321
|
chapters,
|
|
2927
3322
|
cover,
|
|
2928
3323
|
dialect,
|
|
3324
|
+
identifier,
|
|
2929
3325
|
messages.untitled,
|
|
2930
3326
|
onError,
|
|
2931
3327
|
onExport,
|
|
2932
3328
|
title
|
|
2933
3329
|
]);
|
|
3330
|
+
const previewManuscript = useMemo7(
|
|
3331
|
+
() => chapters.map((chapter) => ({
|
|
3332
|
+
id: chapter.id,
|
|
3333
|
+
title: chapter.title || messages.untitled,
|
|
3334
|
+
body: chapter.body
|
|
3335
|
+
})),
|
|
3336
|
+
[chapters, messages.untitled]
|
|
3337
|
+
);
|
|
2934
3338
|
return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", "data-panel-side": panelSide, children: [
|
|
2935
3339
|
/* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
|
|
2936
3340
|
MejiroReader,
|
|
2937
3341
|
{
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3342
|
+
...mergeDefined(
|
|
3343
|
+
{
|
|
3344
|
+
subtitle: messages.manuscriptPreviewSubtitle,
|
|
3345
|
+
chapterNavMode: "panel",
|
|
3346
|
+
enableSurfaceTap: false
|
|
3347
|
+
},
|
|
3348
|
+
previewProps ?? {}
|
|
3349
|
+
),
|
|
3350
|
+
manuscript: previewManuscript,
|
|
2947
3351
|
dialect,
|
|
2948
3352
|
fonts,
|
|
2949
3353
|
chapter: selected,
|
|
@@ -3097,12 +3501,15 @@ function MejiroShelf({
|
|
|
3097
3501
|
}
|
|
3098
3502
|
|
|
3099
3503
|
// src/MejiroToc.tsx
|
|
3100
|
-
import { useMemo as
|
|
3504
|
+
import { useMemo as useMemo8, useState as useState13 } from "react";
|
|
3101
3505
|
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3102
3506
|
function buildEntries(epub, messages) {
|
|
3103
3507
|
return epub.chapters.map((ch, i) => {
|
|
3104
3508
|
const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
|
|
3105
|
-
const headings = ch.paragraphs.
|
|
3509
|
+
const headings = ch.paragraphs.flatMap((p, paragraph) => {
|
|
3510
|
+
const text = p.text.trim();
|
|
3511
|
+
return p.headingLevel && text && text !== title ? [{ paragraph, text }] : [];
|
|
3512
|
+
});
|
|
3106
3513
|
return { index: i, title, headings };
|
|
3107
3514
|
});
|
|
3108
3515
|
}
|
|
@@ -3117,13 +3524,13 @@ function MejiroToc({
|
|
|
3117
3524
|
const messages = useI18n();
|
|
3118
3525
|
const resolvedTitle = title ?? messages.tocTitle;
|
|
3119
3526
|
const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
|
|
3120
|
-
const [query, setQuery] =
|
|
3121
|
-
const entries =
|
|
3122
|
-
const filtered =
|
|
3527
|
+
const [query, setQuery] = useState13("");
|
|
3528
|
+
const entries = useMemo8(() => buildEntries(epub, messages), [epub, messages]);
|
|
3529
|
+
const filtered = useMemo8(() => {
|
|
3123
3530
|
if (!query) return entries;
|
|
3124
3531
|
const needle = query.toLowerCase();
|
|
3125
3532
|
return entries.filter(
|
|
3126
|
-
(e) => e.title.toLowerCase().includes(needle) || e.headings.some((h) => h.toLowerCase().includes(needle))
|
|
3533
|
+
(e) => e.title.toLowerCase().includes(needle) || e.headings.some((h) => h.text.toLowerCase().includes(needle))
|
|
3127
3534
|
);
|
|
3128
3535
|
}, [entries, query]);
|
|
3129
3536
|
const activeIndex = currentAnchor?.chapter ?? -1;
|
|
@@ -3156,7 +3563,7 @@ function MejiroToc({
|
|
|
3156
3563
|
]
|
|
3157
3564
|
}
|
|
3158
3565
|
),
|
|
3159
|
-
entry.headings.length > 0 && /* @__PURE__ */ jsx18("ul", { className: "mejiro-toc-subheads", children: entry.headings.slice(0, 5).map((h) => /* @__PURE__ */ jsx18("li", { className: "mejiro-toc-subhead", children: h }, h)) })
|
|
3566
|
+
entry.headings.length > 0 && /* @__PURE__ */ jsx18("ul", { className: "mejiro-toc-subheads", children: entry.headings.slice(0, 5).map((h) => /* @__PURE__ */ jsx18("li", { className: "mejiro-toc-subhead", children: h.text }, h.paragraph)) })
|
|
3160
3567
|
] }, entry.index)) }),
|
|
3161
3568
|
filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
|
|
3162
3569
|
] });
|
|
@@ -3169,80 +3576,44 @@ import {
|
|
|
3169
3576
|
serializeAnnotations,
|
|
3170
3577
|
sortAnnotations
|
|
3171
3578
|
} from "@libraz/mejiro";
|
|
3172
|
-
import { useCallback as useCallback10, useEffect as
|
|
3173
|
-
function
|
|
3174
|
-
|
|
3175
|
-
const w = globalThis.localStorage;
|
|
3176
|
-
return w ?? null;
|
|
3579
|
+
import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef15, useState as useState14 } from "react";
|
|
3580
|
+
function parseSorted(raw) {
|
|
3581
|
+
return sortAnnotations(parseAnnotations(raw));
|
|
3177
3582
|
}
|
|
3178
3583
|
function useAnnotations(options) {
|
|
3179
3584
|
const { key, throttleMs = 250 } = options;
|
|
3180
3585
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
3181
|
-
const [annotations, setAnnotations] =
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
} catch {
|
|
3186
|
-
return [];
|
|
3187
|
-
}
|
|
3188
|
-
});
|
|
3189
|
-
const storageRef = useRef14(storage);
|
|
3586
|
+
const [annotations, setAnnotations] = useState14(
|
|
3587
|
+
() => readStorage(storage, key, parseSorted, [])
|
|
3588
|
+
);
|
|
3589
|
+
const storageRef = useRef15(storage);
|
|
3190
3590
|
storageRef.current = storage;
|
|
3191
|
-
const annotationsRef =
|
|
3591
|
+
const annotationsRef = useRef15(annotations);
|
|
3192
3592
|
const applyNext = useCallback10((next) => {
|
|
3193
3593
|
annotationsRef.current = next;
|
|
3194
3594
|
setAnnotations(next);
|
|
3195
3595
|
}, []);
|
|
3196
|
-
const
|
|
3197
|
-
|
|
3198
|
-
const
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
if (!write) return;
|
|
3206
|
-
try {
|
|
3207
|
-
write();
|
|
3208
|
-
} catch {
|
|
3209
|
-
}
|
|
3210
|
-
}, []);
|
|
3211
|
-
const cancelPending = useCallback10(() => {
|
|
3212
|
-
if (timerRef.current) {
|
|
3213
|
-
clearTimeout(timerRef.current);
|
|
3214
|
-
timerRef.current = null;
|
|
3215
|
-
}
|
|
3216
|
-
pendingWriteRef.current = null;
|
|
3217
|
-
}, []);
|
|
3218
|
-
useEffect11(() => {
|
|
3219
|
-
const currentStorage = storageRef.current;
|
|
3220
|
-
if (currentStorage) {
|
|
3221
|
-
try {
|
|
3222
|
-
applyNext(sortAnnotations(parseAnnotations(currentStorage.getItem(key))));
|
|
3223
|
-
} catch {
|
|
3224
|
-
applyNext([]);
|
|
3225
|
-
}
|
|
3226
|
-
} else {
|
|
3227
|
-
applyNext([]);
|
|
3228
|
-
}
|
|
3229
|
-
return flushPending;
|
|
3230
|
-
}, [key, applyNext, flushPending]);
|
|
3231
|
-
const onChangeRef = useRef14(options.onChange);
|
|
3596
|
+
const pendingRef = useRef15(null);
|
|
3597
|
+
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
3598
|
+
const pending = pendingRef.current;
|
|
3599
|
+
useEffect12(() => flushOnPageHide(pending.flush), [pending]);
|
|
3600
|
+
useEffect12(() => {
|
|
3601
|
+
applyNext(readStorage(storageRef.current, key, parseSorted, []));
|
|
3602
|
+
return pending.flush;
|
|
3603
|
+
}, [key, applyNext, pending]);
|
|
3604
|
+
const onChangeRef = useRef15(options.onChange);
|
|
3232
3605
|
onChangeRef.current = options.onChange;
|
|
3233
3606
|
const commit = useCallback10(
|
|
3234
3607
|
(next) => {
|
|
3235
3608
|
const currentStorage = storageRef.current;
|
|
3236
3609
|
if (currentStorage) {
|
|
3237
|
-
|
|
3238
|
-
pendingWriteRef.current = () => {
|
|
3610
|
+
pending.schedule(() => {
|
|
3239
3611
|
currentStorage.setItem(key, serializeAnnotations(next));
|
|
3240
|
-
};
|
|
3241
|
-
timerRef.current = setTimeout(flushPending, throttleMs);
|
|
3612
|
+
}, throttleMs);
|
|
3242
3613
|
}
|
|
3243
3614
|
onChangeRef.current?.(next);
|
|
3244
3615
|
},
|
|
3245
|
-
[key, throttleMs,
|
|
3616
|
+
[key, throttleMs, pending]
|
|
3246
3617
|
);
|
|
3247
3618
|
const add = useCallback10(
|
|
3248
3619
|
(input) => {
|
|
@@ -3274,7 +3645,7 @@ function useAnnotations(options) {
|
|
|
3274
3645
|
const next = annotationsRef.current.map((annotation) => {
|
|
3275
3646
|
if (annotation.id !== id) return annotation;
|
|
3276
3647
|
changed = true;
|
|
3277
|
-
return { ...annotation,
|
|
3648
|
+
return { ...mergeDefined(annotation, patch), id };
|
|
3278
3649
|
});
|
|
3279
3650
|
if (!changed) return;
|
|
3280
3651
|
const sorted = sortAnnotations(next);
|
|
@@ -3285,16 +3656,10 @@ function useAnnotations(options) {
|
|
|
3285
3656
|
);
|
|
3286
3657
|
const clear = useCallback10(() => {
|
|
3287
3658
|
applyNext([]);
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
cancelPending();
|
|
3291
|
-
try {
|
|
3292
|
-
currentStorage.removeItem(key);
|
|
3293
|
-
} catch {
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3659
|
+
pending.cancel();
|
|
3660
|
+
removeStorage(storageRef.current, key);
|
|
3296
3661
|
onChangeRef.current?.([]);
|
|
3297
|
-
}, [key,
|
|
3662
|
+
}, [key, pending, applyNext]);
|
|
3298
3663
|
return { annotations, add, remove, update, clear };
|
|
3299
3664
|
}
|
|
3300
3665
|
|
|
@@ -3304,26 +3669,32 @@ import {
|
|
|
3304
3669
|
cloneEditableEpubBook as cloneEditableEpubBook2,
|
|
3305
3670
|
EditableEpub as EditableEpub2
|
|
3306
3671
|
} from "@libraz/mejiro/epub";
|
|
3307
|
-
import { useCallback as useCallback11, useEffect as
|
|
3672
|
+
import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
|
|
3308
3673
|
function useEditableEpub(options = {}) {
|
|
3309
|
-
const [editor, setEditor] =
|
|
3310
|
-
const [loading, setLoading] =
|
|
3311
|
-
const [exporting, setExporting] =
|
|
3312
|
-
const [error, setError] =
|
|
3313
|
-
const [revision, setRevision] =
|
|
3314
|
-
const [selection, setSelectionState] =
|
|
3674
|
+
const [editor, setEditor] = useState15(null);
|
|
3675
|
+
const [loading, setLoading] = useState15(false);
|
|
3676
|
+
const [exporting, setExporting] = useState15(false);
|
|
3677
|
+
const [error, setError] = useState15(null);
|
|
3678
|
+
const [revision, setRevision] = useState15(0);
|
|
3679
|
+
const [selection, setSelectionState] = useState15({
|
|
3315
3680
|
chapter: 0,
|
|
3316
3681
|
paragraph: 0
|
|
3317
3682
|
});
|
|
3318
|
-
const requestIdRef =
|
|
3319
|
-
const onLoadRef =
|
|
3320
|
-
const onErrorRef =
|
|
3321
|
-
const onExportRef =
|
|
3322
|
-
const limitsRef =
|
|
3683
|
+
const requestIdRef = useRef16(0);
|
|
3684
|
+
const onLoadRef = useRef16(options.onLoad);
|
|
3685
|
+
const onErrorRef = useRef16(options.onError);
|
|
3686
|
+
const onExportRef = useRef16(options.onExport);
|
|
3687
|
+
const limitsRef = useRef16(options.limits);
|
|
3688
|
+
const assetResolverRef = useRef16(options.assetResolver);
|
|
3323
3689
|
onLoadRef.current = options.onLoad;
|
|
3324
3690
|
onErrorRef.current = options.onError;
|
|
3325
3691
|
onExportRef.current = options.onExport;
|
|
3326
3692
|
limitsRef.current = options.limits;
|
|
3693
|
+
assetResolverRef.current = options.assetResolver;
|
|
3694
|
+
const editorRef = useRef16(editor);
|
|
3695
|
+
editorRef.current = editor;
|
|
3696
|
+
const selectionRef = useRef16(selection);
|
|
3697
|
+
selectionRef.current = selection;
|
|
3327
3698
|
const loadBufferWithRequest = useCallback11(
|
|
3328
3699
|
async (buffer, requestId) => {
|
|
3329
3700
|
setLoading(true);
|
|
@@ -3331,14 +3702,17 @@ function useEditableEpub(options = {}) {
|
|
|
3331
3702
|
try {
|
|
3332
3703
|
const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
|
|
3333
3704
|
if (requestId !== requestIdRef.current) return null;
|
|
3705
|
+
const firstParagraph = { chapter: 0, paragraph: 0 };
|
|
3706
|
+
editorRef.current = next;
|
|
3707
|
+
selectionRef.current = firstParagraph;
|
|
3334
3708
|
setEditor(next);
|
|
3335
|
-
setSelectionState(
|
|
3709
|
+
setSelectionState(firstParagraph);
|
|
3336
3710
|
setRevision((value) => value + 1);
|
|
3337
3711
|
onLoadRef.current?.(next);
|
|
3338
3712
|
return next;
|
|
3339
3713
|
} catch (err) {
|
|
3340
3714
|
if (requestId === requestIdRef.current) {
|
|
3341
|
-
const nextError =
|
|
3715
|
+
const nextError = toError(err);
|
|
3342
3716
|
setError(nextError);
|
|
3343
3717
|
onErrorRef.current?.(nextError);
|
|
3344
3718
|
}
|
|
@@ -3362,10 +3736,10 @@ function useEditableEpub(options = {}) {
|
|
|
3362
3736
|
setLoading(true);
|
|
3363
3737
|
setError(null);
|
|
3364
3738
|
try {
|
|
3365
|
-
return await loadBufferWithRequest(await file.
|
|
3739
|
+
return await loadBufferWithRequest(await readEpubFile(file, limitsRef.current), requestId);
|
|
3366
3740
|
} catch (err) {
|
|
3367
3741
|
if (requestId === requestIdRef.current) {
|
|
3368
|
-
const nextError =
|
|
3742
|
+
const nextError = toError(err);
|
|
3369
3743
|
setError(nextError);
|
|
3370
3744
|
onErrorRef.current?.(nextError);
|
|
3371
3745
|
setLoading(false);
|
|
@@ -3381,12 +3755,10 @@ function useEditableEpub(options = {}) {
|
|
|
3381
3755
|
setLoading(true);
|
|
3382
3756
|
setError(null);
|
|
3383
3757
|
try {
|
|
3384
|
-
|
|
3385
|
-
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
3386
|
-
return await loadBufferWithRequest(await res.arrayBuffer(), requestId);
|
|
3758
|
+
return await loadBufferWithRequest(await fetchEpubBuffer(url), requestId);
|
|
3387
3759
|
} catch (err) {
|
|
3388
3760
|
if (requestId === requestIdRef.current) {
|
|
3389
|
-
const nextError =
|
|
3761
|
+
const nextError = toError(err);
|
|
3390
3762
|
setError(nextError);
|
|
3391
3763
|
onErrorRef.current?.(nextError);
|
|
3392
3764
|
setLoading(false);
|
|
@@ -3396,74 +3768,74 @@ function useEditableEpub(options = {}) {
|
|
|
3396
3768
|
},
|
|
3397
3769
|
[loadBufferWithRequest]
|
|
3398
3770
|
);
|
|
3399
|
-
|
|
3771
|
+
useEffect13(() => {
|
|
3400
3772
|
if (options.defaultUrl) void loadUrl(options.defaultUrl);
|
|
3401
3773
|
}, [options.defaultUrl, loadUrl]);
|
|
3402
3774
|
const book = editor?.book ?? null;
|
|
3403
3775
|
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3404
|
-
const previewBook =
|
|
3776
|
+
const previewBook = useMemo9(() => {
|
|
3405
3777
|
void revision;
|
|
3406
3778
|
return book ? cloneEditableEpubBook2(book) : null;
|
|
3407
3779
|
}, [book, revision]);
|
|
3408
3780
|
const history = editor?.history ?? null;
|
|
3409
|
-
const setSelection = useCallback11(
|
|
3410
|
-
(nextSelection)
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
);
|
|
3781
|
+
const setSelection = useCallback11((nextSelection) => {
|
|
3782
|
+
const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
|
|
3783
|
+
selectionRef.current = next;
|
|
3784
|
+
setSelectionState(next);
|
|
3785
|
+
}, []);
|
|
3415
3786
|
const updateParagraph = useCallback11(
|
|
3416
3787
|
(text, inlineAnnotations) => {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
});
|
|
3422
|
-
setRevision((value) => value + 1);
|
|
3423
|
-
},
|
|
3424
|
-
[editor, selection.chapter, selection.paragraph]
|
|
3425
|
-
);
|
|
3426
|
-
const setInlineAnnotations = useCallback11(
|
|
3427
|
-
(inlineAnnotations) => {
|
|
3428
|
-
if (!editor) return;
|
|
3429
|
-
editor.setInlineAnnotations(selection.chapter, selection.paragraph, inlineAnnotations);
|
|
3430
|
-
setRevision((value) => value + 1);
|
|
3431
|
-
},
|
|
3432
|
-
[editor, selection.chapter, selection.paragraph]
|
|
3433
|
-
);
|
|
3434
|
-
const addImage = useCallback11(
|
|
3435
|
-
(image) => {
|
|
3436
|
-
if (!editor) return;
|
|
3437
|
-
editor.addImage(selection.chapter, image);
|
|
3788
|
+
const current = editorRef.current;
|
|
3789
|
+
if (!current) return;
|
|
3790
|
+
const { chapter, paragraph } = selectionRef.current;
|
|
3791
|
+
current.updateParagraph(chapter, paragraph, { text, inlineAnnotations });
|
|
3438
3792
|
setRevision((value) => value + 1);
|
|
3439
3793
|
},
|
|
3440
|
-
[
|
|
3794
|
+
[]
|
|
3441
3795
|
);
|
|
3796
|
+
const setInlineAnnotations = useCallback11((inlineAnnotations) => {
|
|
3797
|
+
const current = editorRef.current;
|
|
3798
|
+
if (!current) return;
|
|
3799
|
+
const { chapter, paragraph } = selectionRef.current;
|
|
3800
|
+
current.setInlineAnnotations(chapter, paragraph, inlineAnnotations);
|
|
3801
|
+
setRevision((value) => value + 1);
|
|
3802
|
+
}, []);
|
|
3803
|
+
const addImage = useCallback11((image) => {
|
|
3804
|
+
const current = editorRef.current;
|
|
3805
|
+
if (!current) return;
|
|
3806
|
+
current.addImage(selectionRef.current.chapter, image);
|
|
3807
|
+
setRevision((value) => value + 1);
|
|
3808
|
+
}, []);
|
|
3442
3809
|
const undo = useCallback11(() => {
|
|
3443
|
-
|
|
3444
|
-
|
|
3810
|
+
const current = editorRef.current;
|
|
3811
|
+
if (!current) return false;
|
|
3812
|
+
const changed = current.undo();
|
|
3445
3813
|
if (changed) setRevision((value) => value + 1);
|
|
3446
3814
|
return changed;
|
|
3447
|
-
}, [
|
|
3815
|
+
}, []);
|
|
3448
3816
|
const redo = useCallback11(() => {
|
|
3449
|
-
|
|
3450
|
-
|
|
3817
|
+
const current = editorRef.current;
|
|
3818
|
+
if (!current) return false;
|
|
3819
|
+
const changed = current.redo();
|
|
3451
3820
|
if (changed) setRevision((value) => value + 1);
|
|
3452
3821
|
return changed;
|
|
3453
|
-
}, [
|
|
3822
|
+
}, []);
|
|
3454
3823
|
const exportEpub = useCallback11(
|
|
3455
|
-
async (
|
|
3456
|
-
|
|
3824
|
+
async (exportOptions) => {
|
|
3825
|
+
const current = editorRef.current;
|
|
3826
|
+
if (!current) return null;
|
|
3457
3827
|
setExporting(true);
|
|
3458
3828
|
try {
|
|
3459
|
-
const buffer = await
|
|
3829
|
+
const buffer = await current.export(
|
|
3830
|
+
withAssetResolver(exportOptions, assetResolverRef.current)
|
|
3831
|
+
);
|
|
3460
3832
|
onExportRef.current?.(buffer);
|
|
3461
3833
|
return buffer;
|
|
3462
3834
|
} finally {
|
|
3463
3835
|
setExporting(false);
|
|
3464
3836
|
}
|
|
3465
3837
|
},
|
|
3466
|
-
[
|
|
3838
|
+
[]
|
|
3467
3839
|
);
|
|
3468
3840
|
return {
|
|
3469
3841
|
editor,
|
|
@@ -3488,62 +3860,79 @@ function useEditableEpub(options = {}) {
|
|
|
3488
3860
|
exportEpub
|
|
3489
3861
|
};
|
|
3490
3862
|
}
|
|
3863
|
+
function withAssetResolver(options, resolver) {
|
|
3864
|
+
if (!resolver || options?.assetResolver) return options;
|
|
3865
|
+
return { ...options, assetResolver: resolver };
|
|
3866
|
+
}
|
|
3491
3867
|
|
|
3492
3868
|
// src/useEpubProject.ts
|
|
3493
3869
|
import {
|
|
3494
3870
|
EpubProject as EpubProject2,
|
|
3495
3871
|
parseEpub as parseEpub2
|
|
3496
3872
|
} from "@libraz/mejiro/epub";
|
|
3497
|
-
import { useCallback as useCallback12, useEffect as
|
|
3873
|
+
import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo10, useRef as useRef17, useState as useState16 } from "react";
|
|
3498
3874
|
function useEpubProject(options = {}) {
|
|
3499
3875
|
const defaultTitle = options.defaultChapterTitle;
|
|
3500
3876
|
const defaultBody = options.defaultChapterBody;
|
|
3501
|
-
const [metadata, setMetadataState] =
|
|
3502
|
-
|
|
3503
|
-
language: "ja",
|
|
3504
|
-
...options.metadata
|
|
3505
|
-
});
|
|
3506
|
-
const [chapters, setChaptersState] = useState15(
|
|
3507
|
-
options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
|
|
3877
|
+
const [metadata, setMetadataState] = useState16(
|
|
3878
|
+
() => initialMetadata(options.metadata)
|
|
3508
3879
|
);
|
|
3509
|
-
const [
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
const [
|
|
3513
|
-
const [
|
|
3514
|
-
const [
|
|
3515
|
-
const
|
|
3516
|
-
const
|
|
3517
|
-
const
|
|
3880
|
+
const [chapters, setChaptersState] = useState16(
|
|
3881
|
+
() => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
|
|
3882
|
+
);
|
|
3883
|
+
const [selectedChapter, setSelectedChapterState] = useState16(0);
|
|
3884
|
+
const [cover, setCoverState] = useState16(options.cover ?? null);
|
|
3885
|
+
const [assets, setAssetsState] = useState16(options.assets ?? []);
|
|
3886
|
+
const [previewBook, setPreviewBook] = useState16(null);
|
|
3887
|
+
const [previewError, setPreviewError] = useState16(null);
|
|
3888
|
+
const [previewing, setPreviewing] = useState16(false);
|
|
3889
|
+
const previewRequestIdRef = useRef17(0);
|
|
3890
|
+
const onPreviewRef = useRef17(options.onPreview);
|
|
3891
|
+
const onExportRef = useRef17(options.onExport);
|
|
3518
3892
|
onPreviewRef.current = options.onPreview;
|
|
3519
3893
|
onExportRef.current = options.onExport;
|
|
3894
|
+
const assetResolverRef = useRef17(options.assetResolver);
|
|
3895
|
+
assetResolverRef.current = options.assetResolver;
|
|
3896
|
+
const metadataRef = useRef17(metadata);
|
|
3897
|
+
metadataRef.current = metadata;
|
|
3898
|
+
const chaptersRef = useRef17(chapters);
|
|
3899
|
+
chaptersRef.current = chapters;
|
|
3900
|
+
const selectedRef = useRef17(selectedChapter);
|
|
3901
|
+
selectedRef.current = selectedChapter;
|
|
3902
|
+
const coverRef = useRef17(cover);
|
|
3903
|
+
coverRef.current = cover;
|
|
3904
|
+
const assetsRef = useRef17(assets);
|
|
3905
|
+
assetsRef.current = assets;
|
|
3520
3906
|
const buildProject = useCallback12(() => {
|
|
3907
|
+
const currentCover = coverRef.current;
|
|
3521
3908
|
const project = EpubProject2.fromManuscript({
|
|
3522
|
-
metadata,
|
|
3909
|
+
metadata: metadataRef.current,
|
|
3523
3910
|
includeTitlePage: false,
|
|
3524
3911
|
includeTitleInFirstChapter: true,
|
|
3525
|
-
chapters:
|
|
3912
|
+
chapters: chaptersRef.current.map((chapter) => ({
|
|
3526
3913
|
id: chapter.id,
|
|
3527
3914
|
title: chapter.title || "Untitled",
|
|
3528
3915
|
body: chapter.body
|
|
3529
3916
|
})),
|
|
3530
|
-
...
|
|
3917
|
+
...currentCover ? { cover: currentCover } : {}
|
|
3531
3918
|
});
|
|
3532
|
-
for (const asset of
|
|
3919
|
+
for (const asset of assetsRef.current) project.addAsset(asset);
|
|
3533
3920
|
return project;
|
|
3534
|
-
}, [
|
|
3535
|
-
const
|
|
3536
|
-
|
|
3537
|
-
|
|
3921
|
+
}, []);
|
|
3922
|
+
const exportOptions = useCallback12((signal) => {
|
|
3923
|
+
const resolver = assetResolverRef.current;
|
|
3924
|
+
return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
|
|
3925
|
+
}, []);
|
|
3926
|
+
useEffect14(() => {
|
|
3538
3927
|
const requestId = ++previewRequestIdRef.current;
|
|
3928
|
+
const controller = new AbortController();
|
|
3539
3929
|
let cancelled = false;
|
|
3540
3930
|
setPreviewing(true);
|
|
3541
3931
|
const timer = setTimeout(() => {
|
|
3542
3932
|
void (async () => {
|
|
3543
3933
|
try {
|
|
3544
|
-
const resolver = assetResolverRef.current;
|
|
3545
3934
|
const book = await parseEpub2(
|
|
3546
|
-
await buildProject().export(
|
|
3935
|
+
await buildProject().export(exportOptions(controller.signal))
|
|
3547
3936
|
);
|
|
3548
3937
|
if (cancelled || requestId !== previewRequestIdRef.current) return;
|
|
3549
3938
|
setPreviewBook(book);
|
|
@@ -3551,7 +3940,7 @@ function useEpubProject(options = {}) {
|
|
|
3551
3940
|
onPreviewRef.current?.(book);
|
|
3552
3941
|
} catch (err) {
|
|
3553
3942
|
if (!cancelled && requestId === previewRequestIdRef.current) {
|
|
3554
|
-
setPreviewError(
|
|
3943
|
+
setPreviewError(toError(err));
|
|
3555
3944
|
}
|
|
3556
3945
|
} finally {
|
|
3557
3946
|
if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
|
|
@@ -3562,99 +3951,107 @@ function useEpubProject(options = {}) {
|
|
|
3562
3951
|
cancelled = true;
|
|
3563
3952
|
previewRequestIdRef.current++;
|
|
3564
3953
|
clearTimeout(timer);
|
|
3954
|
+
controller.abort();
|
|
3565
3955
|
};
|
|
3566
|
-
}, [buildProject, options.debounceMs]);
|
|
3956
|
+
}, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
|
|
3567
3957
|
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
3568
3958
|
const setMetadata = useCallback12((patch) => {
|
|
3569
|
-
|
|
3959
|
+
const next = { ...metadataRef.current, ...patch };
|
|
3960
|
+
metadataRef.current = next;
|
|
3961
|
+
setMetadataState(next);
|
|
3962
|
+
}, []);
|
|
3963
|
+
const commit = useCallback12((nextChapters, nextSelected) => {
|
|
3964
|
+
chaptersRef.current = nextChapters;
|
|
3965
|
+
selectedRef.current = nextSelected;
|
|
3966
|
+
setChaptersState(nextChapters);
|
|
3967
|
+
setSelectedChapterState(nextSelected);
|
|
3570
3968
|
}, []);
|
|
3571
3969
|
const setChapters = useCallback12(
|
|
3572
3970
|
(next) => {
|
|
3573
|
-
const normalized = next.length ? next : [defaultChapter3(
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3971
|
+
const normalized = next.length ? next : [defaultChapter3([], defaultTitle, defaultBody)];
|
|
3972
|
+
const prev = selectedRef.current;
|
|
3973
|
+
const selectedId = chaptersRef.current[prev]?.id;
|
|
3974
|
+
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
3975
|
+
commit(
|
|
3976
|
+
normalized,
|
|
3977
|
+
nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
|
|
3978
|
+
);
|
|
3580
3979
|
},
|
|
3581
|
-
[
|
|
3980
|
+
[commit, defaultBody, defaultTitle]
|
|
3582
3981
|
);
|
|
3583
3982
|
const setCover = useCallback12((asset) => {
|
|
3983
|
+
coverRef.current = asset;
|
|
3584
3984
|
setCoverState(asset);
|
|
3585
3985
|
}, []);
|
|
3586
3986
|
const setAssets = useCallback12((next) => {
|
|
3987
|
+
assetsRef.current = next;
|
|
3587
3988
|
setAssetsState(next);
|
|
3588
3989
|
}, []);
|
|
3589
|
-
const setSelectedChapter = useCallback12(
|
|
3590
|
-
(index
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
);
|
|
3990
|
+
const setSelectedChapter = useCallback12((index) => {
|
|
3991
|
+
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
3992
|
+
selectedRef.current = next;
|
|
3993
|
+
setSelectedChapterState(next);
|
|
3994
|
+
}, []);
|
|
3595
3995
|
const patchChapter = useCallback12((index, patch) => {
|
|
3596
|
-
|
|
3597
|
-
(
|
|
3598
|
-
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3599
|
-
)
|
|
3996
|
+
const next = chaptersRef.current.map(
|
|
3997
|
+
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3600
3998
|
);
|
|
3999
|
+
chaptersRef.current = next;
|
|
4000
|
+
setChaptersState(next);
|
|
3601
4001
|
}, []);
|
|
3602
4002
|
const addChapter = useCallback12(
|
|
3603
4003
|
(chapter = {}) => {
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
return next;
|
|
3616
|
-
});
|
|
4004
|
+
const current = chaptersRef.current;
|
|
4005
|
+
const generated = defaultChapter3(current, defaultTitle, defaultBody);
|
|
4006
|
+
const next = [
|
|
4007
|
+
...current,
|
|
4008
|
+
{
|
|
4009
|
+
id: chapter.id ?? generated.id,
|
|
4010
|
+
title: chapter.title ?? generated.title,
|
|
4011
|
+
body: chapter.body ?? generated.body
|
|
4012
|
+
}
|
|
4013
|
+
];
|
|
4014
|
+
commit(next, next.length - 1);
|
|
3617
4015
|
},
|
|
3618
|
-
[defaultBody, defaultTitle]
|
|
4016
|
+
[commit, defaultBody, defaultTitle]
|
|
3619
4017
|
);
|
|
3620
4018
|
const removeChapter = useCallback12(
|
|
3621
|
-
(index =
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
});
|
|
4019
|
+
(index = selectedRef.current) => {
|
|
4020
|
+
const current = chaptersRef.current;
|
|
4021
|
+
if (current.length <= 1) return;
|
|
4022
|
+
const next = current.filter((_, chapterIndex) => chapterIndex !== index);
|
|
4023
|
+
const prev = selectedRef.current;
|
|
4024
|
+
let nextSelected;
|
|
4025
|
+
if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
|
|
4026
|
+
else if (index < prev) nextSelected = prev - 1;
|
|
4027
|
+
else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
|
|
4028
|
+
commit(next, nextSelected);
|
|
3632
4029
|
},
|
|
3633
|
-
[
|
|
4030
|
+
[commit]
|
|
3634
4031
|
);
|
|
3635
|
-
const reorderChapters = useCallback12(
|
|
3636
|
-
|
|
3637
|
-
|
|
4032
|
+
const reorderChapters = useCallback12(
|
|
4033
|
+
(from, to) => {
|
|
4034
|
+
const current = chaptersRef.current;
|
|
4035
|
+
if (from < 0 || from >= current.length) return;
|
|
3638
4036
|
const next = [...current];
|
|
3639
4037
|
const [moved] = next.splice(from, 1);
|
|
3640
4038
|
const target = Math.max(0, Math.min(next.length, to));
|
|
3641
4039
|
next.splice(target, 0, moved);
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
4040
|
+
const prev = selectedRef.current;
|
|
4041
|
+
let nextSelected = prev;
|
|
4042
|
+
if (prev === from) nextSelected = target;
|
|
4043
|
+
else if (from < prev && target >= prev) nextSelected = prev - 1;
|
|
4044
|
+
else if (from > prev && target <= prev) nextSelected = prev + 1;
|
|
4045
|
+
commit(next, nextSelected);
|
|
4046
|
+
},
|
|
4047
|
+
[commit]
|
|
4048
|
+
);
|
|
3651
4049
|
const exportEpub = useCallback12(async () => {
|
|
3652
|
-
const
|
|
3653
|
-
const buffer = await buildProject().export(resolver ? { assetResolver: resolver } : void 0);
|
|
4050
|
+
const buffer = await buildProject().export(exportOptions());
|
|
3654
4051
|
onExportRef.current?.(buffer);
|
|
3655
4052
|
return buffer;
|
|
3656
|
-
}, [buildProject]);
|
|
3657
|
-
return
|
|
4053
|
+
}, [buildProject, exportOptions]);
|
|
4054
|
+
return useMemo10(
|
|
3658
4055
|
() => ({
|
|
3659
4056
|
metadata,
|
|
3660
4057
|
chapters,
|
|
@@ -3701,21 +4098,26 @@ function useEpubProject(options = {}) {
|
|
|
3701
4098
|
]
|
|
3702
4099
|
);
|
|
3703
4100
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
}
|
|
4101
|
+
function initialMetadata(patch = {}) {
|
|
4102
|
+
const metadata = mergeDefined(
|
|
4103
|
+
{ title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
|
|
4104
|
+
patch
|
|
4105
|
+
);
|
|
4106
|
+
if (!metadata.identifier?.trim()) metadata.identifier = `urn:uuid:${crypto.randomUUID()}`;
|
|
4107
|
+
return metadata;
|
|
4108
|
+
}
|
|
4109
|
+
function defaultChapter3(existing, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
|
|
4110
|
+
const index = existing.length;
|
|
4111
|
+
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
3710
4112
|
}
|
|
3711
4113
|
|
|
3712
4114
|
// src/useImageOverlay.ts
|
|
3713
|
-
import { createOverlayDragSession as createOverlayDragSession2 } from "@libraz/mejiro/browser";
|
|
4115
|
+
import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
|
|
3714
4116
|
import {
|
|
3715
4117
|
useCallback as useCallback13,
|
|
3716
|
-
useEffect as
|
|
3717
|
-
useRef as
|
|
3718
|
-
useState as
|
|
4118
|
+
useEffect as useEffect15,
|
|
4119
|
+
useRef as useRef18,
|
|
4120
|
+
useState as useState17
|
|
3719
4121
|
} from "react";
|
|
3720
4122
|
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
3721
4123
|
const defW = options?.defaultWidth ?? 120;
|
|
@@ -3723,17 +4125,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3723
4125
|
const defX = options?.defaultX ?? 80;
|
|
3724
4126
|
const defY = options?.defaultY ?? 100;
|
|
3725
4127
|
const margin = options?.margin;
|
|
3726
|
-
const [imageRect, setImageRect] =
|
|
3727
|
-
const rectRef =
|
|
4128
|
+
const [imageRect, setImageRect] = useState17(null);
|
|
4129
|
+
const rectRef = useRef18(imageRect);
|
|
3728
4130
|
rectRef.current = imageRect;
|
|
3729
|
-
const layoutRef =
|
|
4131
|
+
const layoutRef = useRef18(layout);
|
|
3730
4132
|
layoutRef.current = layout;
|
|
3731
|
-
const spreadRef =
|
|
4133
|
+
const spreadRef = useRef18(spreadIdx);
|
|
3732
4134
|
spreadRef.current = spreadIdx;
|
|
3733
|
-
const onUpdateRef =
|
|
4135
|
+
const onUpdateRef = useRef18(onUpdate);
|
|
3734
4136
|
onUpdateRef.current = onUpdate;
|
|
3735
|
-
const activeDragCleanupsRef =
|
|
3736
|
-
|
|
4137
|
+
const activeDragCleanupsRef = useRef18(/* @__PURE__ */ new Set());
|
|
4138
|
+
useEffect15(
|
|
3737
4139
|
() => () => {
|
|
3738
4140
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
3739
4141
|
activeDragCleanupsRef.current.clear();
|
|
@@ -3750,20 +4152,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3750
4152
|
},
|
|
3751
4153
|
[margin]
|
|
3752
4154
|
);
|
|
3753
|
-
const syncedRef =
|
|
3754
|
-
|
|
3755
|
-
spreadIdx
|
|
3756
|
-
});
|
|
3757
|
-
useEffect14(() => {
|
|
4155
|
+
const syncedRef = useRef18({ layout, spreadIdx, margin });
|
|
4156
|
+
useEffect15(() => {
|
|
3758
4157
|
const prev = syncedRef.current;
|
|
3759
|
-
syncedRef.current = { layout, spreadIdx };
|
|
3760
|
-
if (prev.layout === layout && prev.spreadIdx === spreadIdx) return;
|
|
4158
|
+
syncedRef.current = { layout, spreadIdx, margin };
|
|
4159
|
+
if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
|
|
3761
4160
|
if (rectRef.current === null) return;
|
|
3762
4161
|
if (prev.layout && prev.spreadIdx !== spreadIdx) {
|
|
3763
4162
|
prev.layout.syncImages(prev.spreadIdx, void 0);
|
|
3764
4163
|
}
|
|
3765
4164
|
syncToLayout(rectRef.current);
|
|
3766
|
-
}, [layout, spreadIdx, syncToLayout]);
|
|
4165
|
+
}, [layout, spreadIdx, margin, syncToLayout]);
|
|
3767
4166
|
const toggleImage = useCallback13(() => {
|
|
3768
4167
|
if (rectRef.current) {
|
|
3769
4168
|
setImageRect(null);
|
|
@@ -3783,6 +4182,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3783
4182
|
);
|
|
3784
4183
|
const onOverlayPointerDown = useCallback13(
|
|
3785
4184
|
(e) => {
|
|
4185
|
+
if (!isPrimaryPointerPress3(e)) return;
|
|
3786
4186
|
e.preventDefault();
|
|
3787
4187
|
const current = rectRef.current;
|
|
3788
4188
|
if (!current) return;
|
|
@@ -3795,7 +4195,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3795
4195
|
pointerId: e.nativeEvent.pointerId,
|
|
3796
4196
|
captureElement: target,
|
|
3797
4197
|
activeElement: target,
|
|
3798
|
-
dragClass: "dragging",
|
|
4198
|
+
dragClass: "is-dragging",
|
|
3799
4199
|
registry: activeDragCleanupsRef.current,
|
|
3800
4200
|
onChange: applyRect
|
|
3801
4201
|
});
|
|
@@ -3804,6 +4204,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3804
4204
|
);
|
|
3805
4205
|
const onResizePointerDown = useCallback13(
|
|
3806
4206
|
(e) => {
|
|
4207
|
+
if (!isPrimaryPointerPress3(e)) return;
|
|
3807
4208
|
e.preventDefault();
|
|
3808
4209
|
e.stopPropagation();
|
|
3809
4210
|
const current = rectRef.current;
|
|
@@ -3817,7 +4218,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3817
4218
|
pointerId: e.nativeEvent.pointerId,
|
|
3818
4219
|
captureElement: target,
|
|
3819
4220
|
activeElement: target.parentElement,
|
|
3820
|
-
dragClass: "dragging",
|
|
4221
|
+
dragClass: "is-dragging",
|
|
3821
4222
|
registry: activeDragCleanupsRef.current,
|
|
3822
4223
|
onChange: applyRect
|
|
3823
4224
|
});
|
|
@@ -3834,69 +4235,61 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3834
4235
|
}
|
|
3835
4236
|
|
|
3836
4237
|
// src/useLibrary.ts
|
|
3837
|
-
import { useCallback as useCallback14, useEffect as
|
|
4238
|
+
import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo11, useState as useState18 } from "react";
|
|
4239
|
+
function resolveCurrentId(volumes, currentId, preferredId) {
|
|
4240
|
+
if (volumes.length === 0) return null;
|
|
4241
|
+
if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
|
|
4242
|
+
if (currentId != null && volumes.some((v) => v.id === currentId)) return currentId;
|
|
4243
|
+
return volumes[0]?.id ?? null;
|
|
4244
|
+
}
|
|
3838
4245
|
function useLibrary(options) {
|
|
3839
4246
|
const { volumes, initialVolumeId, onChange } = options;
|
|
3840
|
-
const
|
|
3841
|
-
|
|
3842
|
-
volumes.findIndex((v) => v.id === initialVolumeId)
|
|
3843
|
-
) : 0;
|
|
3844
|
-
const [currentId, setCurrentId] = useState17(
|
|
3845
|
-
volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
|
|
4247
|
+
const [currentId, setCurrentId] = useState18(
|
|
4248
|
+
() => resolveCurrentId(volumes, null, initialVolumeId)
|
|
3846
4249
|
);
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
const found = volumes.findIndex((v) => v.id === currentId);
|
|
3861
|
-
return found >= 0 ? found : 0;
|
|
3862
|
-
}, [currentId, volumes]);
|
|
3863
|
-
const current = useMemo10(
|
|
4250
|
+
const [navigated, setNavigated] = useState18(false);
|
|
4251
|
+
const resolvedId = resolveCurrentId(volumes, currentId, navigated ? void 0 : initialVolumeId);
|
|
4252
|
+
useEffect16(() => {
|
|
4253
|
+
if (resolvedId === currentId) return;
|
|
4254
|
+
setCurrentId(resolvedId);
|
|
4255
|
+
const volume = volumes.find((v) => v.id === resolvedId);
|
|
4256
|
+
if (volume) onChange?.(volume);
|
|
4257
|
+
}, [currentId, resolvedId, volumes, onChange]);
|
|
4258
|
+
const index = useMemo11(
|
|
4259
|
+
() => resolvedId == null ? -1 : volumes.findIndex((v) => v.id === resolvedId),
|
|
4260
|
+
[resolvedId, volumes]
|
|
4261
|
+
);
|
|
4262
|
+
const current = useMemo11(
|
|
3864
4263
|
() => index >= 0 && index < volumes.length ? volumes[index] : null,
|
|
3865
4264
|
[index, volumes]
|
|
3866
4265
|
);
|
|
3867
|
-
const
|
|
4266
|
+
const activate = useCallback14(
|
|
3868
4267
|
(i) => {
|
|
3869
4268
|
const v = volumes[i];
|
|
3870
|
-
if (v)
|
|
4269
|
+
if (!v) return;
|
|
4270
|
+
setNavigated(true);
|
|
4271
|
+
setCurrentId(v.id);
|
|
4272
|
+
onChange?.(v);
|
|
3871
4273
|
},
|
|
3872
4274
|
[volumes, onChange]
|
|
3873
4275
|
);
|
|
3874
4276
|
const next = useCallback14(() => {
|
|
3875
4277
|
if (index < 0) return;
|
|
3876
4278
|
const ni = Math.min(volumes.length - 1, index + 1);
|
|
3877
|
-
if (ni !== index)
|
|
3878
|
-
|
|
3879
|
-
fire(ni);
|
|
3880
|
-
}
|
|
3881
|
-
}, [index, volumes, fire]);
|
|
4279
|
+
if (ni !== index) activate(ni);
|
|
4280
|
+
}, [index, activate, volumes.length]);
|
|
3882
4281
|
const prev = useCallback14(() => {
|
|
3883
4282
|
if (index < 0) return;
|
|
3884
4283
|
const ni = Math.max(0, index - 1);
|
|
3885
|
-
if (ni !== index)
|
|
3886
|
-
|
|
3887
|
-
fire(ni);
|
|
3888
|
-
}
|
|
3889
|
-
}, [index, volumes, fire]);
|
|
4284
|
+
if (ni !== index) activate(ni);
|
|
4285
|
+
}, [index, activate]);
|
|
3890
4286
|
const goTo = useCallback14(
|
|
3891
4287
|
(id) => {
|
|
3892
4288
|
const ni = volumes.findIndex((v) => v.id === id);
|
|
3893
4289
|
if (ni < 0) return;
|
|
3894
|
-
if (ni !== index)
|
|
3895
|
-
setCurrentId(id);
|
|
3896
|
-
fire(ni);
|
|
3897
|
-
}
|
|
4290
|
+
if (ni !== index) activate(ni);
|
|
3898
4291
|
},
|
|
3899
|
-
[index, volumes,
|
|
4292
|
+
[index, volumes, activate]
|
|
3900
4293
|
);
|
|
3901
4294
|
return { list: volumes, current, currentIndex: index, next, prev, goTo };
|
|
3902
4295
|
}
|
|
@@ -3904,25 +4297,25 @@ function useLibrary(options) {
|
|
|
3904
4297
|
// src/useManuscriptLayout.ts
|
|
3905
4298
|
import {
|
|
3906
4299
|
useCallback as useCallback15,
|
|
3907
|
-
useEffect as
|
|
4300
|
+
useEffect as useEffect17,
|
|
3908
4301
|
useLayoutEffect as useLayoutEffect5,
|
|
3909
|
-
useRef as
|
|
3910
|
-
useState as
|
|
4302
|
+
useRef as useRef19,
|
|
4303
|
+
useState as useState19
|
|
3911
4304
|
} from "react";
|
|
3912
4305
|
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
3913
4306
|
const dialect = options.dialect ?? "mejiro";
|
|
3914
4307
|
const enableResize = options.enableResize ?? true;
|
|
3915
4308
|
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
3916
|
-
const [layout, setLayout] =
|
|
3917
|
-
const [pageWidth, setPageWidth] =
|
|
3918
|
-
const [pageHeight, setPageHeight] =
|
|
3919
|
-
const [contentHeight, setContentHeight] =
|
|
3920
|
-
const [elapsedMs, setElapsedMs] =
|
|
3921
|
-
const requestIdRef =
|
|
3922
|
-
const layoutRef =
|
|
4309
|
+
const [layout, setLayout] = useState19(null);
|
|
4310
|
+
const [pageWidth, setPageWidth] = useState19(0);
|
|
4311
|
+
const [pageHeight, setPageHeight] = useState19(0);
|
|
4312
|
+
const [contentHeight, setContentHeight] = useState19(0);
|
|
4313
|
+
const [elapsedMs, setElapsedMs] = useState19(0);
|
|
4314
|
+
const requestIdRef = useRef19(0);
|
|
4315
|
+
const layoutRef = useRef19(null);
|
|
3923
4316
|
layoutRef.current = layout;
|
|
3924
|
-
const pendingRestore =
|
|
3925
|
-
const optionsRef =
|
|
4317
|
+
const pendingRestore = useRef19(null);
|
|
4318
|
+
const optionsRef = useRef19(options);
|
|
3926
4319
|
optionsRef.current = options;
|
|
3927
4320
|
const recompute = useCallback15(
|
|
3928
4321
|
async (opts = {}) => {
|
|
@@ -3962,13 +4355,20 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
|
3962
4355
|
setContentHeight(0);
|
|
3963
4356
|
setElapsedMs(0);
|
|
3964
4357
|
}, [recompute]);
|
|
3965
|
-
|
|
4358
|
+
useEffect17(() => {
|
|
3966
4359
|
void recompute();
|
|
3967
4360
|
return () => {
|
|
3968
4361
|
requestIdRef.current++;
|
|
3969
4362
|
};
|
|
3970
4363
|
}, [recompute]);
|
|
3971
|
-
|
|
4364
|
+
useLayoutEffect5(() => {
|
|
4365
|
+
const anchor = pendingRestore.current;
|
|
4366
|
+
const restore = optionsRef.current.restorePosition;
|
|
4367
|
+
if (!(layout && anchor && restore)) return;
|
|
4368
|
+
pendingRestore.current = null;
|
|
4369
|
+
restore(layout, anchor);
|
|
4370
|
+
}, [layout]);
|
|
4371
|
+
useEffect17(() => {
|
|
3972
4372
|
if (!enableResize) return;
|
|
3973
4373
|
const el = surface.current;
|
|
3974
4374
|
if (!el) return;
|
|
@@ -4018,89 +4418,44 @@ import {
|
|
|
4018
4418
|
parseReadingPosition,
|
|
4019
4419
|
serializeReadingPosition
|
|
4020
4420
|
} from "@libraz/mejiro";
|
|
4021
|
-
import { useCallback as useCallback16, useEffect as
|
|
4022
|
-
function resolveDefaultStorage2() {
|
|
4023
|
-
if (typeof globalThis === "undefined") return null;
|
|
4024
|
-
const w = globalThis.localStorage;
|
|
4025
|
-
return w ?? null;
|
|
4026
|
-
}
|
|
4421
|
+
import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef20, useState as useState20 } from "react";
|
|
4027
4422
|
function useReadingPosition(options) {
|
|
4028
4423
|
const { key, throttleMs = 250 } = options;
|
|
4029
|
-
const storage = options.storage ??
|
|
4030
|
-
const [position, setPosition] =
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
} catch {
|
|
4035
|
-
return null;
|
|
4036
|
-
}
|
|
4037
|
-
});
|
|
4038
|
-
const storageRef = useRef19(storage);
|
|
4424
|
+
const storage = options.storage ?? resolveDefaultStorage();
|
|
4425
|
+
const [position, setPosition] = useState20(
|
|
4426
|
+
() => readStorage(storage, key, parseReadingPosition, null)
|
|
4427
|
+
);
|
|
4428
|
+
const storageRef = useRef20(storage);
|
|
4039
4429
|
storageRef.current = storage;
|
|
4040
|
-
const
|
|
4041
|
-
|
|
4042
|
-
const
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
if (!write) return;
|
|
4050
|
-
try {
|
|
4051
|
-
write();
|
|
4052
|
-
} catch {
|
|
4053
|
-
}
|
|
4054
|
-
}, []);
|
|
4055
|
-
const cancelPending = useCallback16(() => {
|
|
4056
|
-
if (timerRef.current) {
|
|
4057
|
-
clearTimeout(timerRef.current);
|
|
4058
|
-
timerRef.current = null;
|
|
4059
|
-
}
|
|
4060
|
-
pendingWriteRef.current = null;
|
|
4061
|
-
}, []);
|
|
4062
|
-
useEffect17(() => {
|
|
4063
|
-
const currentStorage = storageRef.current;
|
|
4064
|
-
if (currentStorage) {
|
|
4065
|
-
try {
|
|
4066
|
-
setPosition(parseReadingPosition(currentStorage.getItem(key)));
|
|
4067
|
-
} catch {
|
|
4068
|
-
setPosition(null);
|
|
4069
|
-
}
|
|
4070
|
-
} else {
|
|
4071
|
-
setPosition(null);
|
|
4072
|
-
}
|
|
4073
|
-
return flushPending;
|
|
4074
|
-
}, [key, flushPending]);
|
|
4075
|
-
const onChangeRef = useRef19(options.onChange);
|
|
4430
|
+
const pendingRef = useRef20(null);
|
|
4431
|
+
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
4432
|
+
const pending = pendingRef.current;
|
|
4433
|
+
useEffect18(() => flushOnPageHide(pending.flush), [pending]);
|
|
4434
|
+
useEffect18(() => {
|
|
4435
|
+
setPosition(readStorage(storageRef.current, key, parseReadingPosition, null));
|
|
4436
|
+
return pending.flush;
|
|
4437
|
+
}, [key, pending]);
|
|
4438
|
+
const onChangeRef = useRef20(options.onChange);
|
|
4076
4439
|
onChangeRef.current = options.onChange;
|
|
4077
4440
|
const save = useCallback16(
|
|
4078
4441
|
(next) => {
|
|
4079
4442
|
setPosition(next);
|
|
4080
4443
|
const currentStorage = storageRef.current;
|
|
4081
4444
|
if (currentStorage) {
|
|
4082
|
-
|
|
4083
|
-
pendingWriteRef.current = () => {
|
|
4445
|
+
pending.schedule(() => {
|
|
4084
4446
|
currentStorage.setItem(key, serializeReadingPosition(next));
|
|
4085
|
-
};
|
|
4086
|
-
timerRef.current = setTimeout(flushPending, throttleMs);
|
|
4447
|
+
}, throttleMs);
|
|
4087
4448
|
}
|
|
4088
4449
|
onChangeRef.current?.(next);
|
|
4089
4450
|
},
|
|
4090
|
-
[key, throttleMs,
|
|
4451
|
+
[key, throttleMs, pending]
|
|
4091
4452
|
);
|
|
4092
4453
|
const clear = useCallback16(() => {
|
|
4093
4454
|
setPosition(null);
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
if (currentStorage) {
|
|
4097
|
-
try {
|
|
4098
|
-
currentStorage.removeItem(key);
|
|
4099
|
-
} catch {
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4455
|
+
pending.cancel();
|
|
4456
|
+
removeStorage(storageRef.current, key);
|
|
4102
4457
|
onChangeRef.current?.(null);
|
|
4103
|
-
}, [key,
|
|
4458
|
+
}, [key, pending]);
|
|
4104
4459
|
return { position, save, clear };
|
|
4105
4460
|
}
|
|
4106
4461
|
|