@libraz/mejiro-react 0.8.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/README.md +12 -3
- package/dist/index.d.ts +521 -151
- package/dist/index.js +1973 -1443
- 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(
|
|
@@ -114,6 +127,11 @@ function MejiroDropZone({
|
|
|
114
127
|
className: rootClass,
|
|
115
128
|
style,
|
|
116
129
|
onClick: () => inputRef.current?.click(),
|
|
130
|
+
onKeyDown: (e) => {
|
|
131
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
inputRef.current?.click();
|
|
134
|
+
},
|
|
117
135
|
onDragOver: (e) => {
|
|
118
136
|
e.preventDefault();
|
|
119
137
|
setDragover(true);
|
|
@@ -150,8 +168,35 @@ function MejiroDropZone({
|
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
// src/MejiroEditor.tsx
|
|
153
|
-
import {
|
|
154
|
-
|
|
171
|
+
import {
|
|
172
|
+
cloneEditableEpubBook,
|
|
173
|
+
EditableEpub,
|
|
174
|
+
exportEditableEpub
|
|
175
|
+
} from "@libraz/mejiro/epub";
|
|
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
|
+
}
|
|
155
200
|
|
|
156
201
|
// src/MejiroReader.tsx
|
|
157
202
|
import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
|
|
@@ -160,12 +205,12 @@ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
|
|
|
160
205
|
import {
|
|
161
206
|
forwardRef,
|
|
162
207
|
useCallback as useCallback6,
|
|
163
|
-
useEffect as
|
|
208
|
+
useEffect as useEffect8,
|
|
164
209
|
useImperativeHandle,
|
|
165
210
|
useLayoutEffect as useLayoutEffect4,
|
|
166
211
|
useMemo as useMemo4,
|
|
167
|
-
useRef as
|
|
168
|
-
useState as
|
|
212
|
+
useRef as useRef10,
|
|
213
|
+
useState as useState8
|
|
169
214
|
} from "react";
|
|
170
215
|
|
|
171
216
|
// src/MejiroPageIndicator.tsx
|
|
@@ -181,26 +226,74 @@ function MejiroPageIndicator({ current, total }) {
|
|
|
181
226
|
// src/MejiroScrollView.tsx
|
|
182
227
|
import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
|
|
183
228
|
import {
|
|
184
|
-
useEffect,
|
|
229
|
+
useEffect as useEffect2,
|
|
185
230
|
useLayoutEffect,
|
|
186
|
-
useMemo as
|
|
187
|
-
useRef as
|
|
231
|
+
useMemo as useMemo3,
|
|
232
|
+
useRef as useRef3
|
|
188
233
|
} from "react";
|
|
189
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
|
+
|
|
190
279
|
// src/MejiroPageView.tsx
|
|
191
280
|
import { normalizeFontFamily } from "@libraz/mejiro/browser";
|
|
281
|
+
import { paragraphClassName as paragraphClassName2 } from "@libraz/mejiro/render";
|
|
282
|
+
|
|
283
|
+
// src/MejiroPage.tsx
|
|
284
|
+
import { paragraphClassName } from "@libraz/mejiro/render";
|
|
192
285
|
|
|
193
286
|
// src/renderInlineNode.tsx
|
|
194
287
|
import {
|
|
195
288
|
segmentToInlineNode
|
|
196
289
|
} from "@libraz/mejiro/render";
|
|
197
290
|
import { createElement, Fragment as Fragment2 } from "react";
|
|
198
|
-
import { jsx as
|
|
291
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
199
292
|
function renderSegment(segment, key) {
|
|
200
293
|
return renderInlineNode(segmentToInlineNode(segment), key);
|
|
201
294
|
}
|
|
202
295
|
function renderInlineNode(node, key) {
|
|
203
|
-
if (node.type === "text") return /* @__PURE__ */
|
|
296
|
+
if (node.type === "text") return /* @__PURE__ */ jsx5(Fragment2, { children: node.text }, key);
|
|
204
297
|
return createElement(
|
|
205
298
|
node.tag,
|
|
206
299
|
{
|
|
@@ -214,35 +307,36 @@ function renderInlineNode(node, key) {
|
|
|
214
307
|
}
|
|
215
308
|
|
|
216
309
|
// src/MejiroPage.tsx
|
|
217
|
-
import { jsx as
|
|
310
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
218
311
|
function renderLine(line, lineIndex) {
|
|
219
312
|
const nodes = [];
|
|
220
313
|
if (lineIndex > 0) {
|
|
221
|
-
nodes.push(/* @__PURE__ */
|
|
314
|
+
nodes.push(/* @__PURE__ */ jsx6("br", {}, `br-${lineIndex}`));
|
|
222
315
|
}
|
|
223
316
|
for (let i = 0; i < line.segments.length; i++) {
|
|
224
317
|
nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
|
|
225
318
|
}
|
|
226
319
|
return nodes;
|
|
227
320
|
}
|
|
321
|
+
function paragraphScaleStyle(scale) {
|
|
322
|
+
return scale == null ? void 0 : { "--mejiro-paragraph-scale": scale };
|
|
323
|
+
}
|
|
228
324
|
function MejiroPage({ page, className, style }) {
|
|
229
325
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
230
|
-
return /* @__PURE__ */
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
paraClass += " mejiro-paragraph--heading";
|
|
236
|
-
}
|
|
326
|
+
return /* @__PURE__ */ jsx6("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
|
|
327
|
+
const paraClass = paragraphClassName(
|
|
328
|
+
paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
|
|
329
|
+
paragraph.headingLevel
|
|
330
|
+
);
|
|
237
331
|
return (
|
|
238
332
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
239
|
-
/* @__PURE__ */
|
|
333
|
+
/* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
240
334
|
);
|
|
241
335
|
}) });
|
|
242
336
|
}
|
|
243
337
|
|
|
244
338
|
// src/MejiroPageView.tsx
|
|
245
|
-
import { jsx as
|
|
339
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
246
340
|
function MejiroPageView({
|
|
247
341
|
result,
|
|
248
342
|
fontFamily,
|
|
@@ -254,33 +348,307 @@ function MejiroPageView({
|
|
|
254
348
|
if (result.hasImages || slotMode) {
|
|
255
349
|
const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
|
|
256
350
|
const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
|
|
257
|
-
return /* @__PURE__ */
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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: {
|
|
261
403
|
position: "absolute",
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
530
|
};
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
276
557
|
);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
};
|
|
280
647
|
}
|
|
281
648
|
|
|
282
649
|
// src/MejiroScrollView.tsx
|
|
283
|
-
import { jsx as
|
|
650
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
651
|
+
var NO_IMAGES = [];
|
|
284
652
|
function MejiroScrollView({
|
|
285
653
|
layout,
|
|
286
654
|
pageWidth,
|
|
@@ -289,26 +657,52 @@ function MejiroScrollView({
|
|
|
289
657
|
fontFamily,
|
|
290
658
|
fontSize,
|
|
291
659
|
lineSpacing,
|
|
660
|
+
slotMode,
|
|
292
661
|
onVisiblePageChange,
|
|
293
662
|
scrollToPage,
|
|
294
|
-
pageGap = 24
|
|
663
|
+
pageGap = 24,
|
|
664
|
+
selectionRects,
|
|
665
|
+
spreadIdx,
|
|
666
|
+
images = NO_IMAGES,
|
|
667
|
+
onImagePointerDown,
|
|
668
|
+
onImageResizePointerDown,
|
|
669
|
+
onImageClose
|
|
295
670
|
}) {
|
|
296
|
-
const containerRef =
|
|
297
|
-
const pageRefs =
|
|
298
|
-
const onVisiblePageChangeRef =
|
|
671
|
+
const containerRef = useRef3(null);
|
|
672
|
+
const pageRefs = useRef3([]);
|
|
673
|
+
const onVisiblePageChangeRef = useRef3(onVisiblePageChange);
|
|
299
674
|
onVisiblePageChangeRef.current = onVisiblePageChange;
|
|
300
|
-
const programmaticScrollRef =
|
|
301
|
-
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(() => {
|
|
302
688
|
const total = layout.totalPages;
|
|
303
689
|
return Array.from({ length: total }, (_, i) => layout.getPage(i));
|
|
304
|
-
}, [layout]);
|
|
690
|
+
}, [layout, images]);
|
|
305
691
|
const pageCount = pages.length;
|
|
306
|
-
const
|
|
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]);
|
|
307
701
|
const contentStyle = { height: contentHeight };
|
|
308
702
|
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
|
|
309
703
|
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
310
704
|
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
311
|
-
|
|
705
|
+
useEffect2(() => {
|
|
312
706
|
const container = containerRef.current;
|
|
313
707
|
if (!container) return;
|
|
314
708
|
if (pageCount === 0) return;
|
|
@@ -331,10 +725,9 @@ function MejiroScrollView({
|
|
|
331
725
|
}
|
|
332
726
|
}
|
|
333
727
|
if (mostVisibleIdx >= 0) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
);
|
|
728
|
+
const source = programmaticScrollRef.current ? "programmatic" : "user";
|
|
729
|
+
if (source === "user") userPageRef.current = mostVisibleIdx;
|
|
730
|
+
onVisiblePageChangeRef.current?.(mostVisibleIdx, source);
|
|
338
731
|
}
|
|
339
732
|
},
|
|
340
733
|
{ root: container, threshold: [0.25, 0.5, 0.75] }
|
|
@@ -347,8 +740,10 @@ function MejiroScrollView({
|
|
|
347
740
|
useLayoutEffect(() => {
|
|
348
741
|
if (scrollToPage == null) return;
|
|
349
742
|
if (pageCount === 0) return;
|
|
743
|
+
if (scrollToPage === userPageRef.current) return;
|
|
350
744
|
const el = pageRefs.current[scrollToPage];
|
|
351
745
|
if (!(el && containerRef.current)) return;
|
|
746
|
+
userPageRef.current = null;
|
|
352
747
|
programmaticScrollRef.current = true;
|
|
353
748
|
containerRef.current.scrollTo({
|
|
354
749
|
top: el.offsetTop,
|
|
@@ -359,12 +754,12 @@ function MejiroScrollView({
|
|
|
359
754
|
}, 0);
|
|
360
755
|
return () => clearTimeout(timer);
|
|
361
756
|
}, [scrollToPage, pageCount]);
|
|
362
|
-
return /* @__PURE__ */
|
|
757
|
+
return /* @__PURE__ */ jsx9("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx9(
|
|
363
758
|
"div",
|
|
364
759
|
{
|
|
365
760
|
className: "mejiro-reader-scroll-track",
|
|
366
761
|
style: { display: "flex", flexDirection: "column", gap: pageGap },
|
|
367
|
-
children: pages.map((result, i) => /* @__PURE__ */
|
|
762
|
+
children: pages.map((result, i) => /* @__PURE__ */ jsxs5(
|
|
368
763
|
"div",
|
|
369
764
|
{
|
|
370
765
|
ref: (el) => {
|
|
@@ -372,24 +767,58 @@ function MejiroScrollView({
|
|
|
372
767
|
},
|
|
373
768
|
"data-page-idx": i,
|
|
374
769
|
className: "mejiro-reader-page",
|
|
375
|
-
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
|
+
},
|
|
376
776
|
children: [
|
|
377
|
-
/* @__PURE__ */
|
|
378
|
-
/* @__PURE__ */
|
|
379
|
-
/* @__PURE__ */
|
|
380
|
-
/* @__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 })
|
|
381
781
|
] }),
|
|
382
|
-
/* @__PURE__ */
|
|
383
|
-
|
|
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,
|
|
384
804
|
{
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
))
|
|
393
822
|
]
|
|
394
823
|
},
|
|
395
824
|
i
|
|
@@ -400,7 +829,8 @@ function MejiroScrollView({
|
|
|
400
829
|
|
|
401
830
|
// src/MejiroSettingsPanel.tsx
|
|
402
831
|
import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
|
|
403
|
-
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";
|
|
404
834
|
var DEFAULT_FONTS = [
|
|
405
835
|
{ value: "serif", label: "System Serif" },
|
|
406
836
|
{ value: "sans-serif", label: "System Sans" }
|
|
@@ -417,6 +847,48 @@ function parseClampedInput(value, min, max, fallback) {
|
|
|
417
847
|
if (value.trim() === "") return fallback;
|
|
418
848
|
return clampNumber(Number(value), min, max, fallback);
|
|
419
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
|
+
}
|
|
420
892
|
function MejiroSettingsPanel({
|
|
421
893
|
open,
|
|
422
894
|
settings,
|
|
@@ -429,216 +901,127 @@ function MejiroSettingsPanel({
|
|
|
429
901
|
const patch = (next) => onChange({ ...settings, ...next });
|
|
430
902
|
const currentFont = normalizeFontFamily3(settings.fontFamily);
|
|
431
903
|
const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
|
|
432
|
-
return /* @__PURE__ */
|
|
433
|
-
/* @__PURE__ */
|
|
434
|
-
/* @__PURE__ */
|
|
435
|
-
/* @__PURE__ */
|
|
436
|
-
/* @__PURE__ */
|
|
437
|
-
/* @__PURE__ */
|
|
438
|
-
/* @__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(
|
|
439
911
|
"button",
|
|
440
912
|
{
|
|
441
913
|
type: "button",
|
|
442
914
|
className: "mejiro-reader-btn mejiro-reader-btn--icon",
|
|
443
915
|
"aria-label": messages.settingsSizeDown,
|
|
444
|
-
onClick: () => patch({
|
|
916
|
+
onClick: () => patch({
|
|
917
|
+
fontSize: clampNumber(
|
|
918
|
+
settings.fontSize - 1,
|
|
919
|
+
minFontSize,
|
|
920
|
+
maxFontSize,
|
|
921
|
+
minFontSize
|
|
922
|
+
)
|
|
923
|
+
}),
|
|
445
924
|
children: "A\u2212"
|
|
446
925
|
}
|
|
447
926
|
),
|
|
448
|
-
/* @__PURE__ */
|
|
449
|
-
|
|
927
|
+
/* @__PURE__ */ jsx10(
|
|
928
|
+
CommittedNumberInput,
|
|
450
929
|
{
|
|
451
930
|
id: "mejiro-reader-font-size",
|
|
452
|
-
type: "number",
|
|
453
931
|
value: settings.fontSize,
|
|
454
932
|
min: minFontSize,
|
|
455
933
|
max: maxFontSize,
|
|
456
|
-
|
|
457
|
-
fontSize: parseClampedInput(
|
|
458
|
-
e.target.value,
|
|
459
|
-
minFontSize,
|
|
460
|
-
maxFontSize,
|
|
461
|
-
settings.fontSize
|
|
462
|
-
)
|
|
463
|
-
})
|
|
934
|
+
onCommit: (fontSize) => patch({ fontSize })
|
|
464
935
|
}
|
|
465
936
|
),
|
|
466
|
-
/* @__PURE__ */
|
|
937
|
+
/* @__PURE__ */ jsx10(
|
|
467
938
|
"button",
|
|
468
939
|
{
|
|
469
940
|
type: "button",
|
|
470
941
|
className: "mejiro-reader-btn mejiro-reader-btn--icon",
|
|
471
942
|
"aria-label": messages.settingsSizeUp,
|
|
472
|
-
onClick: () => patch({
|
|
943
|
+
onClick: () => patch({
|
|
944
|
+
fontSize: clampNumber(
|
|
945
|
+
settings.fontSize + 1,
|
|
946
|
+
minFontSize,
|
|
947
|
+
maxFontSize,
|
|
948
|
+
minFontSize
|
|
949
|
+
)
|
|
950
|
+
}),
|
|
473
951
|
children: "A+"
|
|
474
952
|
}
|
|
475
953
|
)
|
|
476
954
|
] })
|
|
477
955
|
] }),
|
|
478
|
-
/* @__PURE__ */
|
|
479
|
-
/* @__PURE__ */
|
|
480
|
-
/* @__PURE__ */
|
|
481
|
-
/* @__PURE__ */
|
|
482
|
-
/* @__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(
|
|
483
961
|
"select",
|
|
484
962
|
{
|
|
485
963
|
id: "mejiro-reader-kinsoku",
|
|
486
964
|
value: settings.mode ?? "strict",
|
|
487
965
|
onChange: (e) => patch({ mode: e.target.value }),
|
|
488
966
|
children: [
|
|
489
|
-
/* @__PURE__ */
|
|
490
|
-
/* @__PURE__ */
|
|
967
|
+
/* @__PURE__ */ jsx10("option", { value: "strict", children: messages.settingsStrict }),
|
|
968
|
+
/* @__PURE__ */ jsx10("option", { value: "loose", children: messages.settingsLoose })
|
|
491
969
|
]
|
|
492
970
|
}
|
|
493
971
|
)
|
|
494
972
|
] }),
|
|
495
|
-
/* @__PURE__ */
|
|
496
|
-
/* @__PURE__ */
|
|
497
|
-
/* @__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(
|
|
498
976
|
"select",
|
|
499
977
|
{
|
|
500
978
|
id: "mejiro-reader-hanging",
|
|
501
979
|
value: String(settings.enableHanging ?? true),
|
|
502
980
|
onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
|
|
503
981
|
children: [
|
|
504
|
-
/* @__PURE__ */
|
|
505
|
-
/* @__PURE__ */
|
|
982
|
+
/* @__PURE__ */ jsx10("option", { value: "true", children: messages.toggleOn }),
|
|
983
|
+
/* @__PURE__ */ jsx10("option", { value: "false", children: messages.toggleOff })
|
|
506
984
|
]
|
|
507
985
|
}
|
|
508
986
|
)
|
|
509
987
|
] }),
|
|
510
|
-
/* @__PURE__ */
|
|
511
|
-
/* @__PURE__ */
|
|
512
|
-
/* @__PURE__ */
|
|
513
|
-
|
|
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,
|
|
514
992
|
{
|
|
515
993
|
id: "mejiro-reader-line-spacing",
|
|
516
994
|
className: "mejiro-reader-control--wide",
|
|
517
|
-
type: "number",
|
|
518
995
|
value: settings.lineSpacing ?? 1.8,
|
|
519
|
-
min: 1,
|
|
520
|
-
max: 3,
|
|
521
|
-
step: 0.1,
|
|
522
|
-
|
|
523
|
-
lineSpacing: parseClampedInput(
|
|
524
|
-
e.target.value,
|
|
525
|
-
1,
|
|
526
|
-
3,
|
|
527
|
-
settings.lineSpacing ?? 1.8
|
|
528
|
-
)
|
|
529
|
-
})
|
|
530
|
-
}
|
|
531
|
-
)
|
|
532
|
-
] })
|
|
533
|
-
] })
|
|
534
|
-
] }) }) });
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// src/MejiroSpread.tsx
|
|
538
|
-
import { normalizeFontFamily as normalizeFontFamily4 } from "@libraz/mejiro/browser";
|
|
539
|
-
import {
|
|
540
|
-
useRef as useRef3
|
|
541
|
-
} from "react";
|
|
542
|
-
|
|
543
|
-
// src/MejiroImageOverlay.tsx
|
|
544
|
-
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
545
|
-
function MejiroImageOverlay({
|
|
546
|
-
rect,
|
|
547
|
-
label,
|
|
548
|
-
onOverlayPointerDown,
|
|
549
|
-
onResizePointerDown,
|
|
550
|
-
onClose
|
|
551
|
-
}) {
|
|
552
|
-
const messages = useI18n();
|
|
553
|
-
const resolvedLabel = label ?? messages.imageButton;
|
|
554
|
-
return /* @__PURE__ */ jsxs6(
|
|
555
|
-
"div",
|
|
556
|
-
{
|
|
557
|
-
className: "mejiro-reader-image-overlay",
|
|
558
|
-
style: {
|
|
559
|
-
left: rect.x,
|
|
560
|
-
top: rect.y,
|
|
561
|
-
width: rect.w,
|
|
562
|
-
height: rect.h
|
|
563
|
-
},
|
|
564
|
-
onPointerDown: onOverlayPointerDown,
|
|
565
|
-
children: [
|
|
566
|
-
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-image-overlay-label", children: [
|
|
567
|
-
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-icon" }),
|
|
568
|
-
/* @__PURE__ */ jsx9("span", { children: resolvedLabel })
|
|
569
|
-
] }),
|
|
570
|
-
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
|
|
571
|
-
/* @__PURE__ */ jsx9(
|
|
572
|
-
"button",
|
|
573
|
-
{
|
|
574
|
-
type: "button",
|
|
575
|
-
"aria-label": messages.imageRemoveButton,
|
|
576
|
-
title: messages.imageRemoveButton,
|
|
577
|
-
className: "mejiro-reader-image-overlay-close",
|
|
578
|
-
onPointerDown: (e) => {
|
|
579
|
-
e.stopPropagation();
|
|
580
|
-
e.preventDefault();
|
|
581
|
-
onClose?.();
|
|
582
|
-
},
|
|
583
|
-
onKeyDown: (e) => {
|
|
584
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
585
|
-
e.stopPropagation();
|
|
586
|
-
e.preventDefault();
|
|
587
|
-
onClose?.();
|
|
588
|
-
}
|
|
996
|
+
min: 1,
|
|
997
|
+
max: 3,
|
|
998
|
+
step: 0.1,
|
|
999
|
+
onCommit: (lineSpacing) => patch({ lineSpacing })
|
|
589
1000
|
}
|
|
590
1001
|
)
|
|
591
|
-
]
|
|
592
|
-
}
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// src/MejiroSelectionLayer.tsx
|
|
597
|
-
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
598
|
-
function MejiroSelectionLayer({
|
|
599
|
-
rects,
|
|
600
|
-
side,
|
|
601
|
-
className,
|
|
602
|
-
style,
|
|
603
|
-
rectClassName,
|
|
604
|
-
rectStyle
|
|
605
|
-
}) {
|
|
606
|
-
const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
|
|
607
|
-
const rectClass = rectClassName ?? "mejiro-selection-rect";
|
|
608
|
-
return /* @__PURE__ */ jsx10(
|
|
609
|
-
"div",
|
|
610
|
-
{
|
|
611
|
-
className: wrapperClass,
|
|
612
|
-
style: {
|
|
613
|
-
position: "absolute",
|
|
614
|
-
inset: 0,
|
|
615
|
-
pointerEvents: "none",
|
|
616
|
-
...style
|
|
617
|
-
},
|
|
618
|
-
children: rects.map((r, i) => {
|
|
619
|
-
if (r.side !== side) return null;
|
|
620
|
-
const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
|
|
621
|
-
return /* @__PURE__ */ jsx10(
|
|
622
|
-
"div",
|
|
623
|
-
{
|
|
624
|
-
className: rectClass,
|
|
625
|
-
style: {
|
|
626
|
-
position: "absolute",
|
|
627
|
-
width: r.width,
|
|
628
|
-
height: r.height,
|
|
629
|
-
...posStyle,
|
|
630
|
-
...rectStyle
|
|
631
|
-
}
|
|
632
|
-
},
|
|
633
|
-
i
|
|
634
|
-
);
|
|
635
|
-
})
|
|
636
|
-
}
|
|
637
|
-
);
|
|
1002
|
+
] })
|
|
1003
|
+
] })
|
|
1004
|
+
] }) }) });
|
|
638
1005
|
}
|
|
639
1006
|
|
|
640
1007
|
// src/MejiroSpread.tsx
|
|
1008
|
+
import {
|
|
1009
|
+
isPrimaryPointerPress as isPrimaryPointerPress2,
|
|
1010
|
+
normalizeFontFamily as normalizeFontFamily4
|
|
1011
|
+
} from "@libraz/mejiro/browser";
|
|
1012
|
+
import {
|
|
1013
|
+
useRef as useRef5
|
|
1014
|
+
} from "react";
|
|
641
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
|
+
}
|
|
642
1025
|
function defaultHeader(data) {
|
|
643
1026
|
return /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-page-header", children: [
|
|
644
1027
|
/* @__PURE__ */ jsx11("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
|
|
@@ -657,6 +1040,7 @@ function MejiroSpread({
|
|
|
657
1040
|
rightHeader = {},
|
|
658
1041
|
leftHeader = {},
|
|
659
1042
|
images = [],
|
|
1043
|
+
slotMode,
|
|
660
1044
|
renderPageHeader,
|
|
661
1045
|
indicator,
|
|
662
1046
|
onPrev,
|
|
@@ -665,24 +1049,25 @@ function MejiroSpread({
|
|
|
665
1049
|
onImageResizePointerDown,
|
|
666
1050
|
onImageClose,
|
|
667
1051
|
anchorAtCoord,
|
|
1052
|
+
spreadIdx,
|
|
668
1053
|
selectionRects,
|
|
669
1054
|
onSelectionChange,
|
|
670
1055
|
singlePage = false,
|
|
1056
|
+
singleSide = "right",
|
|
671
1057
|
onSwipe,
|
|
672
1058
|
onSurfaceTap
|
|
673
1059
|
}) {
|
|
674
1060
|
const messages = useI18n();
|
|
675
|
-
const
|
|
676
|
-
const useSlot = true;
|
|
677
|
-
const selectionStartRef = useRef3(null);
|
|
1061
|
+
const selectionRef = useRef5(null);
|
|
678
1062
|
const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
|
|
679
|
-
const gestureRef =
|
|
1063
|
+
const gestureRef = useRef5(null);
|
|
680
1064
|
const gestureEnabled = onSwipe != null || onSurfaceTap != null;
|
|
681
1065
|
const SWIPE_THRESHOLD = 40;
|
|
682
1066
|
const TAP_MOVE_THRESHOLD = 8;
|
|
683
1067
|
function handleGesturePointerDown(e) {
|
|
684
1068
|
if (!gestureEnabled) return;
|
|
685
|
-
if (e
|
|
1069
|
+
if (!isPrimaryPointerPress2(e)) return;
|
|
1070
|
+
if (e.target?.closest(OVERLAY_SELECTOR)) return;
|
|
686
1071
|
gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
|
|
687
1072
|
}
|
|
688
1073
|
function handleGesturePointerUp(e) {
|
|
@@ -700,15 +1085,13 @@ function MejiroSpread({
|
|
|
700
1085
|
}
|
|
701
1086
|
if (ax < TAP_MOVE_THRESHOLD && ay < TAP_MOVE_THRESHOLD) {
|
|
702
1087
|
const target = e.target;
|
|
703
|
-
if (target?.closest(
|
|
1088
|
+
if (target?.closest(`button, a, ${OVERLAY_SELECTOR}`)) return;
|
|
704
1089
|
onSurfaceTap?.();
|
|
705
1090
|
}
|
|
706
1091
|
}
|
|
707
1092
|
function resolvePointer(e) {
|
|
708
1093
|
if (!anchorAtCoord) return null;
|
|
709
|
-
const
|
|
710
|
-
if (!target) return null;
|
|
711
|
-
const content = target.closest(".mejiro-reader-page-content");
|
|
1094
|
+
const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
|
|
712
1095
|
if (!content) return null;
|
|
713
1096
|
const pageEl = content.closest(".mejiro-reader-page");
|
|
714
1097
|
const isRight = pageEl?.classList.contains("mejiro-reader-page--right") ?? true;
|
|
@@ -719,26 +1102,28 @@ function MejiroSpread({
|
|
|
719
1102
|
return anchorAtCoord(spreadX, offsetY);
|
|
720
1103
|
}
|
|
721
1104
|
function handlePointerDown(e) {
|
|
722
|
-
if (!selectionEnabled) return;
|
|
1105
|
+
if (!selectionEnabled || selectionRef.current) return;
|
|
1106
|
+
if (!e.target?.closest(CONTENT_SELECTOR)) return;
|
|
723
1107
|
const anchor = resolvePointer(e);
|
|
724
1108
|
if (!anchor) return;
|
|
725
1109
|
e.preventDefault();
|
|
726
|
-
|
|
1110
|
+
selectionRef.current = { start: anchor, pointerId: e.pointerId };
|
|
727
1111
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
728
1112
|
onSelectionChange?.(null);
|
|
729
1113
|
}
|
|
730
1114
|
function handlePointerMove(e) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if (!start) return;
|
|
1115
|
+
const session = selectionRef.current;
|
|
1116
|
+
if (!session || e.pointerId !== session.pointerId) return;
|
|
734
1117
|
const end = resolvePointer(e);
|
|
735
1118
|
if (!end) return;
|
|
1119
|
+
const { start } = session;
|
|
736
1120
|
if (end.paragraph === start.paragraph && end.charIndex === start.charIndex) return;
|
|
737
1121
|
onSelectionChange?.({ start, end });
|
|
738
1122
|
}
|
|
739
|
-
function
|
|
740
|
-
|
|
741
|
-
|
|
1123
|
+
function endSelection(e) {
|
|
1124
|
+
const session = selectionRef.current;
|
|
1125
|
+
if (!session || e.pointerId !== session.pointerId) return;
|
|
1126
|
+
selectionRef.current = null;
|
|
742
1127
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
743
1128
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
744
1129
|
}
|
|
@@ -747,8 +1132,13 @@ function MejiroSpread({
|
|
|
747
1132
|
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily4(fontFamily);
|
|
748
1133
|
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
749
1134
|
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
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;
|
|
750
1139
|
const renderPage = (side) => {
|
|
751
1140
|
const isRight = side === "right";
|
|
1141
|
+
const hostsImages = side === imageSide;
|
|
752
1142
|
const result = isRight ? spread.right : spread.left;
|
|
753
1143
|
const header = isRight ? rightHeader : leftHeader;
|
|
754
1144
|
const pageKey = `${side}-${header.pageNumber ?? "blank"}`;
|
|
@@ -756,7 +1146,7 @@ function MejiroSpread({
|
|
|
756
1146
|
width: pageWidth,
|
|
757
1147
|
height: pageHeight
|
|
758
1148
|
};
|
|
759
|
-
if (
|
|
1149
|
+
if (hostsImages && hasImages) pageStyle.overflow = "visible";
|
|
760
1150
|
return /* @__PURE__ */ jsxs7(
|
|
761
1151
|
"div",
|
|
762
1152
|
{
|
|
@@ -770,7 +1160,7 @@ function MejiroSpread({
|
|
|
770
1160
|
MejiroPageView,
|
|
771
1161
|
{
|
|
772
1162
|
result,
|
|
773
|
-
slotMode
|
|
1163
|
+
slotMode,
|
|
774
1164
|
fontFamily,
|
|
775
1165
|
lineSpacing,
|
|
776
1166
|
className: "mejiro-reader-page-content",
|
|
@@ -778,12 +1168,12 @@ function MejiroSpread({
|
|
|
778
1168
|
},
|
|
779
1169
|
pageKey
|
|
780
1170
|
),
|
|
781
|
-
|
|
1171
|
+
visibleRects && visibleRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: visibleRects, side })
|
|
782
1172
|
] }) }),
|
|
783
|
-
|
|
1173
|
+
hostsImages && hostedImages.map((item) => /* @__PURE__ */ jsx11(
|
|
784
1174
|
MejiroImageOverlay,
|
|
785
1175
|
{
|
|
786
|
-
rect: item.rect,
|
|
1176
|
+
rect: isRight ? item.rect : { ...item.rect, x: item.rect.x + pageWidth },
|
|
787
1177
|
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
788
1178
|
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
789
1179
|
onClose: () => onImageClose?.(item.id)
|
|
@@ -799,12 +1189,13 @@ function MejiroSpread({
|
|
|
799
1189
|
if (gestureEnabled) handleGesturePointerDown(e);
|
|
800
1190
|
if (selectionEnabled) handlePointerDown(e);
|
|
801
1191
|
}
|
|
802
|
-
function combinedPointerMove(e) {
|
|
803
|
-
if (selectionEnabled) handlePointerMove(e);
|
|
804
|
-
}
|
|
805
1192
|
function combinedPointerUp(e) {
|
|
806
1193
|
if (gestureEnabled) handleGesturePointerUp(e);
|
|
807
|
-
|
|
1194
|
+
endSelection(e);
|
|
1195
|
+
}
|
|
1196
|
+
function combinedPointerCancel(e) {
|
|
1197
|
+
gestureRef.current = null;
|
|
1198
|
+
endSelection(e);
|
|
808
1199
|
}
|
|
809
1200
|
const useCombined = selectionEnabled || gestureEnabled;
|
|
810
1201
|
return /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs7(
|
|
@@ -812,11 +1203,12 @@ function MejiroSpread({
|
|
|
812
1203
|
{
|
|
813
1204
|
className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
|
|
814
1205
|
onPointerDown: useCombined ? combinedPointerDown : void 0,
|
|
815
|
-
onPointerMove: useCombined ?
|
|
1206
|
+
onPointerMove: useCombined ? handlePointerMove : void 0,
|
|
816
1207
|
onPointerUp: useCombined ? combinedPointerUp : void 0,
|
|
817
|
-
onPointerCancel: useCombined ?
|
|
1208
|
+
onPointerCancel: useCombined ? combinedPointerCancel : void 0,
|
|
1209
|
+
onLostPointerCapture: selectionEnabled ? endSelection : void 0,
|
|
818
1210
|
children: [
|
|
819
|
-
renderPage("right"),
|
|
1211
|
+
renderPage(singlePage ? singleSide : "right"),
|
|
820
1212
|
!singlePage && renderPage("left"),
|
|
821
1213
|
/* @__PURE__ */ jsx11(
|
|
822
1214
|
"button",
|
|
@@ -855,7 +1247,10 @@ function MejiroStats({
|
|
|
855
1247
|
readingTimeLocale = "ja"
|
|
856
1248
|
}) {
|
|
857
1249
|
if (!chapter) return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats" });
|
|
858
|
-
const totalChars = chapter.paragraphs.reduce(
|
|
1250
|
+
const totalChars = chapter.paragraphs.reduce(
|
|
1251
|
+
(s, p) => p.headingLevel != null ? s : s + [...p.text].length,
|
|
1252
|
+
0
|
|
1253
|
+
);
|
|
859
1254
|
const totalRuby = chapter.paragraphs.reduce(
|
|
860
1255
|
(s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
|
|
861
1256
|
0
|
|
@@ -872,29 +1267,127 @@ function MejiroStats({
|
|
|
872
1267
|
return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
|
|
873
1268
|
}
|
|
874
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
|
+
|
|
875
1368
|
// src/useChapterLayout.ts
|
|
876
1369
|
import {
|
|
877
|
-
useCallback,
|
|
878
|
-
useEffect as
|
|
1370
|
+
useCallback as useCallback2,
|
|
1371
|
+
useEffect as useEffect4,
|
|
879
1372
|
useLayoutEffect as useLayoutEffect2,
|
|
880
|
-
useRef as
|
|
881
|
-
useState as
|
|
1373
|
+
useRef as useRef6,
|
|
1374
|
+
useState as useState4
|
|
882
1375
|
} from "react";
|
|
883
1376
|
function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
884
1377
|
const enableResize = options.enableResize ?? true;
|
|
885
1378
|
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
886
|
-
const [layout, setLayout] =
|
|
887
|
-
const [pageWidth, setPageWidth] =
|
|
888
|
-
const [pageHeight, setPageHeight] =
|
|
889
|
-
const [contentHeight, setContentHeight] =
|
|
890
|
-
const [elapsedMs, setElapsedMs] =
|
|
891
|
-
const layoutRef =
|
|
892
|
-
const requestIdRef =
|
|
893
|
-
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);
|
|
894
1387
|
layoutRef.current = layout;
|
|
895
|
-
const optionsRef =
|
|
1388
|
+
const optionsRef = useRef6(options);
|
|
896
1389
|
optionsRef.current = options;
|
|
897
|
-
const recompute =
|
|
1390
|
+
const recompute = useCallback2(
|
|
898
1391
|
async (opts = {}) => {
|
|
899
1392
|
const blank = opts.blank ?? true;
|
|
900
1393
|
const o = optionsRef.current;
|
|
@@ -933,13 +1426,20 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
933
1426
|
setContentHeight(0);
|
|
934
1427
|
setElapsedMs(0);
|
|
935
1428
|
}, [recompute]);
|
|
936
|
-
|
|
1429
|
+
useEffect4(() => {
|
|
937
1430
|
void recompute();
|
|
938
1431
|
return () => {
|
|
939
1432
|
requestIdRef.current++;
|
|
940
1433
|
};
|
|
941
1434
|
}, [recompute]);
|
|
942
|
-
|
|
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(() => {
|
|
943
1443
|
if (!enableResize) return;
|
|
944
1444
|
const el = surface.current;
|
|
945
1445
|
if (!el) return;
|
|
@@ -994,367 +1494,200 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
994
1494
|
|
|
995
1495
|
// src/useEpub.ts
|
|
996
1496
|
import { parseEpub } from "@libraz/mejiro/epub";
|
|
997
|
-
import { useCallback as
|
|
1497
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef7, useState as useState5 } from "react";
|
|
998
1498
|
function useEpub(options = {}) {
|
|
999
|
-
const [epub, setEpub] =
|
|
1000
|
-
const [loading, setLoading] =
|
|
1001
|
-
const [error, setError] =
|
|
1002
|
-
const requestIdRef =
|
|
1003
|
-
const onLoadRef =
|
|
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);
|
|
1004
1504
|
onLoadRef.current = options.onLoad;
|
|
1005
|
-
const onErrorRef =
|
|
1006
|
-
onErrorRef.current = options.onError;
|
|
1007
|
-
const fetchOptionsRef =
|
|
1008
|
-
fetchOptionsRef.current = options.fetchOptions;
|
|
1009
|
-
const fetchEpubRef =
|
|
1010
|
-
fetchEpubRef.current = options.fetchEpub;
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (requestId !== requestIdRef.current) return null;
|
|
1039
|
-
setEpub(book);
|
|
1040
|
-
onLoadRef.current?.(book);
|
|
1041
|
-
return book;
|
|
1042
|
-
} catch (err) {
|
|
1043
|
-
if (requestId === requestIdRef.current) {
|
|
1044
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1045
|
-
setError(nextError);
|
|
1046
|
-
onErrorRef.current?.(nextError);
|
|
1047
|
-
}
|
|
1048
|
-
return null;
|
|
1049
|
-
} finally {
|
|
1050
|
-
if (requestId === requestIdRef.current) setLoading(false);
|
|
1051
|
-
}
|
|
1052
|
-
}, []);
|
|
1053
|
-
const loadUrl = useCallback2(async (url) => {
|
|
1054
|
-
const requestId = ++requestIdRef.current;
|
|
1055
|
-
setLoading(true);
|
|
1056
|
-
setError(null);
|
|
1057
|
-
try {
|
|
1058
|
-
let buffer;
|
|
1059
|
-
if (fetchEpubRef.current) {
|
|
1060
|
-
buffer = await fetchEpubRef.current(url);
|
|
1061
|
-
} else {
|
|
1062
|
-
const init = fetchOptionsRef.current;
|
|
1063
|
-
const res = init ? await fetch(url, init) : await fetch(url);
|
|
1064
|
-
if (!res.ok) return null;
|
|
1065
|
-
buffer = await res.arrayBuffer();
|
|
1066
|
-
}
|
|
1067
|
-
const book = await parseEpub(buffer);
|
|
1068
|
-
if (requestId !== requestIdRef.current) return null;
|
|
1069
|
-
setEpub(book);
|
|
1070
|
-
onLoadRef.current?.(book);
|
|
1071
|
-
return book;
|
|
1072
|
-
} catch (err) {
|
|
1073
|
-
if (requestId === requestIdRef.current) {
|
|
1074
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1075
|
-
setError(nextError);
|
|
1076
|
-
onErrorRef.current?.(nextError);
|
|
1077
|
-
}
|
|
1078
|
-
return null;
|
|
1079
|
-
} finally {
|
|
1080
|
-
if (requestId === requestIdRef.current) setLoading(false);
|
|
1081
|
-
}
|
|
1082
|
-
}, []);
|
|
1083
|
-
const defaultUrl = options.defaultUrl;
|
|
1084
|
-
useEffect3(() => {
|
|
1085
|
-
if (defaultUrl) void loadUrl(defaultUrl);
|
|
1086
|
-
}, [defaultUrl, loadUrl]);
|
|
1087
|
-
const replaceEpub = useCallback2((book) => {
|
|
1088
|
-
requestIdRef.current++;
|
|
1089
|
-
setLoading(false);
|
|
1090
|
-
setError(null);
|
|
1091
|
-
setEpub(book);
|
|
1092
|
-
}, []);
|
|
1093
|
-
return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// src/useMejiroBook.ts
|
|
1097
|
-
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1098
|
-
import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
|
|
1099
|
-
function useMejiroBook(initial) {
|
|
1100
|
-
const bookRef = useRef6(null);
|
|
1101
|
-
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1102
|
-
const [options, setLocal] = useState4(initial);
|
|
1103
|
-
const setOptions = useCallback3(async (next) => {
|
|
1104
|
-
setLocal((prev) => ({ ...prev, ...next }));
|
|
1105
|
-
await bookRef.current?.setOptions(next);
|
|
1106
|
-
}, []);
|
|
1107
|
-
return { book: bookRef.current, options, setOptions };
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// src/useMultiImageOverlay.ts
|
|
1111
|
-
import { moveImageOverlayRect, resizeImageOverlayRect } from "@libraz/mejiro";
|
|
1112
|
-
import {
|
|
1113
|
-
useCallback as useCallback4,
|
|
1114
|
-
useEffect as useEffect4,
|
|
1115
|
-
useMemo as useMemo3,
|
|
1116
|
-
useRef as useRef7,
|
|
1117
|
-
useState as useState5
|
|
1118
|
-
} from "react";
|
|
1119
|
-
var nextId = 0;
|
|
1120
|
-
function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
1121
|
-
const defW = options.defaultWidth ?? 120;
|
|
1122
|
-
const defH = options.defaultHeight ?? 160;
|
|
1123
|
-
const defX = options.defaultX ?? 80;
|
|
1124
|
-
const defY = options.defaultY ?? 100;
|
|
1125
|
-
const margin = options.margin;
|
|
1126
|
-
const [images, setImages] = useState5(() => /* @__PURE__ */ new Map());
|
|
1127
|
-
const imagesRef = useRef7(images);
|
|
1128
|
-
imagesRef.current = images;
|
|
1129
|
-
const layoutRef = useRef7(null);
|
|
1130
|
-
layoutRef.current = layout;
|
|
1131
|
-
const spreadIdxRef = useRef7(spreadIdx);
|
|
1132
|
-
spreadIdxRef.current = spreadIdx;
|
|
1133
|
-
const onUpdateRef = useRef7(options.onUpdate);
|
|
1134
|
-
onUpdateRef.current = options.onUpdate;
|
|
1135
|
-
const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1136
|
-
useEffect4(
|
|
1137
|
-
() => () => {
|
|
1138
|
-
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
1139
|
-
activeDragCleanupsRef.current.clear();
|
|
1140
|
-
},
|
|
1141
|
-
[]
|
|
1142
|
-
);
|
|
1143
|
-
const syncSpread = useCallback4(
|
|
1144
|
-
(si) => {
|
|
1145
|
-
const lo = layoutRef.current;
|
|
1146
|
-
if (!lo) return;
|
|
1147
|
-
const items = imagesRef.current.get(si) ?? [];
|
|
1148
|
-
const bookImages = items.map((it) => ({
|
|
1149
|
-
x: it.rect.x,
|
|
1150
|
-
y: it.rect.y,
|
|
1151
|
-
w: it.rect.w,
|
|
1152
|
-
h: it.rect.h,
|
|
1153
|
-
margin
|
|
1154
|
-
}));
|
|
1155
|
-
lo.setImages(si, bookImages);
|
|
1156
|
-
onUpdateRef.current?.(lo.getSpread(si));
|
|
1157
|
-
},
|
|
1158
|
-
[margin]
|
|
1159
|
-
);
|
|
1160
|
-
const commit = useCallback4(
|
|
1161
|
-
(next, affectedSpread) => {
|
|
1162
|
-
imagesRef.current = next;
|
|
1163
|
-
setImages(next);
|
|
1164
|
-
syncSpread(affectedSpread);
|
|
1165
|
-
},
|
|
1166
|
-
[syncSpread]
|
|
1167
|
-
);
|
|
1168
|
-
const findById = useCallback4(
|
|
1169
|
-
(id) => {
|
|
1170
|
-
for (const [s, list] of imagesRef.current) {
|
|
1171
|
-
const i = list.findIndex((it) => it.id === id);
|
|
1172
|
-
if (i >= 0) return { spread: s, index: i, item: list[i] };
|
|
1173
|
-
}
|
|
1174
|
-
return null;
|
|
1175
|
-
},
|
|
1176
|
-
[]
|
|
1177
|
-
);
|
|
1178
|
-
const addImage = useCallback4(
|
|
1179
|
-
(overrides) => {
|
|
1180
|
-
const si = spreadIdxRef.current;
|
|
1181
|
-
const list = imagesRef.current.get(si) ?? [];
|
|
1182
|
-
const last = list[list.length - 1];
|
|
1183
|
-
const baseX = last ? last.rect.x - 100 : defX;
|
|
1184
|
-
const baseY = last ? last.rect.y + 40 : defY;
|
|
1185
|
-
nextId += 1;
|
|
1186
|
-
const item = {
|
|
1187
|
-
id: `mejiro-img-${nextId}`,
|
|
1188
|
-
rect: {
|
|
1189
|
-
x: overrides?.x ?? baseX,
|
|
1190
|
-
y: overrides?.y ?? baseY,
|
|
1191
|
-
w: overrides?.w ?? defW,
|
|
1192
|
-
h: overrides?.h ?? defH
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
const next = new Map(imagesRef.current);
|
|
1196
|
-
next.set(si, [...list, item]);
|
|
1197
|
-
commit(next, si);
|
|
1198
|
-
return item;
|
|
1199
|
-
},
|
|
1200
|
-
[defX, defY, defW, defH, commit]
|
|
1201
|
-
);
|
|
1202
|
-
const removeImage = useCallback4(
|
|
1203
|
-
(id) => {
|
|
1204
|
-
const found = findById(id);
|
|
1205
|
-
if (!found) return;
|
|
1206
|
-
const next = new Map(imagesRef.current);
|
|
1207
|
-
const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
|
|
1208
|
-
if (list.length === 0) next.delete(found.spread);
|
|
1209
|
-
else next.set(found.spread, list);
|
|
1210
|
-
commit(next, found.spread);
|
|
1211
|
-
},
|
|
1212
|
-
[findById, commit]
|
|
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) => {
|
|
1514
|
+
const requestId = ++requestIdRef.current;
|
|
1515
|
+
setLoading(true);
|
|
1516
|
+
setError(null);
|
|
1517
|
+
try {
|
|
1518
|
+
const book = await parseEpub(await read(), { limits: limitsRef.current });
|
|
1519
|
+
if (requestId !== requestIdRef.current) return null;
|
|
1520
|
+
setEpub(book);
|
|
1521
|
+
onLoadRef.current?.(book);
|
|
1522
|
+
return book;
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
if (requestId === requestIdRef.current) {
|
|
1525
|
+
const nextError = toError(err);
|
|
1526
|
+
setError(nextError);
|
|
1527
|
+
onErrorRef.current?.(nextError);
|
|
1528
|
+
}
|
|
1529
|
+
return null;
|
|
1530
|
+
} finally {
|
|
1531
|
+
if (requestId === requestIdRef.current) setLoading(false);
|
|
1532
|
+
}
|
|
1533
|
+
}, []);
|
|
1534
|
+
const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
|
|
1535
|
+
const loadFile = useCallback3(
|
|
1536
|
+
(file) => load(() => readEpubFile(file, limitsRef.current)),
|
|
1537
|
+
[load]
|
|
1213
1538
|
);
|
|
1214
|
-
const
|
|
1215
|
-
(
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const list = (next.get(found.spread) ?? []).map(
|
|
1220
|
-
(it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
|
|
1221
|
-
);
|
|
1222
|
-
next.set(found.spread, list);
|
|
1223
|
-
commit(next, found.spread);
|
|
1224
|
-
},
|
|
1225
|
-
[findById, commit]
|
|
1539
|
+
const loadUrl = useCallback3(
|
|
1540
|
+
(url) => load(
|
|
1541
|
+
() => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
|
|
1542
|
+
),
|
|
1543
|
+
[load]
|
|
1226
1544
|
);
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1545
|
+
const defaultUrl = options.defaultUrl;
|
|
1546
|
+
useEffect5(() => {
|
|
1547
|
+
if (defaultUrl) void loadUrl(defaultUrl);
|
|
1548
|
+
}, [defaultUrl, loadUrl]);
|
|
1549
|
+
const replaceEpub = useCallback3((book) => {
|
|
1550
|
+
requestIdRef.current++;
|
|
1551
|
+
setLoading(false);
|
|
1552
|
+
setError(null);
|
|
1553
|
+
setEpub(book);
|
|
1554
|
+
}, []);
|
|
1555
|
+
return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/useMejiroBook.ts
|
|
1559
|
+
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1560
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
|
|
1561
|
+
function useMejiroBook(initial, options = {}) {
|
|
1562
|
+
const bookRef = useRef8(null);
|
|
1563
|
+
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1564
|
+
const [snapshot, setLocal] = useState6(initial);
|
|
1565
|
+
const optionsRef = useRef8(options);
|
|
1566
|
+
optionsRef.current = options;
|
|
1567
|
+
const pendingRef = useRef8(null);
|
|
1568
|
+
const flush = useCallback4(() => {
|
|
1569
|
+
const pending = pendingRef.current;
|
|
1570
|
+
if (!pending) return;
|
|
1571
|
+
pendingRef.current = null;
|
|
1572
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1573
|
+
const { waiters } = pending;
|
|
1574
|
+
const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
|
|
1575
|
+
void applied.then(
|
|
1576
|
+
() => {
|
|
1577
|
+
for (const waiter of waiters) waiter.resolve();
|
|
1578
|
+
},
|
|
1579
|
+
(err) => {
|
|
1580
|
+
const error = toError(err);
|
|
1581
|
+
const handler = optionsRef.current.onError;
|
|
1582
|
+
if (handler) {
|
|
1583
|
+
handler(error);
|
|
1584
|
+
for (const waiter of waiters) waiter.resolve();
|
|
1585
|
+
} else {
|
|
1586
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
1236
1587
|
}
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
}, []);
|
|
1591
|
+
const setOptions = useCallback4(
|
|
1592
|
+
(next) => {
|
|
1593
|
+
setLocal((prev) => mergeDefined(prev, next));
|
|
1594
|
+
const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
|
|
1595
|
+
pendingRef.current = pending;
|
|
1596
|
+
pending.patch = mergeDefined(pending.patch, next);
|
|
1597
|
+
const settled = new Promise((resolve, reject) => {
|
|
1598
|
+
pending.waiters.push({ resolve, reject });
|
|
1599
|
+
});
|
|
1600
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1601
|
+
const debounceMs = optionsRef.current.debounceMs ?? 0;
|
|
1602
|
+
if (debounceMs > 0) {
|
|
1603
|
+
pending.timer = setTimeout(() => {
|
|
1604
|
+
pending.timer = null;
|
|
1605
|
+
flush();
|
|
1606
|
+
}, debounceMs);
|
|
1237
1607
|
} else {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
commit(next, target);
|
|
1608
|
+
pending.timer = null;
|
|
1609
|
+
flush();
|
|
1241
1610
|
}
|
|
1611
|
+
return settled;
|
|
1242
1612
|
},
|
|
1243
|
-
[
|
|
1244
|
-
);
|
|
1245
|
-
const onOverlayPointerDown = useCallback4(
|
|
1246
|
-
(id, e) => {
|
|
1247
|
-
e.preventDefault();
|
|
1248
|
-
const found = findById(id);
|
|
1249
|
-
if (!found) return;
|
|
1250
|
-
const startX = e.clientX;
|
|
1251
|
-
const startY = e.clientY;
|
|
1252
|
-
const start = { ...found.item.rect };
|
|
1253
|
-
const target = e.currentTarget;
|
|
1254
|
-
target.setPointerCapture(e.nativeEvent.pointerId);
|
|
1255
|
-
target.classList.add("is-dragging");
|
|
1256
|
-
let rafId = 0;
|
|
1257
|
-
const onMove = (me) => {
|
|
1258
|
-
const dx = me.clientX - startX;
|
|
1259
|
-
const dy = me.clientY - startY;
|
|
1260
|
-
const r = moveImageOverlayRect(start, dx, dy);
|
|
1261
|
-
cancelAnimationFrame(rafId);
|
|
1262
|
-
rafId = requestAnimationFrame(() => updateImage(id, r));
|
|
1263
|
-
};
|
|
1264
|
-
let cleanup;
|
|
1265
|
-
const onUp = () => cleanup();
|
|
1266
|
-
cleanup = () => {
|
|
1267
|
-
cancelAnimationFrame(rafId);
|
|
1268
|
-
target.classList.remove("is-dragging");
|
|
1269
|
-
document.removeEventListener("pointermove", onMove);
|
|
1270
|
-
document.removeEventListener("pointerup", onUp);
|
|
1271
|
-
activeDragCleanupsRef.current.delete(cleanup);
|
|
1272
|
-
};
|
|
1273
|
-
document.addEventListener("pointermove", onMove);
|
|
1274
|
-
document.addEventListener("pointerup", onUp);
|
|
1275
|
-
activeDragCleanupsRef.current.add(cleanup);
|
|
1276
|
-
},
|
|
1277
|
-
[findById, updateImage]
|
|
1613
|
+
[flush]
|
|
1278
1614
|
);
|
|
1279
|
-
|
|
1280
|
-
(
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
const start = { ...found.item.rect };
|
|
1288
|
-
const target = e.currentTarget;
|
|
1289
|
-
target.setPointerCapture(e.nativeEvent.pointerId);
|
|
1290
|
-
target.parentElement?.classList.add("is-dragging");
|
|
1291
|
-
let rafId = 0;
|
|
1292
|
-
const onMove = (me) => {
|
|
1293
|
-
const dx = me.clientX - startX;
|
|
1294
|
-
const dy = me.clientY - startY;
|
|
1295
|
-
const r = resizeImageOverlayRect(start, dx, dy);
|
|
1296
|
-
cancelAnimationFrame(rafId);
|
|
1297
|
-
rafId = requestAnimationFrame(() => updateImage(id, r));
|
|
1298
|
-
};
|
|
1299
|
-
let cleanup;
|
|
1300
|
-
const onUp = () => cleanup();
|
|
1301
|
-
cleanup = () => {
|
|
1302
|
-
cancelAnimationFrame(rafId);
|
|
1303
|
-
target.parentElement?.classList.remove("is-dragging");
|
|
1304
|
-
document.removeEventListener("pointermove", onMove);
|
|
1305
|
-
document.removeEventListener("pointerup", onUp);
|
|
1306
|
-
activeDragCleanupsRef.current.delete(cleanup);
|
|
1307
|
-
};
|
|
1308
|
-
document.addEventListener("pointermove", onMove);
|
|
1309
|
-
document.addEventListener("pointerup", onUp);
|
|
1310
|
-
activeDragCleanupsRef.current.add(cleanup);
|
|
1615
|
+
useEffect6(
|
|
1616
|
+
() => () => {
|
|
1617
|
+
bookRef.current?.dispose();
|
|
1618
|
+
const pending = pendingRef.current;
|
|
1619
|
+
if (!pending) return;
|
|
1620
|
+
pendingRef.current = null;
|
|
1621
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1622
|
+
for (const waiter of pending.waiters) waiter.resolve();
|
|
1311
1623
|
},
|
|
1312
|
-
[
|
|
1624
|
+
[]
|
|
1313
1625
|
);
|
|
1314
|
-
|
|
1315
|
-
if (!layout) return;
|
|
1316
|
-
for (const si of imagesRef.current.keys()) syncSpread(si);
|
|
1317
|
-
}, [layout, syncSpread]);
|
|
1318
|
-
const currentImages = useMemo3(() => images.get(spreadIdx) ?? [], [images, spreadIdx]);
|
|
1319
|
-
const hasImages = useMemo3(() => [...images.values()].some((list) => list.length > 0), [images]);
|
|
1320
|
-
return {
|
|
1321
|
-
imagesBySpread: images,
|
|
1322
|
-
currentImages,
|
|
1323
|
-
hasImages,
|
|
1324
|
-
addImage,
|
|
1325
|
-
removeImage,
|
|
1326
|
-
updateImage,
|
|
1327
|
-
clearImages,
|
|
1328
|
-
onOverlayPointerDown,
|
|
1329
|
-
onResizePointerDown
|
|
1330
|
-
};
|
|
1626
|
+
return { book: bookRef.current, options: snapshot, setOptions };
|
|
1331
1627
|
}
|
|
1332
1628
|
|
|
1333
1629
|
// src/useSpread.ts
|
|
1334
|
-
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";
|
|
1631
|
+
function ignoreNavigationKey(e) {
|
|
1632
|
+
if (e.defaultPrevented) return true;
|
|
1633
|
+
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
|
|
1634
|
+
const target = e.target;
|
|
1635
|
+
if (!target) return false;
|
|
1636
|
+
if (target.isContentEditable) return true;
|
|
1637
|
+
const tag = target.tagName;
|
|
1638
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
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
|
+
}
|
|
1335
1656
|
function useSpread(layout, options = {}) {
|
|
1336
1657
|
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1337
1658
|
const turnDuration = options.turnDuration ?? 180;
|
|
1338
|
-
const
|
|
1659
|
+
const single = options.single ?? false;
|
|
1660
|
+
const onChangeRef = useRef9(options.onChange);
|
|
1339
1661
|
onChangeRef.current = options.onChange;
|
|
1340
|
-
const [spreadIdx, setSpreadIdx] =
|
|
1341
|
-
const [
|
|
1342
|
-
|
|
1343
|
-
|
|
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);
|
|
1344
1671
|
layoutRef.current = layout;
|
|
1345
|
-
const spreadIdxRef =
|
|
1672
|
+
const spreadIdxRef = useRef9(0);
|
|
1346
1673
|
spreadIdxRef.current = spreadIdx;
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
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;
|
|
1352
1684
|
const refresh = useCallback5(() => {
|
|
1353
1685
|
if (!layoutRef.current) {
|
|
1354
1686
|
setSpread(null);
|
|
1355
1687
|
return;
|
|
1356
1688
|
}
|
|
1357
|
-
|
|
1689
|
+
const page = firstPageOf(spreadIdxRef.current, singleRef.current);
|
|
1690
|
+
setSpread(layoutRef.current.getSpread(Math.floor(page / 2)));
|
|
1358
1691
|
}, []);
|
|
1359
1692
|
useLayoutEffect3(() => {
|
|
1360
1693
|
layoutGenerationRef.current++;
|
|
@@ -1370,9 +1703,9 @@ function useSpread(layout, options = {}) {
|
|
|
1370
1703
|
}
|
|
1371
1704
|
setSpread(layout.getSpread(0));
|
|
1372
1705
|
}, [layout]);
|
|
1373
|
-
|
|
1706
|
+
useEffect7(() => {
|
|
1374
1707
|
if (!layout) return;
|
|
1375
|
-
setSpread(layout.getSpread(
|
|
1708
|
+
setSpread(layout.getSpread(layoutSpreadIdx));
|
|
1376
1709
|
const last = lastEmittedRef.current;
|
|
1377
1710
|
if (!last) {
|
|
1378
1711
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
@@ -1382,11 +1715,12 @@ function useSpread(layout, options = {}) {
|
|
|
1382
1715
|
onChangeRef.current?.(spreadIdx);
|
|
1383
1716
|
}
|
|
1384
1717
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
1385
|
-
}, [layout, spreadIdx]);
|
|
1718
|
+
}, [layout, spreadIdx, layoutSpreadIdx]);
|
|
1386
1719
|
const goTo = useCallback5(
|
|
1387
1720
|
(index) => {
|
|
1388
1721
|
if (!layoutRef.current) return;
|
|
1389
|
-
const
|
|
1722
|
+
const targetSingle = singleRef.current;
|
|
1723
|
+
const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
|
|
1390
1724
|
const target = Math.max(0, Math.min(max, index));
|
|
1391
1725
|
if (target === spreadIdxRef.current) return;
|
|
1392
1726
|
if (turnDuration > 0) {
|
|
@@ -1395,7 +1729,7 @@ function useSpread(layout, options = {}) {
|
|
|
1395
1729
|
setTurning(true);
|
|
1396
1730
|
turnTimerRef.current = setTimeout(() => {
|
|
1397
1731
|
if (generation !== layoutGenerationRef.current) return;
|
|
1398
|
-
setSpreadIdx(target);
|
|
1732
|
+
setSpreadIdx(indexOfPageIn(firstPageOf(target, targetSingle), singleRef.current));
|
|
1399
1733
|
setTurning(false);
|
|
1400
1734
|
turnTimerRef.current = null;
|
|
1401
1735
|
}, turnDuration);
|
|
@@ -1412,22 +1746,31 @@ function useSpread(layout, options = {}) {
|
|
|
1412
1746
|
turnTimerRef.current = null;
|
|
1413
1747
|
}
|
|
1414
1748
|
setTurning(false);
|
|
1415
|
-
const max =
|
|
1749
|
+
const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
|
|
1416
1750
|
setSpreadIdx(Math.max(0, Math.min(max, index)));
|
|
1417
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
|
+
);
|
|
1418
1760
|
const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
|
|
1419
1761
|
const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
|
|
1420
|
-
|
|
1762
|
+
useEffect7(() => {
|
|
1421
1763
|
if (!enableKeyboard) return;
|
|
1422
1764
|
const onKey = (e) => {
|
|
1423
1765
|
if (!layoutRef.current) return;
|
|
1766
|
+
if (ignoreNavigationKey(e)) return;
|
|
1424
1767
|
if (e.key === "ArrowLeft") next();
|
|
1425
1768
|
else if (e.key === "ArrowRight") prev();
|
|
1426
1769
|
};
|
|
1427
1770
|
window.addEventListener("keydown", onKey);
|
|
1428
1771
|
return () => window.removeEventListener("keydown", onKey);
|
|
1429
1772
|
}, [enableKeyboard, next, prev]);
|
|
1430
|
-
|
|
1773
|
+
useEffect7(
|
|
1431
1774
|
() => () => {
|
|
1432
1775
|
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1433
1776
|
},
|
|
@@ -1436,6 +1779,9 @@ function useSpread(layout, options = {}) {
|
|
|
1436
1779
|
return {
|
|
1437
1780
|
spreadIdx,
|
|
1438
1781
|
spread,
|
|
1782
|
+
firstPage,
|
|
1783
|
+
layoutSpreadIdx,
|
|
1784
|
+
singleSide,
|
|
1439
1785
|
totalPages,
|
|
1440
1786
|
totalSpreads,
|
|
1441
1787
|
turning,
|
|
@@ -1443,12 +1789,61 @@ function useSpread(layout, options = {}) {
|
|
|
1443
1789
|
prev,
|
|
1444
1790
|
goTo,
|
|
1445
1791
|
setSpread: setSpreadIndex,
|
|
1792
|
+
indexOfPage,
|
|
1793
|
+
anchorAt,
|
|
1446
1794
|
refresh
|
|
1447
1795
|
};
|
|
1448
1796
|
}
|
|
1449
1797
|
|
|
1450
1798
|
// src/MejiroReader.tsx
|
|
1451
1799
|
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
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
|
+
}
|
|
1452
1847
|
function MejiroReaderInner(props, ref) {
|
|
1453
1848
|
const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
|
|
1454
1849
|
const dialectProp = "dialect" in props ? props.dialect : void 0;
|
|
@@ -1467,6 +1862,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1467
1862
|
enableSurfaceTap = true,
|
|
1468
1863
|
fallback,
|
|
1469
1864
|
fetchOptions,
|
|
1865
|
+
limits,
|
|
1470
1866
|
fetchEpub: fetchEpubFn,
|
|
1471
1867
|
locale,
|
|
1472
1868
|
messages,
|
|
@@ -1494,15 +1890,18 @@ function MejiroReaderInner(props, ref) {
|
|
|
1494
1890
|
onPageRead,
|
|
1495
1891
|
onChapterCompleted
|
|
1496
1892
|
} = props;
|
|
1497
|
-
const surfaceRef =
|
|
1498
|
-
const fileRef =
|
|
1499
|
-
const [settingsOpen, setSettingsOpen] =
|
|
1500
|
-
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);
|
|
1501
1897
|
const chapterIsUncontrolled = chapterProp == null;
|
|
1502
1898
|
const chapter = chapterProp ?? chapterState;
|
|
1503
|
-
const [chromeHidden, setChromeHidden] =
|
|
1504
|
-
const [
|
|
1505
|
-
|
|
1899
|
+
const [chromeHidden, setChromeHidden] = useState8(false);
|
|
1900
|
+
const [userScroll, setUserScroll] = useState8(
|
|
1901
|
+
null
|
|
1902
|
+
);
|
|
1903
|
+
const [autoSingle, setAutoSingle] = useState8(false);
|
|
1904
|
+
useEffect8(() => {
|
|
1506
1905
|
if (!settingsOpen) return;
|
|
1507
1906
|
const onKey = (e2) => {
|
|
1508
1907
|
if (e2.key === "Escape") setSettingsOpen(false);
|
|
@@ -1510,7 +1909,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1510
1909
|
window.addEventListener("keydown", onKey);
|
|
1511
1910
|
return () => window.removeEventListener("keydown", onKey);
|
|
1512
1911
|
}, [settingsOpen]);
|
|
1513
|
-
|
|
1912
|
+
useEffect8(() => {
|
|
1514
1913
|
if (spreadMode !== "auto") return;
|
|
1515
1914
|
const surface = surfaceRef.current;
|
|
1516
1915
|
if (!surface) return;
|
|
@@ -1527,16 +1926,19 @@ function MejiroReaderInner(props, ref) {
|
|
|
1527
1926
|
const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
|
|
1528
1927
|
const resolvedGeometry = useMemo4(() => {
|
|
1529
1928
|
const columns = effectiveSingle ? 1 : 2;
|
|
1530
|
-
|
|
1531
|
-
return {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
+
);
|
|
1540
1942
|
}, [fit, pageGeometryProp, effectiveSingle]);
|
|
1541
1943
|
const surfaceAspect = useMemo4(() => {
|
|
1542
1944
|
const columns = effectiveSingle ? 1 : 2;
|
|
@@ -1544,103 +1946,135 @@ function MejiroReaderInner(props, ref) {
|
|
|
1544
1946
|
return `${columns} / ${aspect}`;
|
|
1545
1947
|
}, [effectiveSingle, resolvedGeometry]);
|
|
1546
1948
|
const resolvedOptions = useMemo4(
|
|
1547
|
-
() => (
|
|
1949
|
+
() => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
|
|
1548
1950
|
[optionsProp]
|
|
1549
1951
|
);
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1952
|
+
const onErrorRef = useRef10(onError);
|
|
1953
|
+
onErrorRef.current = onError;
|
|
1954
|
+
const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
|
|
1955
|
+
const {
|
|
1956
|
+
book,
|
|
1957
|
+
options: bookOptions,
|
|
1958
|
+
setOptions
|
|
1959
|
+
} = useMejiroBook(resolvedOptions, {
|
|
1960
|
+
debounceMs: OPTIONS_DEBOUNCE_MS,
|
|
1961
|
+
onError: reportError
|
|
1962
|
+
});
|
|
1963
|
+
const optionsPropKey = JSON.stringify(resolvedOptions);
|
|
1964
|
+
const syncedOptionsPropKeyRef = useRef10(optionsPropKey);
|
|
1965
|
+
const resolvedOptionsRef = useRef10(resolvedOptions);
|
|
1966
|
+
resolvedOptionsRef.current = resolvedOptions;
|
|
1967
|
+
useEffect8(() => {
|
|
1968
|
+
if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
|
|
1969
|
+
syncedOptionsPropKeyRef.current = optionsPropKey;
|
|
1970
|
+
void setOptions(resolvedOptionsRef.current);
|
|
1971
|
+
}, [optionsPropKey, setOptions]);
|
|
1972
|
+
const manuscriptSourceKey = manuscriptKey(manuscriptProp);
|
|
1559
1973
|
const synthesizedEpub = useMemo4(() => {
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1562
|
-
|
|
1974
|
+
if (manuscriptSourceKey === void 0) return null;
|
|
1975
|
+
const chapters = JSON.parse(manuscriptSourceKey);
|
|
1976
|
+
return manuscriptToEpubBook(chapters, { dialect: dialectProp });
|
|
1977
|
+
}, [manuscriptSourceKey, dialectProp]);
|
|
1563
1978
|
const epubCtx = useEpub({
|
|
1564
1979
|
// `epub` / `manuscript` take precedence: skip the URL fetch when a parsed
|
|
1565
1980
|
// book (or a synthesized manuscript book) is supplied.
|
|
1566
1981
|
defaultUrl: epubProp !== void 0 || manuscriptProp !== void 0 ? void 0 : epubUrl,
|
|
1567
1982
|
fetchOptions,
|
|
1983
|
+
limits,
|
|
1568
1984
|
fetchEpub: fetchEpubFn,
|
|
1569
1985
|
onLoad: (b) => {
|
|
1570
1986
|
if (chapterProp == null) setChapterState(0);
|
|
1987
|
+
clearImagesRef.current();
|
|
1571
1988
|
onLoad?.(b);
|
|
1572
1989
|
}
|
|
1573
1990
|
});
|
|
1574
1991
|
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
1575
1992
|
const isManuscriptSource = manuscriptProp !== void 0;
|
|
1576
1993
|
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
1577
|
-
|
|
1578
|
-
onErrorRef.current = onError;
|
|
1579
|
-
useEffect6(() => {
|
|
1994
|
+
useEffect8(() => {
|
|
1580
1995
|
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
1581
1996
|
}, [epubCtx.error]);
|
|
1582
|
-
const pageGeometryRef =
|
|
1997
|
+
const pageGeometryRef = useRef10(resolvedGeometry);
|
|
1583
1998
|
pageGeometryRef.current = resolvedGeometry;
|
|
1584
|
-
const
|
|
1585
|
-
const spreadIdxPropRef =
|
|
1999
|
+
const spreadAnchorRef = useRef10(() => null);
|
|
2000
|
+
const spreadIdxPropRef = useRef10(spreadIdxProp);
|
|
1586
2001
|
spreadIdxPropRef.current = spreadIdxProp;
|
|
2002
|
+
const visibleAnchorRef = useRef10(
|
|
2003
|
+
null
|
|
2004
|
+
);
|
|
1587
2005
|
const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
|
|
1588
2006
|
pageGeometry: () => pageGeometryRef.current,
|
|
1589
2007
|
// Preserve the reading position across a reflow re-layout (size / option
|
|
1590
2008
|
// changes), but only in uncontrolled mode — when `spreadIdx` is controlled
|
|
1591
2009
|
// the host owns the position and the controlled-restore effect handles it.
|
|
1592
|
-
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
|
+
}
|
|
1593
2016
|
});
|
|
1594
|
-
const spreadChangedRef =
|
|
2017
|
+
const spreadChangedRef = useRef10(void 0);
|
|
1595
2018
|
spreadChangedRef.current = (i) => {
|
|
1596
2019
|
onSpreadChange?.(i);
|
|
1597
2020
|
onSpreadIdxChange?.(i);
|
|
1598
2021
|
};
|
|
1599
2022
|
const spreadCtx = useSpread(layoutCtx.layout, {
|
|
1600
2023
|
enableKeyboard,
|
|
2024
|
+
single: effectiveSingle,
|
|
1601
2025
|
onChange: (i) => spreadChangedRef.current?.(i)
|
|
1602
2026
|
});
|
|
1603
|
-
|
|
1604
|
-
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);
|
|
1605
2039
|
setSpreadRef.current = spreadCtx.setSpread;
|
|
1606
2040
|
useLayoutEffect4(() => {
|
|
1607
2041
|
const anchor = layoutCtx.pendingRestore.current;
|
|
1608
2042
|
if (!(anchor && layoutCtx.layout)) return;
|
|
1609
2043
|
layoutCtx.pendingRestore.current = null;
|
|
1610
2044
|
const loc = layoutCtx.layout.locateAnchor(anchor);
|
|
1611
|
-
setSpreadRef.current(loc
|
|
2045
|
+
setSpreadRef.current(loc ? indexOfPageRef.current(loc.pageIdx) : 0);
|
|
2046
|
+
visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
|
|
1612
2047
|
}, [layoutCtx.layout]);
|
|
1613
|
-
const optionsKey =
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
bookOptions.lineSpacing,
|
|
1617
|
-
bookOptions.mode,
|
|
1618
|
-
bookOptions.enableHanging
|
|
1619
|
-
].join("|");
|
|
1620
|
-
const optionsKeyRef = useRef9(optionsKey);
|
|
1621
|
-
const bookOptionsRef = useRef9(bookOptions);
|
|
2048
|
+
const optionsKey = reflowOptionsKey(bookOptions);
|
|
2049
|
+
const optionsKeyRef = useRef10(optionsKey);
|
|
2050
|
+
const bookOptionsRef = useRef10(bookOptions);
|
|
1622
2051
|
bookOptionsRef.current = bookOptions;
|
|
1623
|
-
const recomputeRef =
|
|
2052
|
+
const recomputeRef = useRef10(layoutCtx.recompute);
|
|
1624
2053
|
recomputeRef.current = layoutCtx.recompute;
|
|
1625
|
-
|
|
2054
|
+
useEffect8(() => {
|
|
1626
2055
|
if (optionsKeyRef.current === optionsKey) return;
|
|
1627
2056
|
optionsKeyRef.current = optionsKey;
|
|
1628
2057
|
const timer = setTimeout(() => {
|
|
1629
2058
|
void (async () => {
|
|
1630
|
-
|
|
1631
|
-
|
|
2059
|
+
try {
|
|
2060
|
+
await setOptions({ ...bookOptionsRef.current });
|
|
2061
|
+
await recomputeRef.current({ blank: false });
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
reportError(toError(err));
|
|
2064
|
+
}
|
|
1632
2065
|
})();
|
|
1633
|
-
},
|
|
2066
|
+
}, OPTIONS_DEBOUNCE_MS);
|
|
1634
2067
|
return () => clearTimeout(timer);
|
|
1635
|
-
}, [optionsKey, setOptions]);
|
|
2068
|
+
}, [optionsKey, setOptions, reportError]);
|
|
1636
2069
|
const geometryKey = JSON.stringify(resolvedGeometry ?? null);
|
|
1637
|
-
const geometryKeyRef =
|
|
1638
|
-
|
|
2070
|
+
const geometryKeyRef = useRef10(geometryKey);
|
|
2071
|
+
useEffect8(() => {
|
|
1639
2072
|
if (geometryKeyRef.current === geometryKey) return;
|
|
1640
2073
|
geometryKeyRef.current = geometryKey;
|
|
1641
2074
|
void recomputeRef.current({ blank: false });
|
|
1642
2075
|
}, [geometryKey]);
|
|
1643
|
-
const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.
|
|
2076
|
+
const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.layoutSpreadIdx, {
|
|
2077
|
+
defaultX: IMAGE_DEFAULT_X,
|
|
1644
2078
|
onUpdate: () => spreadCtx.refresh()
|
|
1645
2079
|
});
|
|
1646
2080
|
const annotationRects = useMemo4(() => {
|
|
@@ -1652,27 +2086,44 @@ function MejiroReaderInner(props, ref) {
|
|
|
1652
2086
|
start: annotation.start,
|
|
1653
2087
|
end: annotation.end
|
|
1654
2088
|
});
|
|
1655
|
-
for (const rect of rects) result.push(rect);
|
|
2089
|
+
for (const rect of rects) result.push({ ...rect, color: annotation.color });
|
|
1656
2090
|
}
|
|
1657
2091
|
return result;
|
|
1658
|
-
}, [annotations, layoutCtx.layout, chapter]);
|
|
1659
|
-
const clearImagesRef =
|
|
2092
|
+
}, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
|
|
2093
|
+
const clearImagesRef = useRef10(imageCtx.clearImages);
|
|
1660
2094
|
clearImagesRef.current = imageCtx.clearImages;
|
|
1661
|
-
const onLoadRef =
|
|
2095
|
+
const onLoadRef = useRef10(onLoad);
|
|
1662
2096
|
onLoadRef.current = onLoad;
|
|
1663
|
-
|
|
2097
|
+
const eRef = useRef10(e);
|
|
2098
|
+
eRef.current = e;
|
|
2099
|
+
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
|
|
2100
|
+
useEffect8(() => {
|
|
1664
2101
|
if (!controlled) return;
|
|
1665
2102
|
if (chapterIsUncontrolled) setChapterState(0);
|
|
1666
2103
|
clearImagesRef.current();
|
|
1667
2104
|
book.clearCache();
|
|
1668
|
-
if (
|
|
1669
|
-
}, [controlled,
|
|
1670
|
-
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);
|
|
1671
2114
|
spreadGoToRef.current = spreadCtx.goTo;
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2115
|
+
const appliedSpreadIdxPropRef = useRef10(spreadIdxProp);
|
|
2116
|
+
useEffect8(() => {
|
|
2117
|
+
if (spreadIdxProp == null) {
|
|
2118
|
+
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const propChanged = appliedSpreadIdxPropRef.current !== spreadIdxProp;
|
|
2122
|
+
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
2123
|
+
if (spreadCtx.spreadIdx === spreadIdxProp) return;
|
|
2124
|
+
if (propChanged) spreadGoToRef.current(spreadIdxProp);
|
|
2125
|
+
else setSpreadRef.current(spreadIdxProp);
|
|
2126
|
+
}, [spreadIdxProp, spreadCtx.spreadIdx]);
|
|
1676
2127
|
useLayoutEffect4(() => {
|
|
1677
2128
|
const next = spreadIdxPropRef.current;
|
|
1678
2129
|
if (next == null || !layoutCtx.layout) return;
|
|
@@ -1686,7 +2137,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1686
2137
|
},
|
|
1687
2138
|
[chapter, chapterProp, onChapterChange]
|
|
1688
2139
|
);
|
|
1689
|
-
const listenersRef =
|
|
2140
|
+
const listenersRef = useRef10(/* @__PURE__ */ new Map());
|
|
1690
2141
|
const emit = useCallback6(
|
|
1691
2142
|
(event, payload) => {
|
|
1692
2143
|
const set = listenersRef.current.get(event);
|
|
@@ -1695,8 +2146,15 @@ function MejiroReaderInner(props, ref) {
|
|
|
1695
2146
|
},
|
|
1696
2147
|
[]
|
|
1697
2148
|
);
|
|
1698
|
-
const pendingAnchorRef =
|
|
1699
|
-
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;
|
|
1700
2158
|
const tryApplyPendingAnchor = useCallback6(() => {
|
|
1701
2159
|
const pending = pendingAnchorRef.current;
|
|
1702
2160
|
if (!pending) return;
|
|
@@ -1708,48 +2166,45 @@ function MejiroReaderInner(props, ref) {
|
|
|
1708
2166
|
});
|
|
1709
2167
|
if (!loc) return;
|
|
1710
2168
|
pendingAnchorRef.current = null;
|
|
1711
|
-
spreadGoToRef.current(loc.
|
|
2169
|
+
spreadGoToRef.current(indexOfPageRef.current(loc.pageIdx));
|
|
1712
2170
|
pending.resolve();
|
|
1713
2171
|
}, [chapter, layout]);
|
|
1714
|
-
|
|
2172
|
+
useEffect8(() => {
|
|
1715
2173
|
tryApplyPendingAnchor();
|
|
1716
2174
|
}, [tryApplyPendingAnchor]);
|
|
1717
|
-
|
|
2175
|
+
useEffect8(
|
|
1718
2176
|
() => () => {
|
|
1719
2177
|
pendingAnchorRef.current?.resolve();
|
|
1720
2178
|
pendingAnchorRef.current = null;
|
|
1721
2179
|
},
|
|
1722
2180
|
[]
|
|
1723
2181
|
);
|
|
1724
|
-
const
|
|
1725
|
-
const dwellRef = useRef9(null);
|
|
1726
|
-
const onPageReadRef = useRef9(onPageRead);
|
|
2182
|
+
const onPageReadRef = useRef10(onPageRead);
|
|
1727
2183
|
onPageReadRef.current = onPageRead;
|
|
1728
|
-
const onChapterCompletedRef =
|
|
2184
|
+
const onChapterCompletedRef = useRef10(onChapterCompleted);
|
|
1729
2185
|
onChapterCompletedRef.current = onChapterCompleted;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
useEffect6(() => {
|
|
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(() => {
|
|
1753
2208
|
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
1754
2209
|
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
1755
2210
|
} else if (!spreadCtx.turning && prevTurningRef.current) {
|
|
@@ -1778,23 +2233,14 @@ function MejiroReaderInner(props, ref) {
|
|
|
1778
2233
|
}),
|
|
1779
2234
|
getAnchor: () => {
|
|
1780
2235
|
if (!layout) return null;
|
|
1781
|
-
const inCh =
|
|
2236
|
+
const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
1782
2237
|
return inCh ? { chapter, ...inCh } : null;
|
|
1783
2238
|
},
|
|
1784
2239
|
getVisibleRange: () => {
|
|
1785
2240
|
if (!layout) return null;
|
|
1786
|
-
const start =
|
|
2241
|
+
const start = spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
1787
2242
|
if (!start) return null;
|
|
1788
|
-
const
|
|
1789
|
-
let end;
|
|
1790
|
-
if (next) {
|
|
1791
|
-
end = next;
|
|
1792
|
-
} else {
|
|
1793
|
-
const ch = e?.chapters[chapter];
|
|
1794
|
-
const lastP = (ch?.paragraphs.length ?? 1) - 1;
|
|
1795
|
-
const lastText = ch?.paragraphs[lastP]?.text ?? "";
|
|
1796
|
-
end = { paragraph: Math.max(0, lastP), charIndex: [...lastText].length };
|
|
1797
|
-
}
|
|
2243
|
+
const end = spreadCtx.anchorAt(spreadCtx.spreadIdx + 1) ?? layout.endAnchor() ?? start;
|
|
1798
2244
|
return {
|
|
1799
2245
|
start: { chapter, ...start },
|
|
1800
2246
|
end: { chapter, ...end }
|
|
@@ -1819,10 +2265,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
1819
2265
|
spreadCtx.spreadIdx,
|
|
1820
2266
|
spreadCtx.totalPages,
|
|
1821
2267
|
spreadCtx.totalSpreads,
|
|
2268
|
+
spreadCtx.anchorAt,
|
|
1822
2269
|
onChapter,
|
|
1823
2270
|
chapter,
|
|
1824
2271
|
layout,
|
|
1825
|
-
e,
|
|
1826
2272
|
setOptions,
|
|
1827
2273
|
tryApplyPendingAnchor
|
|
1828
2274
|
]
|
|
@@ -1874,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1874
2320
|
MejiroStats,
|
|
1875
2321
|
{
|
|
1876
2322
|
chapter: e?.chapters[chapter] ?? null,
|
|
1877
|
-
totalPages:
|
|
2323
|
+
totalPages: spreadCtx.totalPages,
|
|
1878
2324
|
elapsedMs: layoutCtx.elapsedMs,
|
|
1879
2325
|
fontLabel
|
|
1880
2326
|
}
|
|
@@ -1893,7 +2339,12 @@ function MejiroReaderInner(props, ref) {
|
|
|
1893
2339
|
{
|
|
1894
2340
|
type: "button",
|
|
1895
2341
|
className: `mejiro-reader-btn${imageCtx.hasImages ? " is-active" : ""}`,
|
|
1896
|
-
onClick: () => imageCtx.addImage(
|
|
2342
|
+
onClick: () => imageCtx.addImage(
|
|
2343
|
+
singlePageImagePlacement(
|
|
2344
|
+
mode === "paginated" ? spreadCtx.singleSide : null,
|
|
2345
|
+
layoutCtx.pageWidth
|
|
2346
|
+
)
|
|
2347
|
+
),
|
|
1897
2348
|
children: resolvedMessages.imageButton
|
|
1898
2349
|
}
|
|
1899
2350
|
),
|
|
@@ -1911,7 +2362,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1911
2362
|
)
|
|
1912
2363
|
] })
|
|
1913
2364
|
] }) : null;
|
|
1914
|
-
const currentSpread = spreadCtx.
|
|
2365
|
+
const currentSpread = spreadCtx.layoutSpreadIdx;
|
|
1915
2366
|
const rightPage = currentSpread * 2 + 1;
|
|
1916
2367
|
const leftPage = currentSpread * 2 + 2;
|
|
1917
2368
|
const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
|
|
@@ -1955,18 +2406,29 @@ function MejiroReaderInner(props, ref) {
|
|
|
1955
2406
|
fontFamily: bookOptions.fontFamily,
|
|
1956
2407
|
fontSize: bookOptions.fontSize,
|
|
1957
2408
|
lineSpacing: bookOptions.lineSpacing,
|
|
1958
|
-
scrollToPage:
|
|
2409
|
+
scrollToPage: scrollTargetPage(
|
|
2410
|
+
spreadCtx,
|
|
2411
|
+
userScroll?.layout === layoutCtx.layout ? userScroll.page : null
|
|
2412
|
+
),
|
|
1959
2413
|
onVisiblePageChange: (pageIdx, source) => {
|
|
1960
2414
|
if (source === "programmatic") return;
|
|
1961
|
-
|
|
2415
|
+
if (layoutCtx.layout) setUserScroll({ layout: layoutCtx.layout, page: pageIdx });
|
|
2416
|
+
const target = spreadCtx.indexOfPage(pageIdx);
|
|
1962
2417
|
if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
|
|
1963
|
-
}
|
|
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
|
|
1964
2425
|
}
|
|
1965
2426
|
),
|
|
1966
2427
|
layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
|
|
1967
2428
|
MejiroSpread,
|
|
1968
2429
|
{
|
|
1969
2430
|
singlePage: effectiveSingle,
|
|
2431
|
+
singleSide: spreadCtx.singleSide ?? void 0,
|
|
1970
2432
|
onSwipe: (dir) => dir === "next" ? spreadCtx.next() : spreadCtx.prev(),
|
|
1971
2433
|
onSurfaceTap: enableSurfaceTap ? () => setChromeHidden((v) => !v) : void 0,
|
|
1972
2434
|
spread: spreadCtx.spread,
|
|
@@ -1998,6 +2460,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1998
2460
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
1999
2461
|
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
2000
2462
|
onImageClose: imageCtx.removeImage,
|
|
2463
|
+
spreadIdx: spreadCtx.layoutSpreadIdx,
|
|
2001
2464
|
selectionRects: annotationRects.length ? annotationRects : void 0
|
|
2002
2465
|
},
|
|
2003
2466
|
`${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
|
|
@@ -2030,28 +2493,6 @@ MejiroReader.displayName = "MejiroReader";
|
|
|
2030
2493
|
|
|
2031
2494
|
// src/MejiroEditor.tsx
|
|
2032
2495
|
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2033
|
-
function cloneBook(book) {
|
|
2034
|
-
return {
|
|
2035
|
-
...book,
|
|
2036
|
-
chapters: book.chapters.map((chapter) => ({
|
|
2037
|
-
...chapter,
|
|
2038
|
-
blocks: chapter.blocks.map(
|
|
2039
|
-
(block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
|
|
2040
|
-
),
|
|
2041
|
-
imageAssets: new Map(chapter.imageAssets),
|
|
2042
|
-
originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
|
|
2043
|
-
paragraphs: chapter.paragraphs.map((paragraph) => ({
|
|
2044
|
-
...paragraph,
|
|
2045
|
-
inlineAnnotations: [...paragraph.inlineAnnotations]
|
|
2046
|
-
})),
|
|
2047
|
-
paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
|
|
2048
|
-
})),
|
|
2049
|
-
packageData: {
|
|
2050
|
-
...book.packageData,
|
|
2051
|
-
files: new Map(book.packageData.files)
|
|
2052
|
-
}
|
|
2053
|
-
};
|
|
2054
|
-
}
|
|
2055
2496
|
function utf16ToCodepoint(text, utf16Offset) {
|
|
2056
2497
|
let cp = 0;
|
|
2057
2498
|
let i = 0;
|
|
@@ -2063,15 +2504,29 @@ function utf16ToCodepoint(text, utf16Offset) {
|
|
|
2063
2504
|
}
|
|
2064
2505
|
return cp;
|
|
2065
2506
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2507
|
+
var WATERMARK_PREFIX = "[mejiro-watermark]";
|
|
2508
|
+
var WATERMARK_BLOCK_ID = "mejiro-watermark";
|
|
2509
|
+
function watermarkedBook(book, watermark) {
|
|
2510
|
+
const copy = cloneEditableEpubBook(book);
|
|
2511
|
+
const text = `${WATERMARK_PREFIX} ${watermark.text}`;
|
|
2512
|
+
for (const chapter of copy.chapters) {
|
|
2513
|
+
chapter.blocks.unshift({
|
|
2514
|
+
kind: "paragraph",
|
|
2515
|
+
id: watermarkBlockId(chapter),
|
|
2516
|
+
text,
|
|
2517
|
+
inlineAnnotations: []
|
|
2518
|
+
});
|
|
2519
|
+
chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
|
|
2520
|
+
chapter.isDirty = true;
|
|
2521
|
+
}
|
|
2522
|
+
return copy;
|
|
2523
|
+
}
|
|
2524
|
+
function watermarkBlockId(chapter) {
|
|
2525
|
+
const used = new Set(chapter.blocks.map((block) => block.id));
|
|
2526
|
+
let id = WATERMARK_BLOCK_ID;
|
|
2527
|
+
let suffix = 2;
|
|
2528
|
+
while (used.has(id)) id = `${WATERMARK_BLOCK_ID}-${suffix++}`;
|
|
2529
|
+
return id;
|
|
2075
2530
|
}
|
|
2076
2531
|
function MejiroEditor({
|
|
2077
2532
|
epubUrl,
|
|
@@ -2083,75 +2538,80 @@ function MejiroEditor({
|
|
|
2083
2538
|
onBeforeExport,
|
|
2084
2539
|
exportPolicy,
|
|
2085
2540
|
assetResolver,
|
|
2541
|
+
limits,
|
|
2086
2542
|
onLoad,
|
|
2087
2543
|
onExport,
|
|
2088
2544
|
onError
|
|
2089
2545
|
}) {
|
|
2090
2546
|
const messages = useI18n();
|
|
2091
|
-
const imageInputRef =
|
|
2092
|
-
const textareaRef =
|
|
2093
|
-
const
|
|
2094
|
-
const [
|
|
2095
|
-
const [
|
|
2096
|
-
const [
|
|
2097
|
-
const [
|
|
2098
|
-
const [
|
|
2099
|
-
const [
|
|
2100
|
-
const [
|
|
2101
|
-
const [
|
|
2102
|
-
const
|
|
2103
|
-
const
|
|
2104
|
-
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);
|
|
2563
|
+
limitsRef.current = limits;
|
|
2105
2564
|
const book = editor?.book ?? null;
|
|
2106
2565
|
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
2107
2566
|
const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
|
|
2108
2567
|
const textDirty = paragraph ? text !== paragraph.text : false;
|
|
2109
2568
|
const previewBook = useMemo5(() => {
|
|
2110
2569
|
void revision;
|
|
2111
|
-
return book ?
|
|
2570
|
+
return book ? cloneEditableEpubBook(book) : null;
|
|
2112
2571
|
}, [book, revision]);
|
|
2113
|
-
|
|
2572
|
+
useEffect9(() => {
|
|
2114
2573
|
onLoadRef.current = onLoad;
|
|
2115
2574
|
}, [onLoad]);
|
|
2116
|
-
|
|
2575
|
+
useEffect9(() => {
|
|
2117
2576
|
onErrorRef.current = onError;
|
|
2118
2577
|
}, [onError]);
|
|
2119
|
-
const
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
try {
|
|
2123
|
-
const next = await EditableEpub.load(buffer);
|
|
2124
|
-
if (requestId !== loadRequestIdRef.current) return;
|
|
2125
|
-
setEditor(next);
|
|
2126
|
-
setSelection({ chapter: 0, paragraph: 0 });
|
|
2127
|
-
setRevision((value) => value + 1);
|
|
2128
|
-
onLoadRef.current?.(next);
|
|
2129
|
-
} catch (err) {
|
|
2130
|
-
if (requestId === loadRequestIdRef.current) {
|
|
2131
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2132
|
-
setError(nextError);
|
|
2133
|
-
onErrorRef.current?.(nextError);
|
|
2134
|
-
}
|
|
2135
|
-
} finally {
|
|
2136
|
-
if (requestId === loadRequestIdRef.current) setLoading(false);
|
|
2137
|
-
}
|
|
2578
|
+
const reportError = useCallback7((nextError) => {
|
|
2579
|
+
setError(nextError);
|
|
2580
|
+
onErrorRef.current?.(nextError);
|
|
2138
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
|
+
);
|
|
2139
2601
|
async function loadFile(file) {
|
|
2140
2602
|
const requestId = ++loadRequestIdRef.current;
|
|
2141
2603
|
setLoading(true);
|
|
2142
2604
|
setError(null);
|
|
2143
2605
|
try {
|
|
2144
|
-
await loadBufferForRequest(await file.
|
|
2606
|
+
await loadBufferForRequest(await readEpubFile(file, limitsRef.current), requestId);
|
|
2145
2607
|
} catch (err) {
|
|
2146
2608
|
if (requestId === loadRequestIdRef.current) {
|
|
2147
|
-
|
|
2148
|
-
setError(nextError);
|
|
2149
|
-
onErrorRef.current?.(nextError);
|
|
2609
|
+
reportError(toError(err));
|
|
2150
2610
|
setLoading(false);
|
|
2151
2611
|
}
|
|
2152
2612
|
}
|
|
2153
2613
|
}
|
|
2154
|
-
|
|
2614
|
+
useEffect9(() => {
|
|
2155
2615
|
if (!epubUrl) return;
|
|
2156
2616
|
let cancelled = false;
|
|
2157
2617
|
const requestId = ++loadRequestIdRef.current;
|
|
@@ -2159,17 +2619,11 @@ function MejiroEditor({
|
|
|
2159
2619
|
setLoading(true);
|
|
2160
2620
|
setError(null);
|
|
2161
2621
|
try {
|
|
2162
|
-
const
|
|
2163
|
-
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
2164
|
-
const buffer = await res.arrayBuffer();
|
|
2622
|
+
const buffer = await fetchEpubBuffer(epubUrl);
|
|
2165
2623
|
if (cancelled || requestId !== loadRequestIdRef.current) return;
|
|
2166
2624
|
await loadBufferForRequest(buffer, requestId);
|
|
2167
2625
|
} catch (err) {
|
|
2168
|
-
if (!cancelled && requestId === loadRequestIdRef.current)
|
|
2169
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2170
|
-
setError(nextError);
|
|
2171
|
-
onErrorRef.current?.(nextError);
|
|
2172
|
-
}
|
|
2626
|
+
if (!cancelled && requestId === loadRequestIdRef.current) reportError(toError(err));
|
|
2173
2627
|
} finally {
|
|
2174
2628
|
if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
|
|
2175
2629
|
}
|
|
@@ -2177,14 +2631,35 @@ function MejiroEditor({
|
|
|
2177
2631
|
return () => {
|
|
2178
2632
|
cancelled = true;
|
|
2179
2633
|
};
|
|
2180
|
-
}, [epubUrl, loadBufferForRequest]);
|
|
2181
|
-
|
|
2182
|
-
|
|
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);
|
|
2183
2648
|
setRubyStart(0);
|
|
2184
|
-
setRubyEnd(Math.min(1, [...
|
|
2649
|
+
setRubyEnd(Math.min(1, [...nextText].length));
|
|
2185
2650
|
setRubyText("");
|
|
2186
|
-
}, [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]);
|
|
2187
2660
|
function selectParagraph(chapterIndex, paragraphIndex) {
|
|
2661
|
+
if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
|
|
2662
|
+
if (textDirty) applyText();
|
|
2188
2663
|
setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
|
|
2189
2664
|
}
|
|
2190
2665
|
function captureRubyRange() {
|
|
@@ -2228,38 +2703,49 @@ function MejiroEditor({
|
|
|
2228
2703
|
setRevision((value) => value + 1);
|
|
2229
2704
|
}
|
|
2230
2705
|
async function addImage(file) {
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
data
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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);
|
|
2240
2720
|
}
|
|
2241
2721
|
async function exportEpub() {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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);
|
|
2256
2739
|
}
|
|
2257
2740
|
return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
|
|
2258
2741
|
/* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
|
|
2259
2742
|
previewBook ? /* @__PURE__ */ jsx14(
|
|
2260
2743
|
MejiroReader,
|
|
2261
2744
|
{
|
|
2745
|
+
ref: readerRef,
|
|
2262
2746
|
epub: previewBook,
|
|
2747
|
+
chapter: selection.chapter,
|
|
2748
|
+
onChapterChange: (index) => selectParagraph(index, 0),
|
|
2263
2749
|
fonts,
|
|
2264
2750
|
subtitle: messages.editorPreviewSubtitle,
|
|
2265
2751
|
chapterNavMode: "panel",
|
|
@@ -2377,7 +2863,7 @@ function paragraphBlockId(chapter, paragraphIndex) {
|
|
|
2377
2863
|
|
|
2378
2864
|
// src/MejiroManuscriptEditor.tsx
|
|
2379
2865
|
import { EpubProject } from "@libraz/mejiro/epub";
|
|
2380
|
-
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";
|
|
2381
2867
|
|
|
2382
2868
|
// src/MejiroNotationHighlighter.tsx
|
|
2383
2869
|
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
@@ -2385,8 +2871,8 @@ import {
|
|
|
2385
2871
|
forwardRef as forwardRef2,
|
|
2386
2872
|
useImperativeHandle as useImperativeHandle2,
|
|
2387
2873
|
useMemo as useMemo6,
|
|
2388
|
-
useRef as
|
|
2389
|
-
useState as
|
|
2874
|
+
useRef as useRef12,
|
|
2875
|
+
useState as useState10
|
|
2390
2876
|
} from "react";
|
|
2391
2877
|
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2392
2878
|
var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
|
|
@@ -2402,11 +2888,11 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
2402
2888
|
onCompositionEnd,
|
|
2403
2889
|
...rest
|
|
2404
2890
|
}, ref) {
|
|
2405
|
-
const textareaRef =
|
|
2406
|
-
const overlayRef =
|
|
2891
|
+
const textareaRef = useRef12(null);
|
|
2892
|
+
const overlayRef = useRef12(null);
|
|
2407
2893
|
useImperativeHandle2(ref, () => textareaRef.current, []);
|
|
2408
|
-
const [isComposing, setIsComposing] =
|
|
2409
|
-
const lastSegmentsRef =
|
|
2894
|
+
const [isComposing, setIsComposing] = useState10(false);
|
|
2895
|
+
const lastSegmentsRef = useRef12(null);
|
|
2410
2896
|
const segments = useMemo6(() => {
|
|
2411
2897
|
if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
|
|
2412
2898
|
const next = buildSegments(value, dialect);
|
|
@@ -2495,10 +2981,11 @@ function buildSegments(text, dialect) {
|
|
|
2495
2981
|
}
|
|
2496
2982
|
|
|
2497
2983
|
// src/useManuscriptDraft.ts
|
|
2498
|
-
import { useCallback as useCallback8, useEffect as
|
|
2984
|
+
import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
|
|
2499
2985
|
var DEFAULT_DELAY = 800;
|
|
2500
|
-
function defaultChapter(
|
|
2501
|
-
|
|
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) };
|
|
2502
2989
|
}
|
|
2503
2990
|
function useManuscriptDraft(options = {}) {
|
|
2504
2991
|
const {
|
|
@@ -2509,83 +2996,104 @@ function useManuscriptDraft(options = {}) {
|
|
|
2509
2996
|
defaultChapterTitle,
|
|
2510
2997
|
defaultChapterBody
|
|
2511
2998
|
} = options;
|
|
2512
|
-
const titleForRef =
|
|
2999
|
+
const titleForRef = useRef13(defaultChapterTitle);
|
|
2513
3000
|
titleForRef.current = defaultChapterTitle;
|
|
2514
|
-
const bodyForRef =
|
|
3001
|
+
const bodyForRef = useRef13(defaultChapterBody);
|
|
2515
3002
|
bodyForRef.current = defaultChapterBody;
|
|
2516
|
-
const [chapters, setChaptersState] =
|
|
2517
|
-
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter(
|
|
3003
|
+
const [chapters, setChaptersState] = useState11(
|
|
3004
|
+
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
|
|
2518
3005
|
);
|
|
2519
|
-
const [selected, setSelectedState] =
|
|
2520
|
-
const [autosaveError, setAutosaveError] =
|
|
2521
|
-
const saveRef =
|
|
3006
|
+
const [selected, setSelectedState] = useState11(0);
|
|
3007
|
+
const [autosaveError, setAutosaveError] = useState11(null);
|
|
3008
|
+
const saveRef = useRef13(onAutosave);
|
|
2522
3009
|
saveRef.current = onAutosave;
|
|
2523
|
-
const payloadRef =
|
|
3010
|
+
const payloadRef = useRef13(autosavePayload);
|
|
2524
3011
|
payloadRef.current = autosavePayload;
|
|
2525
|
-
const chaptersRef =
|
|
3012
|
+
const chaptersRef = useRef13(chapters);
|
|
2526
3013
|
chaptersRef.current = chapters;
|
|
2527
|
-
const
|
|
2528
|
-
|
|
2529
|
-
const
|
|
3014
|
+
const selectedRef = useRef13(selected);
|
|
3015
|
+
selectedRef.current = selected;
|
|
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);
|
|
2530
3023
|
const flushAutosave = useCallback8(() => {
|
|
2531
3024
|
const callback = saveRef.current;
|
|
2532
|
-
if (!(callback && dirtyRef.current))
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
3025
|
+
if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const revision = revisionRef.current;
|
|
3029
|
+
inFlightRevisionRef.current = revision;
|
|
3030
|
+
const plain = snapshotChapters(chaptersRef.current);
|
|
3031
|
+
const payload = payloadRef.current ? payloadRef.current(plain) : plain;
|
|
3032
|
+
void Promise.resolve(callback(payload)).then(() => {
|
|
3033
|
+
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
3034
|
+
}).catch((err) => {
|
|
2536
3035
|
if (!mountedRef.current) return;
|
|
2537
|
-
setAutosaveError(
|
|
3036
|
+
setAutosaveError(toError(err));
|
|
3037
|
+
}).finally(() => {
|
|
3038
|
+
if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
|
|
2538
3039
|
});
|
|
2539
3040
|
}, []);
|
|
2540
|
-
|
|
2541
|
-
if (
|
|
2542
|
-
|
|
2543
|
-
|
|
3041
|
+
useEffect10(() => {
|
|
3042
|
+
if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
|
|
3043
|
+
dirtyRef.current = true;
|
|
3044
|
+
revisionRef.current += 1;
|
|
3045
|
+
setAutosaveError(null);
|
|
2544
3046
|
}
|
|
2545
|
-
if (!saveRef.current) return void 0;
|
|
2546
|
-
dirtyRef.current = true;
|
|
2547
|
-
setAutosaveError(null);
|
|
3047
|
+
if (!(saveRef.current && dirtyRef.current)) return void 0;
|
|
2548
3048
|
const timer = setTimeout(() => {
|
|
2549
3049
|
flushAutosave();
|
|
2550
3050
|
}, autosaveDelay);
|
|
2551
3051
|
return () => clearTimeout(timer);
|
|
2552
3052
|
}, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
|
|
2553
|
-
|
|
3053
|
+
useEffect10(() => {
|
|
2554
3054
|
mountedRef.current = true;
|
|
2555
|
-
const
|
|
2556
|
-
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
3055
|
+
const detachPageHide = flushOnPageHide(flushAutosave);
|
|
2557
3056
|
return () => {
|
|
2558
3057
|
flushAutosave();
|
|
2559
3058
|
mountedRef.current = false;
|
|
2560
|
-
|
|
3059
|
+
detachPageHide();
|
|
2561
3060
|
};
|
|
2562
|
-
}, [flushAutosave]);
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
);
|
|
3061
|
+
}, [flushAutosave]);
|
|
3062
|
+
const commit = useCallback8((nextChapters, nextSelected) => {
|
|
3063
|
+
chaptersRef.current = nextChapters;
|
|
3064
|
+
selectedRef.current = nextSelected;
|
|
3065
|
+
setChaptersState(nextChapters);
|
|
3066
|
+
setSelectedState(nextSelected);
|
|
3067
|
+
}, []);
|
|
3068
|
+
const setSelected = useCallback8((index) => {
|
|
3069
|
+
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
3070
|
+
selectedRef.current = next;
|
|
3071
|
+
setSelectedState(next);
|
|
3072
|
+
}, []);
|
|
2569
3073
|
const setChapters = useCallback8(
|
|
2570
3074
|
(next) => {
|
|
2571
|
-
const normalized = next.length ? next : [defaultChapter(
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
3075
|
+
const normalized = next.length ? next : [defaultChapter([], titleForRef.current, bodyForRef.current)];
|
|
3076
|
+
const prev = selectedRef.current;
|
|
3077
|
+
const selectedId = chaptersRef.current[prev]?.id;
|
|
3078
|
+
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
3079
|
+
commit(
|
|
3080
|
+
normalized,
|
|
3081
|
+
nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
|
|
3082
|
+
);
|
|
2578
3083
|
},
|
|
2579
|
-
[
|
|
3084
|
+
[commit]
|
|
2580
3085
|
);
|
|
2581
3086
|
const patchChapter = useCallback8((index, patch) => {
|
|
2582
|
-
|
|
2583
|
-
(
|
|
3087
|
+
const next = chaptersRef.current.map(
|
|
3088
|
+
(chapter, i) => i === index ? { ...chapter, ...patch } : chapter
|
|
2584
3089
|
);
|
|
3090
|
+
chaptersRef.current = next;
|
|
3091
|
+
setChaptersState(next);
|
|
2585
3092
|
}, []);
|
|
2586
|
-
const addChapter = useCallback8(
|
|
2587
|
-
|
|
2588
|
-
const
|
|
3093
|
+
const addChapter = useCallback8(
|
|
3094
|
+
(chapter = {}) => {
|
|
3095
|
+
const current = chaptersRef.current;
|
|
3096
|
+
const generated = defaultChapter(current, titleForRef.current, bodyForRef.current);
|
|
2589
3097
|
const next = [
|
|
2590
3098
|
...current,
|
|
2591
3099
|
{
|
|
@@ -2594,38 +3102,41 @@ function useManuscriptDraft(options = {}) {
|
|
|
2594
3102
|
body: chapter.body ?? generated.body
|
|
2595
3103
|
}
|
|
2596
3104
|
];
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
const removeChapter = useCallback8(
|
|
2602
|
-
|
|
2603
|
-
|
|
3105
|
+
commit(next, next.length - 1);
|
|
3106
|
+
},
|
|
3107
|
+
[commit]
|
|
3108
|
+
);
|
|
3109
|
+
const removeChapter = useCallback8(
|
|
3110
|
+
(index) => {
|
|
3111
|
+
const current = chaptersRef.current;
|
|
3112
|
+
if (current.length <= 1) return;
|
|
2604
3113
|
const next = current.filter((_, i) => i !== index);
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
3114
|
+
const prev = selectedRef.current;
|
|
3115
|
+
let nextSelected;
|
|
3116
|
+
if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
|
|
3117
|
+
else if (index < prev) nextSelected = prev - 1;
|
|
3118
|
+
else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
|
|
3119
|
+
commit(next, nextSelected);
|
|
3120
|
+
},
|
|
3121
|
+
[commit]
|
|
3122
|
+
);
|
|
3123
|
+
const reorderChapters = useCallback8(
|
|
3124
|
+
(from, to) => {
|
|
3125
|
+
const current = chaptersRef.current;
|
|
3126
|
+
if (from < 0 || from >= current.length) return;
|
|
2616
3127
|
const next = [...current];
|
|
2617
3128
|
const [moved] = next.splice(from, 1);
|
|
2618
3129
|
const target = Math.max(0, Math.min(next.length, to));
|
|
2619
3130
|
next.splice(target, 0, moved);
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3131
|
+
const prev = selectedRef.current;
|
|
3132
|
+
let nextSelected = prev;
|
|
3133
|
+
if (prev === from) nextSelected = target;
|
|
3134
|
+
else if (from < prev && target >= prev) nextSelected = prev - 1;
|
|
3135
|
+
else if (from > prev && target <= prev) nextSelected = prev + 1;
|
|
3136
|
+
commit(next, nextSelected);
|
|
3137
|
+
},
|
|
3138
|
+
[commit]
|
|
3139
|
+
);
|
|
2629
3140
|
return {
|
|
2630
3141
|
chapters,
|
|
2631
3142
|
selected,
|
|
@@ -2686,31 +3197,34 @@ function MejiroManuscriptEditor({
|
|
|
2686
3197
|
cover: coverProp,
|
|
2687
3198
|
onCoverChange,
|
|
2688
3199
|
chapters: initialChapters,
|
|
3200
|
+
dialect = "mejiro",
|
|
2689
3201
|
onAutosave,
|
|
2690
3202
|
autosaveDelay,
|
|
2691
3203
|
previewProps,
|
|
2692
3204
|
assetResolver,
|
|
2693
|
-
onExport
|
|
3205
|
+
onExport,
|
|
3206
|
+
onError,
|
|
3207
|
+
panelSide = "right"
|
|
2694
3208
|
}) {
|
|
2695
3209
|
const messages = useI18n();
|
|
2696
|
-
const coverInputRef =
|
|
2697
|
-
const bodyTextareaRef =
|
|
3210
|
+
const coverInputRef = useRef14(null);
|
|
3211
|
+
const bodyTextareaRef = useRef14(null);
|
|
2698
3212
|
const titleControlled = onTitleChange !== void 0;
|
|
2699
3213
|
const authorControlled = onAuthorChange !== void 0;
|
|
2700
3214
|
const coverControlled = onCoverChange !== void 0;
|
|
2701
|
-
const [titleState, setTitleState] =
|
|
2702
|
-
const [authorState, setAuthorState] =
|
|
2703
|
-
const [coverState, setCoverState] =
|
|
3215
|
+
const [titleState, setTitleState] = useState12(titleProp ?? messages.manuscriptDefaultTitle);
|
|
3216
|
+
const [authorState, setAuthorState] = useState12(authorProp ?? "");
|
|
3217
|
+
const [coverState, setCoverState] = useState12(coverProp ?? null);
|
|
2704
3218
|
const title = titleControlled ? titleProp ?? "" : titleState;
|
|
2705
3219
|
const author = authorControlled ? authorProp ?? "" : authorState;
|
|
2706
3220
|
const cover = coverControlled ? coverProp ?? null : coverState;
|
|
2707
|
-
|
|
3221
|
+
useEffect11(() => {
|
|
2708
3222
|
if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
|
|
2709
3223
|
}, [titleControlled, titleProp]);
|
|
2710
|
-
|
|
3224
|
+
useEffect11(() => {
|
|
2711
3225
|
if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
|
|
2712
3226
|
}, [authorControlled, authorProp]);
|
|
2713
|
-
|
|
3227
|
+
useEffect11(() => {
|
|
2714
3228
|
if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
|
|
2715
3229
|
}, [coverControlled, coverProp]);
|
|
2716
3230
|
const setTitle = useCallback9(
|
|
@@ -2752,7 +3266,8 @@ function MejiroManuscriptEditor({
|
|
|
2752
3266
|
reorderChapters
|
|
2753
3267
|
} = draft;
|
|
2754
3268
|
const current = chapters[selected] ?? chapters[0];
|
|
2755
|
-
const [draggingIndex, setDraggingIndex] =
|
|
3269
|
+
const [draggingIndex, setDraggingIndex] = useState12(null);
|
|
3270
|
+
const [exportError, setExportError] = useState12(null);
|
|
2756
3271
|
function wrapSelection(open, close) {
|
|
2757
3272
|
const el = bodyTextareaRef.current;
|
|
2758
3273
|
if (!el) return;
|
|
@@ -2770,39 +3285,70 @@ function MejiroManuscriptEditor({
|
|
|
2770
3285
|
bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
|
|
2771
3286
|
});
|
|
2772
3287
|
}
|
|
3288
|
+
const [identifier] = useState12(() => `urn:uuid:${crypto.randomUUID()}`);
|
|
2773
3289
|
const exportEpub = useCallback9(async () => {
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
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
|
+
}))
|
|
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);
|
|
3316
|
+
}
|
|
3317
|
+
);
|
|
3318
|
+
}, [
|
|
3319
|
+
assetResolver,
|
|
3320
|
+
author,
|
|
3321
|
+
chapters,
|
|
3322
|
+
cover,
|
|
3323
|
+
dialect,
|
|
3324
|
+
identifier,
|
|
3325
|
+
messages.untitled,
|
|
3326
|
+
onError,
|
|
3327
|
+
onExport,
|
|
3328
|
+
title
|
|
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
|
+
);
|
|
3338
|
+
return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", "data-panel-side": panelSide, children: [
|
|
2794
3339
|
/* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
|
|
2795
3340
|
MejiroReader,
|
|
2796
3341
|
{
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
3342
|
+
...mergeDefined(
|
|
3343
|
+
{
|
|
3344
|
+
subtitle: messages.manuscriptPreviewSubtitle,
|
|
3345
|
+
chapterNavMode: "panel",
|
|
3346
|
+
enableSurfaceTap: false
|
|
3347
|
+
},
|
|
3348
|
+
previewProps ?? {}
|
|
3349
|
+
),
|
|
3350
|
+
manuscript: previewManuscript,
|
|
3351
|
+
dialect,
|
|
2806
3352
|
fonts,
|
|
2807
3353
|
chapter: selected,
|
|
2808
3354
|
onChapterChange: setSelected,
|
|
@@ -2816,6 +3362,7 @@ function MejiroManuscriptEditor({
|
|
|
2816
3362
|
/* @__PURE__ */ jsx16("small", { children: messages.manuscriptRubyHint })
|
|
2817
3363
|
] }),
|
|
2818
3364
|
draft.autosaveError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: draft.autosaveError.message }),
|
|
3365
|
+
exportError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: exportError.message }),
|
|
2819
3366
|
/* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
|
|
2820
3367
|
/* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
|
|
2821
3368
|
/* @__PURE__ */ jsx16("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
|
|
@@ -2905,6 +3452,7 @@ function MejiroManuscriptEditor({
|
|
|
2905
3452
|
{
|
|
2906
3453
|
ref: bodyTextareaRef,
|
|
2907
3454
|
className: "mejiro-editor-manuscript",
|
|
3455
|
+
dialect,
|
|
2908
3456
|
value: current.body,
|
|
2909
3457
|
onChange: (body) => patchChapter(selected, { body })
|
|
2910
3458
|
}
|
|
@@ -2953,12 +3501,15 @@ function MejiroShelf({
|
|
|
2953
3501
|
}
|
|
2954
3502
|
|
|
2955
3503
|
// src/MejiroToc.tsx
|
|
2956
|
-
import { useMemo as
|
|
3504
|
+
import { useMemo as useMemo8, useState as useState13 } from "react";
|
|
2957
3505
|
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2958
3506
|
function buildEntries(epub, messages) {
|
|
2959
3507
|
return epub.chapters.map((ch, i) => {
|
|
2960
3508
|
const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
|
|
2961
|
-
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
|
+
});
|
|
2962
3513
|
return { index: i, title, headings };
|
|
2963
3514
|
});
|
|
2964
3515
|
}
|
|
@@ -2973,13 +3524,13 @@ function MejiroToc({
|
|
|
2973
3524
|
const messages = useI18n();
|
|
2974
3525
|
const resolvedTitle = title ?? messages.tocTitle;
|
|
2975
3526
|
const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
|
|
2976
|
-
const [query, setQuery] =
|
|
2977
|
-
const entries =
|
|
2978
|
-
const filtered =
|
|
3527
|
+
const [query, setQuery] = useState13("");
|
|
3528
|
+
const entries = useMemo8(() => buildEntries(epub, messages), [epub, messages]);
|
|
3529
|
+
const filtered = useMemo8(() => {
|
|
2979
3530
|
if (!query) return entries;
|
|
2980
3531
|
const needle = query.toLowerCase();
|
|
2981
3532
|
return entries.filter(
|
|
2982
|
-
(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))
|
|
2983
3534
|
);
|
|
2984
3535
|
}, [entries, query]);
|
|
2985
3536
|
const activeIndex = currentAnchor?.chapter ?? -1;
|
|
@@ -3012,7 +3563,7 @@ function MejiroToc({
|
|
|
3012
3563
|
]
|
|
3013
3564
|
}
|
|
3014
3565
|
),
|
|
3015
|
-
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)) })
|
|
3016
3567
|
] }, entry.index)) }),
|
|
3017
3568
|
filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
|
|
3018
3569
|
] });
|
|
@@ -3025,57 +3576,44 @@ import {
|
|
|
3025
3576
|
serializeAnnotations,
|
|
3026
3577
|
sortAnnotations
|
|
3027
3578
|
} from "@libraz/mejiro";
|
|
3028
|
-
import { useCallback as useCallback10, useEffect as
|
|
3029
|
-
function
|
|
3030
|
-
|
|
3031
|
-
const w = globalThis.localStorage;
|
|
3032
|
-
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));
|
|
3033
3582
|
}
|
|
3034
3583
|
function useAnnotations(options) {
|
|
3035
3584
|
const { key, throttleMs = 250 } = options;
|
|
3036
3585
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
3037
|
-
const [annotations, setAnnotations] =
|
|
3038
|
-
|
|
3039
|
-
try {
|
|
3040
|
-
return sortAnnotations(parseAnnotations(storage.getItem(key)));
|
|
3041
|
-
} catch {
|
|
3042
|
-
return [];
|
|
3043
|
-
}
|
|
3044
|
-
});
|
|
3045
|
-
useEffect10(() => {
|
|
3046
|
-
if (!storage) {
|
|
3047
|
-
setAnnotations([]);
|
|
3048
|
-
return;
|
|
3049
|
-
}
|
|
3050
|
-
try {
|
|
3051
|
-
setAnnotations(sortAnnotations(parseAnnotations(storage.getItem(key))));
|
|
3052
|
-
} catch {
|
|
3053
|
-
setAnnotations([]);
|
|
3054
|
-
}
|
|
3055
|
-
}, [storage, key]);
|
|
3056
|
-
const timerRef = useRef14(null);
|
|
3057
|
-
useEffect10(
|
|
3058
|
-
() => () => {
|
|
3059
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3060
|
-
},
|
|
3061
|
-
[]
|
|
3586
|
+
const [annotations, setAnnotations] = useState14(
|
|
3587
|
+
() => readStorage(storage, key, parseSorted, [])
|
|
3062
3588
|
);
|
|
3063
|
-
const
|
|
3589
|
+
const storageRef = useRef15(storage);
|
|
3590
|
+
storageRef.current = storage;
|
|
3591
|
+
const annotationsRef = useRef15(annotations);
|
|
3592
|
+
const applyNext = useCallback10((next) => {
|
|
3593
|
+
annotationsRef.current = next;
|
|
3594
|
+
setAnnotations(next);
|
|
3595
|
+
}, []);
|
|
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);
|
|
3064
3605
|
onChangeRef.current = options.onChange;
|
|
3065
3606
|
const commit = useCallback10(
|
|
3066
3607
|
(next) => {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
storage.setItem(key, serializeAnnotations(next));
|
|
3072
|
-
} catch {
|
|
3073
|
-
}
|
|
3608
|
+
const currentStorage = storageRef.current;
|
|
3609
|
+
if (currentStorage) {
|
|
3610
|
+
pending.schedule(() => {
|
|
3611
|
+
currentStorage.setItem(key, serializeAnnotations(next));
|
|
3074
3612
|
}, throttleMs);
|
|
3075
3613
|
}
|
|
3076
3614
|
onChangeRef.current?.(next);
|
|
3077
3615
|
},
|
|
3078
|
-
[
|
|
3616
|
+
[key, throttleMs, pending]
|
|
3079
3617
|
);
|
|
3080
3618
|
const add = useCallback10(
|
|
3081
3619
|
(input) => {
|
|
@@ -3084,93 +3622,97 @@ function useAnnotations(options) {
|
|
|
3084
3622
|
id: input.id ?? createAnnotationId(),
|
|
3085
3623
|
createdAt: input.createdAt ?? Date.now()
|
|
3086
3624
|
};
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
return next;
|
|
3091
|
-
});
|
|
3625
|
+
const next = sortAnnotations([...annotationsRef.current, annotation]);
|
|
3626
|
+
applyNext(next);
|
|
3627
|
+
commit(next);
|
|
3092
3628
|
return annotation;
|
|
3093
3629
|
},
|
|
3094
|
-
[commit]
|
|
3630
|
+
[commit, applyNext]
|
|
3095
3631
|
);
|
|
3096
3632
|
const remove = useCallback10(
|
|
3097
3633
|
(id) => {
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3634
|
+
const current = annotationsRef.current;
|
|
3635
|
+
const next = current.filter((annotation) => annotation.id !== id);
|
|
3636
|
+
if (next.length === current.length) return;
|
|
3637
|
+
applyNext(next);
|
|
3638
|
+
commit(next);
|
|
3103
3639
|
},
|
|
3104
|
-
[commit]
|
|
3640
|
+
[commit, applyNext]
|
|
3105
3641
|
);
|
|
3106
3642
|
const update = useCallback10(
|
|
3107
3643
|
(id, patch) => {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
return { ...annotation, ...patch, id };
|
|
3114
|
-
});
|
|
3115
|
-
if (!changed) return current;
|
|
3116
|
-
const sorted = sortAnnotations(next);
|
|
3117
|
-
commit(sorted);
|
|
3118
|
-
return sorted;
|
|
3644
|
+
let changed = false;
|
|
3645
|
+
const next = annotationsRef.current.map((annotation) => {
|
|
3646
|
+
if (annotation.id !== id) return annotation;
|
|
3647
|
+
changed = true;
|
|
3648
|
+
return { ...mergeDefined(annotation, patch), id };
|
|
3119
3649
|
});
|
|
3650
|
+
if (!changed) return;
|
|
3651
|
+
const sorted = sortAnnotations(next);
|
|
3652
|
+
applyNext(sorted);
|
|
3653
|
+
commit(sorted);
|
|
3120
3654
|
},
|
|
3121
|
-
[commit]
|
|
3655
|
+
[commit, applyNext]
|
|
3122
3656
|
);
|
|
3123
3657
|
const clear = useCallback10(() => {
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
try {
|
|
3128
|
-
storage.removeItem(key);
|
|
3129
|
-
} catch {
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3658
|
+
applyNext([]);
|
|
3659
|
+
pending.cancel();
|
|
3660
|
+
removeStorage(storageRef.current, key);
|
|
3132
3661
|
onChangeRef.current?.([]);
|
|
3133
|
-
}, [
|
|
3662
|
+
}, [key, pending, applyNext]);
|
|
3134
3663
|
return { annotations, add, remove, update, clear };
|
|
3135
3664
|
}
|
|
3136
3665
|
|
|
3137
3666
|
// src/useEditableEpub.ts
|
|
3138
3667
|
import {
|
|
3668
|
+
clampEditableEpubSelection,
|
|
3669
|
+
cloneEditableEpubBook as cloneEditableEpubBook2,
|
|
3139
3670
|
EditableEpub as EditableEpub2
|
|
3140
3671
|
} from "@libraz/mejiro/epub";
|
|
3141
|
-
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";
|
|
3142
3673
|
function useEditableEpub(options = {}) {
|
|
3143
|
-
const [editor, setEditor] =
|
|
3144
|
-
const [loading, setLoading] =
|
|
3145
|
-
const [exporting, setExporting] =
|
|
3146
|
-
const [error, setError] =
|
|
3147
|
-
const [revision, setRevision] =
|
|
3148
|
-
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({
|
|
3149
3680
|
chapter: 0,
|
|
3150
3681
|
paragraph: 0
|
|
3151
3682
|
});
|
|
3152
|
-
const requestIdRef =
|
|
3153
|
-
const onLoadRef =
|
|
3154
|
-
const onErrorRef =
|
|
3155
|
-
const onExportRef =
|
|
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);
|
|
3156
3689
|
onLoadRef.current = options.onLoad;
|
|
3157
3690
|
onErrorRef.current = options.onError;
|
|
3158
3691
|
onExportRef.current = options.onExport;
|
|
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;
|
|
3159
3698
|
const loadBufferWithRequest = useCallback11(
|
|
3160
3699
|
async (buffer, requestId) => {
|
|
3161
3700
|
setLoading(true);
|
|
3162
3701
|
setError(null);
|
|
3163
3702
|
try {
|
|
3164
|
-
const next = await EditableEpub2.load(buffer);
|
|
3703
|
+
const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
|
|
3165
3704
|
if (requestId !== requestIdRef.current) return null;
|
|
3705
|
+
const firstParagraph = { chapter: 0, paragraph: 0 };
|
|
3706
|
+
editorRef.current = next;
|
|
3707
|
+
selectionRef.current = firstParagraph;
|
|
3166
3708
|
setEditor(next);
|
|
3167
|
-
setSelectionState(
|
|
3709
|
+
setSelectionState(firstParagraph);
|
|
3168
3710
|
setRevision((value) => value + 1);
|
|
3169
3711
|
onLoadRef.current?.(next);
|
|
3170
3712
|
return next;
|
|
3171
3713
|
} catch (err) {
|
|
3172
3714
|
if (requestId === requestIdRef.current) {
|
|
3173
|
-
const nextError =
|
|
3715
|
+
const nextError = toError(err);
|
|
3174
3716
|
setError(nextError);
|
|
3175
3717
|
onErrorRef.current?.(nextError);
|
|
3176
3718
|
}
|
|
@@ -3194,10 +3736,10 @@ function useEditableEpub(options = {}) {
|
|
|
3194
3736
|
setLoading(true);
|
|
3195
3737
|
setError(null);
|
|
3196
3738
|
try {
|
|
3197
|
-
return await loadBufferWithRequest(await file.
|
|
3739
|
+
return await loadBufferWithRequest(await readEpubFile(file, limitsRef.current), requestId);
|
|
3198
3740
|
} catch (err) {
|
|
3199
3741
|
if (requestId === requestIdRef.current) {
|
|
3200
|
-
const nextError =
|
|
3742
|
+
const nextError = toError(err);
|
|
3201
3743
|
setError(nextError);
|
|
3202
3744
|
onErrorRef.current?.(nextError);
|
|
3203
3745
|
setLoading(false);
|
|
@@ -3213,12 +3755,10 @@ function useEditableEpub(options = {}) {
|
|
|
3213
3755
|
setLoading(true);
|
|
3214
3756
|
setError(null);
|
|
3215
3757
|
try {
|
|
3216
|
-
|
|
3217
|
-
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
3218
|
-
return await loadBufferWithRequest(await res.arrayBuffer(), requestId);
|
|
3758
|
+
return await loadBufferWithRequest(await fetchEpubBuffer(url), requestId);
|
|
3219
3759
|
} catch (err) {
|
|
3220
3760
|
if (requestId === requestIdRef.current) {
|
|
3221
|
-
const nextError =
|
|
3761
|
+
const nextError = toError(err);
|
|
3222
3762
|
setError(nextError);
|
|
3223
3763
|
onErrorRef.current?.(nextError);
|
|
3224
3764
|
setLoading(false);
|
|
@@ -3228,74 +3768,74 @@ function useEditableEpub(options = {}) {
|
|
|
3228
3768
|
},
|
|
3229
3769
|
[loadBufferWithRequest]
|
|
3230
3770
|
);
|
|
3231
|
-
|
|
3771
|
+
useEffect13(() => {
|
|
3232
3772
|
if (options.defaultUrl) void loadUrl(options.defaultUrl);
|
|
3233
3773
|
}, [options.defaultUrl, loadUrl]);
|
|
3234
3774
|
const book = editor?.book ?? null;
|
|
3235
3775
|
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3236
|
-
const previewBook =
|
|
3776
|
+
const previewBook = useMemo9(() => {
|
|
3237
3777
|
void revision;
|
|
3238
|
-
return book ?
|
|
3778
|
+
return book ? cloneEditableEpubBook2(book) : null;
|
|
3239
3779
|
}, [book, revision]);
|
|
3240
3780
|
const history = editor?.history ?? null;
|
|
3241
|
-
const setSelection = useCallback11(
|
|
3242
|
-
(nextSelection)
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
);
|
|
3781
|
+
const setSelection = useCallback11((nextSelection) => {
|
|
3782
|
+
const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
|
|
3783
|
+
selectionRef.current = next;
|
|
3784
|
+
setSelectionState(next);
|
|
3785
|
+
}, []);
|
|
3247
3786
|
const updateParagraph = useCallback11(
|
|
3248
3787
|
(text, inlineAnnotations) => {
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
});
|
|
3254
|
-
setRevision((value) => value + 1);
|
|
3255
|
-
},
|
|
3256
|
-
[editor, selection.chapter, selection.paragraph]
|
|
3257
|
-
);
|
|
3258
|
-
const setInlineAnnotations = useCallback11(
|
|
3259
|
-
(inlineAnnotations) => {
|
|
3260
|
-
if (!editor) return;
|
|
3261
|
-
editor.setInlineAnnotations(selection.chapter, selection.paragraph, inlineAnnotations);
|
|
3262
|
-
setRevision((value) => value + 1);
|
|
3263
|
-
},
|
|
3264
|
-
[editor, selection.chapter, selection.paragraph]
|
|
3265
|
-
);
|
|
3266
|
-
const addImage = useCallback11(
|
|
3267
|
-
(image) => {
|
|
3268
|
-
if (!editor) return;
|
|
3269
|
-
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 });
|
|
3270
3792
|
setRevision((value) => value + 1);
|
|
3271
3793
|
},
|
|
3272
|
-
[
|
|
3794
|
+
[]
|
|
3273
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
|
+
}, []);
|
|
3274
3809
|
const undo = useCallback11(() => {
|
|
3275
|
-
|
|
3276
|
-
|
|
3810
|
+
const current = editorRef.current;
|
|
3811
|
+
if (!current) return false;
|
|
3812
|
+
const changed = current.undo();
|
|
3277
3813
|
if (changed) setRevision((value) => value + 1);
|
|
3278
3814
|
return changed;
|
|
3279
|
-
}, [
|
|
3815
|
+
}, []);
|
|
3280
3816
|
const redo = useCallback11(() => {
|
|
3281
|
-
|
|
3282
|
-
|
|
3817
|
+
const current = editorRef.current;
|
|
3818
|
+
if (!current) return false;
|
|
3819
|
+
const changed = current.redo();
|
|
3283
3820
|
if (changed) setRevision((value) => value + 1);
|
|
3284
3821
|
return changed;
|
|
3285
|
-
}, [
|
|
3822
|
+
}, []);
|
|
3286
3823
|
const exportEpub = useCallback11(
|
|
3287
|
-
async (
|
|
3288
|
-
|
|
3824
|
+
async (exportOptions) => {
|
|
3825
|
+
const current = editorRef.current;
|
|
3826
|
+
if (!current) return null;
|
|
3289
3827
|
setExporting(true);
|
|
3290
3828
|
try {
|
|
3291
|
-
const buffer = await
|
|
3829
|
+
const buffer = await current.export(
|
|
3830
|
+
withAssetResolver(exportOptions, assetResolverRef.current)
|
|
3831
|
+
);
|
|
3292
3832
|
onExportRef.current?.(buffer);
|
|
3293
3833
|
return buffer;
|
|
3294
3834
|
} finally {
|
|
3295
3835
|
setExporting(false);
|
|
3296
3836
|
}
|
|
3297
3837
|
},
|
|
3298
|
-
[
|
|
3838
|
+
[]
|
|
3299
3839
|
);
|
|
3300
3840
|
return {
|
|
3301
3841
|
editor,
|
|
@@ -3320,38 +3860,9 @@ function useEditableEpub(options = {}) {
|
|
|
3320
3860
|
exportEpub
|
|
3321
3861
|
};
|
|
3322
3862
|
}
|
|
3323
|
-
function
|
|
3324
|
-
return
|
|
3325
|
-
|
|
3326
|
-
chapters: book.chapters.map((chapter) => ({
|
|
3327
|
-
...chapter,
|
|
3328
|
-
blocks: chapter.blocks.map(
|
|
3329
|
-
(block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
|
|
3330
|
-
),
|
|
3331
|
-
imageAssets: new Map(chapter.imageAssets),
|
|
3332
|
-
originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
|
|
3333
|
-
paragraphs: chapter.paragraphs.map((paragraph) => ({
|
|
3334
|
-
...paragraph,
|
|
3335
|
-
inlineAnnotations: [...paragraph.inlineAnnotations]
|
|
3336
|
-
})),
|
|
3337
|
-
paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
|
|
3338
|
-
})),
|
|
3339
|
-
packageData: {
|
|
3340
|
-
...book.packageData,
|
|
3341
|
-
files: new Map(book.packageData.files)
|
|
3342
|
-
}
|
|
3343
|
-
};
|
|
3344
|
-
}
|
|
3345
|
-
function clampSelection(book, selection) {
|
|
3346
|
-
if (!book?.chapters.length) return { chapter: 0, paragraph: 0 };
|
|
3347
|
-
const chapter = clampInteger(selection.chapter, 0, book.chapters.length - 1);
|
|
3348
|
-
const paragraphCount = book.chapters[chapter]?.paragraphs.length ?? 0;
|
|
3349
|
-
const paragraph = paragraphCount ? clampInteger(selection.paragraph, 0, paragraphCount - 1) : 0;
|
|
3350
|
-
return { chapter, paragraph };
|
|
3351
|
-
}
|
|
3352
|
-
function clampInteger(value, min, max) {
|
|
3353
|
-
if (!Number.isFinite(value)) return min;
|
|
3354
|
-
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
3863
|
+
function withAssetResolver(options, resolver) {
|
|
3864
|
+
if (!resolver || options?.assetResolver) return options;
|
|
3865
|
+
return { ...options, assetResolver: resolver };
|
|
3355
3866
|
}
|
|
3356
3867
|
|
|
3357
3868
|
// src/useEpubProject.ts
|
|
@@ -3359,52 +3870,69 @@ import {
|
|
|
3359
3870
|
EpubProject as EpubProject2,
|
|
3360
3871
|
parseEpub as parseEpub2
|
|
3361
3872
|
} from "@libraz/mejiro/epub";
|
|
3362
|
-
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";
|
|
3363
3874
|
function useEpubProject(options = {}) {
|
|
3364
3875
|
const defaultTitle = options.defaultChapterTitle;
|
|
3365
3876
|
const defaultBody = options.defaultChapterBody;
|
|
3366
|
-
const [metadata, setMetadataState] =
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
);
|
|
3374
|
-
const [
|
|
3375
|
-
const [previewBook, setPreviewBook] =
|
|
3376
|
-
const [previewError, setPreviewError] =
|
|
3377
|
-
const [previewing, setPreviewing] =
|
|
3378
|
-
const previewRequestIdRef =
|
|
3379
|
-
const onPreviewRef =
|
|
3380
|
-
const onExportRef =
|
|
3877
|
+
const [metadata, setMetadataState] = useState16(
|
|
3878
|
+
() => initialMetadata(options.metadata)
|
|
3879
|
+
);
|
|
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);
|
|
3381
3892
|
onPreviewRef.current = options.onPreview;
|
|
3382
3893
|
onExportRef.current = options.onExport;
|
|
3383
|
-
const
|
|
3384
|
-
|
|
3385
|
-
|
|
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;
|
|
3906
|
+
const buildProject = useCallback12(() => {
|
|
3907
|
+
const currentCover = coverRef.current;
|
|
3908
|
+
const project = EpubProject2.fromManuscript({
|
|
3909
|
+
metadata: metadataRef.current,
|
|
3386
3910
|
includeTitlePage: false,
|
|
3387
3911
|
includeTitleInFirstChapter: true,
|
|
3388
|
-
chapters:
|
|
3912
|
+
chapters: chaptersRef.current.map((chapter) => ({
|
|
3389
3913
|
id: chapter.id,
|
|
3390
3914
|
title: chapter.title || "Untitled",
|
|
3391
3915
|
body: chapter.body
|
|
3392
|
-
}))
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3916
|
+
})),
|
|
3917
|
+
...currentCover ? { cover: currentCover } : {}
|
|
3918
|
+
});
|
|
3919
|
+
for (const asset of assetsRef.current) project.addAsset(asset);
|
|
3920
|
+
return project;
|
|
3921
|
+
}, []);
|
|
3922
|
+
const exportOptions = useCallback12((signal) => {
|
|
3923
|
+
const resolver = assetResolverRef.current;
|
|
3924
|
+
return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
|
|
3925
|
+
}, []);
|
|
3926
|
+
useEffect14(() => {
|
|
3399
3927
|
const requestId = ++previewRequestIdRef.current;
|
|
3928
|
+
const controller = new AbortController();
|
|
3400
3929
|
let cancelled = false;
|
|
3401
3930
|
setPreviewing(true);
|
|
3402
3931
|
const timer = setTimeout(() => {
|
|
3403
3932
|
void (async () => {
|
|
3404
3933
|
try {
|
|
3405
|
-
const resolver = assetResolverRef.current;
|
|
3406
3934
|
const book = await parseEpub2(
|
|
3407
|
-
await buildProject().export(
|
|
3935
|
+
await buildProject().export(exportOptions(controller.signal))
|
|
3408
3936
|
);
|
|
3409
3937
|
if (cancelled || requestId !== previewRequestIdRef.current) return;
|
|
3410
3938
|
setPreviewBook(book);
|
|
@@ -3412,7 +3940,7 @@ function useEpubProject(options = {}) {
|
|
|
3412
3940
|
onPreviewRef.current?.(book);
|
|
3413
3941
|
} catch (err) {
|
|
3414
3942
|
if (!cancelled && requestId === previewRequestIdRef.current) {
|
|
3415
|
-
setPreviewError(
|
|
3943
|
+
setPreviewError(toError(err));
|
|
3416
3944
|
}
|
|
3417
3945
|
} finally {
|
|
3418
3946
|
if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
|
|
@@ -3423,104 +3951,122 @@ function useEpubProject(options = {}) {
|
|
|
3423
3951
|
cancelled = true;
|
|
3424
3952
|
previewRequestIdRef.current++;
|
|
3425
3953
|
clearTimeout(timer);
|
|
3954
|
+
controller.abort();
|
|
3426
3955
|
};
|
|
3427
|
-
}, [buildProject, options.debounceMs]);
|
|
3956
|
+
}, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
|
|
3428
3957
|
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
3429
3958
|
const setMetadata = useCallback12((patch) => {
|
|
3430
|
-
|
|
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);
|
|
3431
3968
|
}, []);
|
|
3432
3969
|
const setChapters = useCallback12(
|
|
3433
3970
|
(next) => {
|
|
3434
|
-
const normalized = next.length ? next : [defaultChapter3(
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
[chapters, defaultBody, defaultTitle]
|
|
3443
|
-
);
|
|
3444
|
-
const setSelectedChapter = useCallback12(
|
|
3445
|
-
(index) => {
|
|
3446
|
-
setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
|
|
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
|
+
);
|
|
3447
3979
|
},
|
|
3448
|
-
[
|
|
3980
|
+
[commit, defaultBody, defaultTitle]
|
|
3449
3981
|
);
|
|
3982
|
+
const setCover = useCallback12((asset) => {
|
|
3983
|
+
coverRef.current = asset;
|
|
3984
|
+
setCoverState(asset);
|
|
3985
|
+
}, []);
|
|
3986
|
+
const setAssets = useCallback12((next) => {
|
|
3987
|
+
assetsRef.current = next;
|
|
3988
|
+
setAssetsState(next);
|
|
3989
|
+
}, []);
|
|
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
|
+
}, []);
|
|
3450
3995
|
const patchChapter = useCallback12((index, patch) => {
|
|
3451
|
-
|
|
3452
|
-
(
|
|
3453
|
-
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3454
|
-
)
|
|
3996
|
+
const next = chaptersRef.current.map(
|
|
3997
|
+
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3455
3998
|
);
|
|
3999
|
+
chaptersRef.current = next;
|
|
4000
|
+
setChaptersState(next);
|
|
3456
4001
|
}, []);
|
|
3457
4002
|
const addChapter = useCallback12(
|
|
3458
4003
|
(chapter = {}) => {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
return next;
|
|
3471
|
-
});
|
|
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);
|
|
3472
4015
|
},
|
|
3473
|
-
[defaultBody, defaultTitle]
|
|
4016
|
+
[commit, defaultBody, defaultTitle]
|
|
3474
4017
|
);
|
|
3475
4018
|
const removeChapter = useCallback12(
|
|
3476
|
-
(index =
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
});
|
|
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);
|
|
3487
4029
|
},
|
|
3488
|
-
[
|
|
4030
|
+
[commit]
|
|
3489
4031
|
);
|
|
3490
|
-
const reorderChapters = useCallback12(
|
|
3491
|
-
|
|
3492
|
-
|
|
4032
|
+
const reorderChapters = useCallback12(
|
|
4033
|
+
(from, to) => {
|
|
4034
|
+
const current = chaptersRef.current;
|
|
4035
|
+
if (from < 0 || from >= current.length) return;
|
|
3493
4036
|
const next = [...current];
|
|
3494
4037
|
const [moved] = next.splice(from, 1);
|
|
3495
4038
|
const target = Math.max(0, Math.min(next.length, to));
|
|
3496
4039
|
next.splice(target, 0, moved);
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
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
|
+
);
|
|
3506
4049
|
const exportEpub = useCallback12(async () => {
|
|
3507
|
-
const
|
|
3508
|
-
const buffer = await buildProject().export(resolver ? { assetResolver: resolver } : void 0);
|
|
4050
|
+
const buffer = await buildProject().export(exportOptions());
|
|
3509
4051
|
onExportRef.current?.(buffer);
|
|
3510
4052
|
return buffer;
|
|
3511
|
-
}, [buildProject]);
|
|
3512
|
-
return
|
|
4053
|
+
}, [buildProject, exportOptions]);
|
|
4054
|
+
return useMemo10(
|
|
3513
4055
|
() => ({
|
|
3514
4056
|
metadata,
|
|
3515
4057
|
chapters,
|
|
3516
4058
|
selectedChapter,
|
|
3517
4059
|
currentChapter,
|
|
4060
|
+
cover,
|
|
4061
|
+
assets,
|
|
3518
4062
|
previewBook,
|
|
3519
4063
|
previewError,
|
|
3520
4064
|
previewing,
|
|
3521
4065
|
setMetadata,
|
|
3522
4066
|
setChapters,
|
|
3523
4067
|
setSelectedChapter,
|
|
4068
|
+
setCover,
|
|
4069
|
+
setAssets,
|
|
3524
4070
|
patchChapter,
|
|
3525
4071
|
addChapter,
|
|
3526
4072
|
removeChapter,
|
|
@@ -3530,8 +4076,10 @@ function useEpubProject(options = {}) {
|
|
|
3530
4076
|
}),
|
|
3531
4077
|
[
|
|
3532
4078
|
addChapter,
|
|
4079
|
+
assets,
|
|
3533
4080
|
buildProject,
|
|
3534
4081
|
chapters,
|
|
4082
|
+
cover,
|
|
3535
4083
|
currentChapter,
|
|
3536
4084
|
exportEpub,
|
|
3537
4085
|
metadata,
|
|
@@ -3542,27 +4090,34 @@ function useEpubProject(options = {}) {
|
|
|
3542
4090
|
reorderChapters,
|
|
3543
4091
|
removeChapter,
|
|
3544
4092
|
selectedChapter,
|
|
4093
|
+
setAssets,
|
|
3545
4094
|
setChapters,
|
|
4095
|
+
setCover,
|
|
3546
4096
|
setMetadata,
|
|
3547
4097
|
setSelectedChapter
|
|
3548
4098
|
]
|
|
3549
4099
|
);
|
|
3550
4100
|
}
|
|
3551
|
-
function
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
}
|
|
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) };
|
|
3557
4112
|
}
|
|
3558
4113
|
|
|
3559
4114
|
// src/useImageOverlay.ts
|
|
3560
|
-
import {
|
|
4115
|
+
import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
|
|
3561
4116
|
import {
|
|
3562
4117
|
useCallback as useCallback13,
|
|
3563
|
-
useEffect as
|
|
3564
|
-
useRef as
|
|
3565
|
-
useState as
|
|
4118
|
+
useEffect as useEffect15,
|
|
4119
|
+
useRef as useRef18,
|
|
4120
|
+
useState as useState17
|
|
3566
4121
|
} from "react";
|
|
3567
4122
|
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
3568
4123
|
const defW = options?.defaultWidth ?? 120;
|
|
@@ -3570,17 +4125,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3570
4125
|
const defX = options?.defaultX ?? 80;
|
|
3571
4126
|
const defY = options?.defaultY ?? 100;
|
|
3572
4127
|
const margin = options?.margin;
|
|
3573
|
-
const [imageRect, setImageRect] =
|
|
3574
|
-
const rectRef =
|
|
4128
|
+
const [imageRect, setImageRect] = useState17(null);
|
|
4129
|
+
const rectRef = useRef18(imageRect);
|
|
3575
4130
|
rectRef.current = imageRect;
|
|
3576
|
-
const layoutRef =
|
|
4131
|
+
const layoutRef = useRef18(layout);
|
|
3577
4132
|
layoutRef.current = layout;
|
|
3578
|
-
const spreadRef =
|
|
4133
|
+
const spreadRef = useRef18(spreadIdx);
|
|
3579
4134
|
spreadRef.current = spreadIdx;
|
|
3580
|
-
const onUpdateRef =
|
|
4135
|
+
const onUpdateRef = useRef18(onUpdate);
|
|
3581
4136
|
onUpdateRef.current = onUpdate;
|
|
3582
|
-
const activeDragCleanupsRef =
|
|
3583
|
-
|
|
4137
|
+
const activeDragCleanupsRef = useRef18(/* @__PURE__ */ new Set());
|
|
4138
|
+
useEffect15(
|
|
3584
4139
|
() => () => {
|
|
3585
4140
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
3586
4141
|
activeDragCleanupsRef.current.clear();
|
|
@@ -3597,6 +4152,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3597
4152
|
},
|
|
3598
4153
|
[margin]
|
|
3599
4154
|
);
|
|
4155
|
+
const syncedRef = useRef18({ layout, spreadIdx, margin });
|
|
4156
|
+
useEffect15(() => {
|
|
4157
|
+
const prev = syncedRef.current;
|
|
4158
|
+
syncedRef.current = { layout, spreadIdx, margin };
|
|
4159
|
+
if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
|
|
4160
|
+
if (rectRef.current === null) return;
|
|
4161
|
+
if (prev.layout && prev.spreadIdx !== spreadIdx) {
|
|
4162
|
+
prev.layout.syncImages(prev.spreadIdx, void 0);
|
|
4163
|
+
}
|
|
4164
|
+
syncToLayout(rectRef.current);
|
|
4165
|
+
}, [layout, spreadIdx, margin, syncToLayout]);
|
|
3600
4166
|
const toggleImage = useCallback13(() => {
|
|
3601
4167
|
if (rectRef.current) {
|
|
3602
4168
|
setImageRect(null);
|
|
@@ -3607,80 +4173,57 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3607
4173
|
syncToLayout(r);
|
|
3608
4174
|
}
|
|
3609
4175
|
}, [defX, defY, defW, defH, syncToLayout]);
|
|
4176
|
+
const applyRect = useCallback13(
|
|
4177
|
+
(rect) => {
|
|
4178
|
+
setImageRect(rect);
|
|
4179
|
+
syncToLayout(rect);
|
|
4180
|
+
},
|
|
4181
|
+
[syncToLayout]
|
|
4182
|
+
);
|
|
3610
4183
|
const onOverlayPointerDown = useCallback13(
|
|
3611
4184
|
(e) => {
|
|
4185
|
+
if (!isPrimaryPointerPress3(e)) return;
|
|
3612
4186
|
e.preventDefault();
|
|
3613
4187
|
const current = rectRef.current;
|
|
3614
4188
|
if (!current) return;
|
|
3615
|
-
const startX = e.clientX;
|
|
3616
|
-
const startY = e.clientY;
|
|
3617
|
-
const start = { ...current };
|
|
3618
4189
|
const target = e.currentTarget;
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
};
|
|
3632
|
-
let cleanup;
|
|
3633
|
-
const onUp = () => cleanup();
|
|
3634
|
-
cleanup = () => {
|
|
3635
|
-
cancelAnimationFrame(rafId);
|
|
3636
|
-
target.classList.remove("dragging");
|
|
3637
|
-
document.removeEventListener("pointermove", onMove);
|
|
3638
|
-
document.removeEventListener("pointerup", onUp);
|
|
3639
|
-
activeDragCleanupsRef.current.delete(cleanup);
|
|
3640
|
-
};
|
|
3641
|
-
document.addEventListener("pointermove", onMove);
|
|
3642
|
-
document.addEventListener("pointerup", onUp);
|
|
3643
|
-
activeDragCleanupsRef.current.add(cleanup);
|
|
4190
|
+
createOverlayDragSession2({
|
|
4191
|
+
mode: "move",
|
|
4192
|
+
rect: current,
|
|
4193
|
+
startX: e.clientX,
|
|
4194
|
+
startY: e.clientY,
|
|
4195
|
+
pointerId: e.nativeEvent.pointerId,
|
|
4196
|
+
captureElement: target,
|
|
4197
|
+
activeElement: target,
|
|
4198
|
+
dragClass: "is-dragging",
|
|
4199
|
+
registry: activeDragCleanupsRef.current,
|
|
4200
|
+
onChange: applyRect
|
|
4201
|
+
});
|
|
3644
4202
|
},
|
|
3645
|
-
[
|
|
4203
|
+
[applyRect]
|
|
3646
4204
|
);
|
|
3647
4205
|
const onResizePointerDown = useCallback13(
|
|
3648
4206
|
(e) => {
|
|
4207
|
+
if (!isPrimaryPointerPress3(e)) return;
|
|
3649
4208
|
e.preventDefault();
|
|
3650
4209
|
e.stopPropagation();
|
|
3651
4210
|
const current = rectRef.current;
|
|
3652
4211
|
if (!current) return;
|
|
3653
|
-
const startX = e.clientX;
|
|
3654
|
-
const startY = e.clientY;
|
|
3655
|
-
const start = { ...current };
|
|
3656
4212
|
const target = e.currentTarget;
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
};
|
|
3670
|
-
let cleanup;
|
|
3671
|
-
const onUp = () => cleanup();
|
|
3672
|
-
cleanup = () => {
|
|
3673
|
-
cancelAnimationFrame(rafId);
|
|
3674
|
-
target.parentElement?.classList.remove("dragging");
|
|
3675
|
-
document.removeEventListener("pointermove", onMove);
|
|
3676
|
-
document.removeEventListener("pointerup", onUp);
|
|
3677
|
-
activeDragCleanupsRef.current.delete(cleanup);
|
|
3678
|
-
};
|
|
3679
|
-
document.addEventListener("pointermove", onMove);
|
|
3680
|
-
document.addEventListener("pointerup", onUp);
|
|
3681
|
-
activeDragCleanupsRef.current.add(cleanup);
|
|
4213
|
+
createOverlayDragSession2({
|
|
4214
|
+
mode: "resize",
|
|
4215
|
+
rect: current,
|
|
4216
|
+
startX: e.clientX,
|
|
4217
|
+
startY: e.clientY,
|
|
4218
|
+
pointerId: e.nativeEvent.pointerId,
|
|
4219
|
+
captureElement: target,
|
|
4220
|
+
activeElement: target.parentElement,
|
|
4221
|
+
dragClass: "is-dragging",
|
|
4222
|
+
registry: activeDragCleanupsRef.current,
|
|
4223
|
+
onChange: applyRect
|
|
4224
|
+
});
|
|
3682
4225
|
},
|
|
3683
|
-
[
|
|
4226
|
+
[applyRect]
|
|
3684
4227
|
);
|
|
3685
4228
|
return {
|
|
3686
4229
|
imageRect,
|
|
@@ -3692,122 +4235,140 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3692
4235
|
}
|
|
3693
4236
|
|
|
3694
4237
|
// src/useLibrary.ts
|
|
3695
|
-
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
|
+
}
|
|
3696
4245
|
function useLibrary(options) {
|
|
3697
4246
|
const { volumes, initialVolumeId, onChange } = options;
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
volumes.findIndex((v) => v.id === initialVolumeId)
|
|
3701
|
-
) : 0;
|
|
3702
|
-
const [currentId, setCurrentId] = useState17(
|
|
3703
|
-
volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
|
|
4247
|
+
const [currentId, setCurrentId] = useState18(
|
|
4248
|
+
() => resolveCurrentId(volumes, null, initialVolumeId)
|
|
3704
4249
|
);
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
const found = volumes.findIndex((v) => v.id === currentId);
|
|
3719
|
-
return found >= 0 ? found : 0;
|
|
3720
|
-
}, [currentId, volumes]);
|
|
3721
|
-
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(
|
|
3722
4263
|
() => index >= 0 && index < volumes.length ? volumes[index] : null,
|
|
3723
4264
|
[index, volumes]
|
|
3724
4265
|
);
|
|
3725
|
-
const
|
|
4266
|
+
const activate = useCallback14(
|
|
3726
4267
|
(i) => {
|
|
3727
4268
|
const v = volumes[i];
|
|
3728
|
-
if (v)
|
|
4269
|
+
if (!v) return;
|
|
4270
|
+
setNavigated(true);
|
|
4271
|
+
setCurrentId(v.id);
|
|
4272
|
+
onChange?.(v);
|
|
3729
4273
|
},
|
|
3730
4274
|
[volumes, onChange]
|
|
3731
4275
|
);
|
|
3732
4276
|
const next = useCallback14(() => {
|
|
3733
4277
|
if (index < 0) return;
|
|
3734
4278
|
const ni = Math.min(volumes.length - 1, index + 1);
|
|
3735
|
-
if (ni !== index)
|
|
3736
|
-
|
|
3737
|
-
fire(ni);
|
|
3738
|
-
}
|
|
3739
|
-
}, [index, volumes, fire]);
|
|
4279
|
+
if (ni !== index) activate(ni);
|
|
4280
|
+
}, [index, activate, volumes.length]);
|
|
3740
4281
|
const prev = useCallback14(() => {
|
|
3741
4282
|
if (index < 0) return;
|
|
3742
4283
|
const ni = Math.max(0, index - 1);
|
|
3743
|
-
if (ni !== index)
|
|
3744
|
-
|
|
3745
|
-
fire(ni);
|
|
3746
|
-
}
|
|
3747
|
-
}, [index, volumes, fire]);
|
|
4284
|
+
if (ni !== index) activate(ni);
|
|
4285
|
+
}, [index, activate]);
|
|
3748
4286
|
const goTo = useCallback14(
|
|
3749
4287
|
(id) => {
|
|
3750
4288
|
const ni = volumes.findIndex((v) => v.id === id);
|
|
3751
4289
|
if (ni < 0) return;
|
|
3752
|
-
if (ni !== index)
|
|
3753
|
-
setCurrentId(id);
|
|
3754
|
-
fire(ni);
|
|
3755
|
-
}
|
|
4290
|
+
if (ni !== index) activate(ni);
|
|
3756
4291
|
},
|
|
3757
|
-
[index, volumes,
|
|
4292
|
+
[index, volumes, activate]
|
|
3758
4293
|
);
|
|
3759
4294
|
return { list: volumes, current, currentIndex: index, next, prev, goTo };
|
|
3760
4295
|
}
|
|
3761
4296
|
|
|
3762
4297
|
// src/useManuscriptLayout.ts
|
|
3763
|
-
import {
|
|
4298
|
+
import {
|
|
4299
|
+
useCallback as useCallback15,
|
|
4300
|
+
useEffect as useEffect17,
|
|
4301
|
+
useLayoutEffect as useLayoutEffect5,
|
|
4302
|
+
useRef as useRef19,
|
|
4303
|
+
useState as useState19
|
|
4304
|
+
} from "react";
|
|
3764
4305
|
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
3765
4306
|
const dialect = options.dialect ?? "mejiro";
|
|
3766
4307
|
const enableResize = options.enableResize ?? true;
|
|
3767
4308
|
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
3768
|
-
const [layout, setLayout] =
|
|
3769
|
-
const [pageWidth, setPageWidth] =
|
|
3770
|
-
const [pageHeight, setPageHeight] =
|
|
3771
|
-
const [contentHeight, setContentHeight] =
|
|
3772
|
-
const [elapsedMs, setElapsedMs] =
|
|
3773
|
-
const requestIdRef =
|
|
3774
|
-
const
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
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);
|
|
4316
|
+
layoutRef.current = layout;
|
|
4317
|
+
const pendingRestore = useRef19(null);
|
|
4318
|
+
const optionsRef = useRef19(options);
|
|
4319
|
+
optionsRef.current = options;
|
|
4320
|
+
const recompute = useCallback15(
|
|
4321
|
+
async (opts = {}) => {
|
|
4322
|
+
const blank = opts.blank ?? true;
|
|
4323
|
+
const requestId = ++requestIdRef.current;
|
|
4324
|
+
if (!(chapter && surface.current)) {
|
|
4325
|
+
setLayout(null);
|
|
4326
|
+
setPageWidth(0);
|
|
4327
|
+
setPageHeight(0);
|
|
4328
|
+
setContentHeight(0);
|
|
4329
|
+
setElapsedMs(0);
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
const captured = !blank && layoutRef.current ? optionsRef.current.capturePosition?.(layoutRef.current) ?? null : null;
|
|
4333
|
+
pendingRestore.current = captured;
|
|
4334
|
+
if (blank) setLayout(null);
|
|
4335
|
+
const dims = book.computePageSize(surface.current);
|
|
4336
|
+
setPageWidth(dims.pageWidth);
|
|
4337
|
+
setPageHeight(dims.pageHeight);
|
|
4338
|
+
setContentHeight(dims.contentHeight);
|
|
4339
|
+
const t0 = performance.now();
|
|
4340
|
+
const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
|
|
4341
|
+
if (requestId !== requestIdRef.current) return;
|
|
4342
|
+
const next = layouts.values().next().value ?? null;
|
|
4343
|
+
layoutRef.current = next;
|
|
4344
|
+
setLayout(next);
|
|
4345
|
+
setElapsedMs(performance.now() - t0);
|
|
4346
|
+
},
|
|
4347
|
+
[book, chapter, dialect, surface]
|
|
4348
|
+
);
|
|
3796
4349
|
useLayoutEffect5(() => {
|
|
3797
4350
|
requestIdRef.current++;
|
|
4351
|
+
pendingRestore.current = null;
|
|
3798
4352
|
setLayout(null);
|
|
3799
4353
|
setPageWidth(0);
|
|
3800
4354
|
setPageHeight(0);
|
|
3801
4355
|
setContentHeight(0);
|
|
3802
4356
|
setElapsedMs(0);
|
|
3803
4357
|
}, [recompute]);
|
|
3804
|
-
|
|
4358
|
+
useEffect17(() => {
|
|
3805
4359
|
void recompute();
|
|
3806
4360
|
return () => {
|
|
3807
4361
|
requestIdRef.current++;
|
|
3808
4362
|
};
|
|
3809
4363
|
}, [recompute]);
|
|
3810
|
-
|
|
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(() => {
|
|
3811
4372
|
if (!enableResize) return;
|
|
3812
4373
|
const el = surface.current;
|
|
3813
4374
|
if (!el) return;
|
|
@@ -3822,12 +4383,12 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
|
3822
4383
|
const scheduleReflow = (immediate) => {
|
|
3823
4384
|
clear();
|
|
3824
4385
|
if (immediate) {
|
|
3825
|
-
void recompute();
|
|
4386
|
+
void recompute({ blank: false });
|
|
3826
4387
|
return;
|
|
3827
4388
|
}
|
|
3828
4389
|
timer = setTimeout(() => {
|
|
3829
4390
|
timer = null;
|
|
3830
|
-
void recompute();
|
|
4391
|
+
void recompute({ blank: false });
|
|
3831
4392
|
}, resizeDebounce);
|
|
3832
4393
|
};
|
|
3833
4394
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -3849,7 +4410,7 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
|
3849
4410
|
clear();
|
|
3850
4411
|
};
|
|
3851
4412
|
}, [surface, enableResize, resizeDebounce, recompute]);
|
|
3852
|
-
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
|
|
4413
|
+
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
|
|
3853
4414
|
}
|
|
3854
4415
|
|
|
3855
4416
|
// src/useReadingPosition.ts
|
|
@@ -3857,75 +4418,44 @@ import {
|
|
|
3857
4418
|
parseReadingPosition,
|
|
3858
4419
|
serializeReadingPosition
|
|
3859
4420
|
} from "@libraz/mejiro";
|
|
3860
|
-
import { useCallback as useCallback16, useEffect as
|
|
3861
|
-
function resolveDefaultStorage2() {
|
|
3862
|
-
if (typeof globalThis === "undefined") return null;
|
|
3863
|
-
const w = globalThis.localStorage;
|
|
3864
|
-
return w ?? null;
|
|
3865
|
-
}
|
|
4421
|
+
import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef20, useState as useState20 } from "react";
|
|
3866
4422
|
function useReadingPosition(options) {
|
|
3867
4423
|
const { key, throttleMs = 250 } = options;
|
|
3868
|
-
const storage = options.storage ??
|
|
3869
|
-
const [position, setPosition] =
|
|
3870
|
-
|
|
3871
|
-
try {
|
|
3872
|
-
return parseReadingPosition(storage.getItem(key));
|
|
3873
|
-
} catch {
|
|
3874
|
-
return null;
|
|
3875
|
-
}
|
|
3876
|
-
});
|
|
3877
|
-
const storageRef = useRef19(storage);
|
|
3878
|
-
storageRef.current = storage;
|
|
3879
|
-
useEffect16(() => {
|
|
3880
|
-
const currentStorage = storageRef.current;
|
|
3881
|
-
if (!currentStorage) {
|
|
3882
|
-
setPosition(null);
|
|
3883
|
-
return;
|
|
3884
|
-
}
|
|
3885
|
-
try {
|
|
3886
|
-
setPosition(parseReadingPosition(currentStorage.getItem(key)));
|
|
3887
|
-
} catch {
|
|
3888
|
-
setPosition(null);
|
|
3889
|
-
}
|
|
3890
|
-
}, [key]);
|
|
3891
|
-
const timerRef = useRef19(null);
|
|
3892
|
-
useEffect16(
|
|
3893
|
-
() => () => {
|
|
3894
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3895
|
-
},
|
|
3896
|
-
[]
|
|
4424
|
+
const storage = options.storage ?? resolveDefaultStorage();
|
|
4425
|
+
const [position, setPosition] = useState20(
|
|
4426
|
+
() => readStorage(storage, key, parseReadingPosition, null)
|
|
3897
4427
|
);
|
|
3898
|
-
const
|
|
4428
|
+
const storageRef = useRef20(storage);
|
|
4429
|
+
storageRef.current = storage;
|
|
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);
|
|
3899
4439
|
onChangeRef.current = options.onChange;
|
|
3900
4440
|
const save = useCallback16(
|
|
3901
4441
|
(next) => {
|
|
3902
4442
|
setPosition(next);
|
|
3903
4443
|
const currentStorage = storageRef.current;
|
|
3904
4444
|
if (currentStorage) {
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
try {
|
|
3908
|
-
currentStorage.setItem(key, serializeReadingPosition(next));
|
|
3909
|
-
} catch {
|
|
3910
|
-
}
|
|
4445
|
+
pending.schedule(() => {
|
|
4446
|
+
currentStorage.setItem(key, serializeReadingPosition(next));
|
|
3911
4447
|
}, throttleMs);
|
|
3912
4448
|
}
|
|
3913
4449
|
onChangeRef.current?.(next);
|
|
3914
4450
|
},
|
|
3915
|
-
[key, throttleMs]
|
|
4451
|
+
[key, throttleMs, pending]
|
|
3916
4452
|
);
|
|
3917
4453
|
const clear = useCallback16(() => {
|
|
3918
4454
|
setPosition(null);
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
if (currentStorage) {
|
|
3922
|
-
try {
|
|
3923
|
-
currentStorage.removeItem(key);
|
|
3924
|
-
} catch {
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
4455
|
+
pending.cancel();
|
|
4456
|
+
removeStorage(storageRef.current, key);
|
|
3927
4457
|
onChangeRef.current?.(null);
|
|
3928
|
-
}, [key]);
|
|
4458
|
+
}, [key, pending]);
|
|
3929
4459
|
return { position, save, clear };
|
|
3930
4460
|
}
|
|
3931
4461
|
|