@libraz/mejiro-react 0.9.1 → 0.9.2
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 +3 -1
- package/dist/index.d.ts +40 -12
- package/dist/index.js +535 -433
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -60,27 +60,29 @@ function MejiroChapterNav({
|
|
|
60
60
|
/* @__PURE__ */ jsx2("ol", { className: "mejiro-reader-chapter-list", children: epub.chapters.map((ch, i) => {
|
|
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
|
-
const chapterKey = `${title}-${ch.paragraphs.map((p) => p.text).join("|").slice(0, 120)}`;
|
|
64
63
|
const headings = ch.paragraphs.flatMap(
|
|
65
64
|
(p, paragraph) => p.headingLevel && p.text.trim() && p.text.trim() !== title ? [{ paragraph, text: p.text }] : []
|
|
66
65
|
).slice(0, 3);
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
return (
|
|
67
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: chapter titles and text are not guaranteed unique
|
|
68
|
+
/* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
|
|
69
|
+
"button",
|
|
70
|
+
{
|
|
71
|
+
type: "button",
|
|
72
|
+
className: `mejiro-reader-chapter-card${i === chapter ? " is-active" : ""}`,
|
|
73
|
+
"aria-current": i === chapter ? "true" : void 0,
|
|
74
|
+
onClick: () => onChange(i),
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-number", children: String(i + 1).padStart(2, "0") }),
|
|
77
|
+
/* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
|
|
78
|
+
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
|
|
79
|
+
preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
|
|
80
|
+
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)) })
|
|
81
|
+
] })
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
) }, i)
|
|
85
|
+
);
|
|
84
86
|
}) })
|
|
85
87
|
] });
|
|
86
88
|
}
|
|
@@ -158,6 +160,7 @@ function MejiroDropZone({
|
|
|
158
160
|
hidden: true,
|
|
159
161
|
onChange: (e) => {
|
|
160
162
|
const file = e.target.files?.[0];
|
|
163
|
+
e.target.value = "";
|
|
161
164
|
if (file && isValid(file)) onFile(file);
|
|
162
165
|
}
|
|
163
166
|
}
|
|
@@ -173,7 +176,7 @@ import {
|
|
|
173
176
|
EditableEpub,
|
|
174
177
|
exportEditableEpub
|
|
175
178
|
} from "@libraz/mejiro/epub";
|
|
176
|
-
import { useCallback as useCallback7, useEffect as useEffect9, useMemo as
|
|
179
|
+
import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef12, useState as useState9 } from "react";
|
|
177
180
|
|
|
178
181
|
// src/errors.ts
|
|
179
182
|
import { assertEpubInputSize } from "@libraz/mejiro/epub";
|
|
@@ -208,8 +211,8 @@ import {
|
|
|
208
211
|
useEffect as useEffect8,
|
|
209
212
|
useImperativeHandle,
|
|
210
213
|
useLayoutEffect as useLayoutEffect4,
|
|
211
|
-
useMemo as
|
|
212
|
-
useRef as
|
|
214
|
+
useMemo as useMemo5,
|
|
215
|
+
useRef as useRef11,
|
|
213
216
|
useState as useState8
|
|
214
217
|
} from "react";
|
|
215
218
|
|
|
@@ -324,10 +327,7 @@ function paragraphScaleStyle(scale) {
|
|
|
324
327
|
function MejiroPage({ page, className, style }) {
|
|
325
328
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
326
329
|
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
|
-
);
|
|
330
|
+
const paraClass = paragraphClassName(paragraph.kind, paragraph.headingLevel);
|
|
331
331
|
return (
|
|
332
332
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
333
333
|
/* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
@@ -468,6 +468,8 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
468
468
|
spreadIdxRef.current = spreadIdx;
|
|
469
469
|
const onUpdateRef = useRef2(options.onUpdate);
|
|
470
470
|
onUpdateRef.current = options.onUpdate;
|
|
471
|
+
const marginRef = useRef2(margin);
|
|
472
|
+
marginRef.current = margin;
|
|
471
473
|
const activeDragCleanupsRef = useRef2(/* @__PURE__ */ new Set());
|
|
472
474
|
useEffect(
|
|
473
475
|
() => () => {
|
|
@@ -476,15 +478,12 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
476
478
|
},
|
|
477
479
|
[]
|
|
478
480
|
);
|
|
479
|
-
const syncSpreads = useCallback(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
},
|
|
486
|
-
[margin]
|
|
487
|
-
);
|
|
481
|
+
const syncSpreads = useCallback((spreads) => {
|
|
482
|
+
const lo = layoutRef.current;
|
|
483
|
+
if (!lo) return;
|
|
484
|
+
applyImages(lo, imagesRef.current, spreads, marginRef.current);
|
|
485
|
+
for (const si of spreads) onUpdateRef.current?.(lo.getSpread(si));
|
|
486
|
+
}, []);
|
|
488
487
|
const appliedRef = useRef2({
|
|
489
488
|
layout: null,
|
|
490
489
|
margin
|
|
@@ -1061,11 +1060,13 @@ function MejiroSpread({
|
|
|
1061
1060
|
const selectionRef = useRef5(null);
|
|
1062
1061
|
const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
|
|
1063
1062
|
const gestureRef = useRef5(null);
|
|
1063
|
+
const swipedRef = useRef5(false);
|
|
1064
1064
|
const gestureEnabled = onSwipe != null || onSurfaceTap != null;
|
|
1065
1065
|
const SWIPE_THRESHOLD = 40;
|
|
1066
1066
|
const TAP_MOVE_THRESHOLD = 8;
|
|
1067
1067
|
function handleGesturePointerDown(e) {
|
|
1068
1068
|
if (!gestureEnabled) return;
|
|
1069
|
+
swipedRef.current = false;
|
|
1069
1070
|
if (!isPrimaryPointerPress2(e)) return;
|
|
1070
1071
|
if (e.target?.closest(OVERLAY_SELECTOR)) return;
|
|
1071
1072
|
gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
|
|
@@ -1080,6 +1081,7 @@ function MejiroSpread({
|
|
|
1080
1081
|
const ax = Math.abs(dx);
|
|
1081
1082
|
const ay = Math.abs(dy);
|
|
1082
1083
|
if (ax >= SWIPE_THRESHOLD && ax >= ay * 1.4) {
|
|
1084
|
+
swipedRef.current = true;
|
|
1083
1085
|
onSwipe?.(dx < 0 ? "next" : "prev");
|
|
1084
1086
|
return;
|
|
1085
1087
|
}
|
|
@@ -1089,6 +1091,12 @@ function MejiroSpread({
|
|
|
1089
1091
|
onSurfaceTap?.();
|
|
1090
1092
|
}
|
|
1091
1093
|
}
|
|
1094
|
+
function suppressSwipeClick(e) {
|
|
1095
|
+
if (!swipedRef.current) return;
|
|
1096
|
+
swipedRef.current = false;
|
|
1097
|
+
e.preventDefault();
|
|
1098
|
+
e.stopPropagation();
|
|
1099
|
+
}
|
|
1092
1100
|
function resolvePointer(e) {
|
|
1093
1101
|
if (!anchorAtCoord) return null;
|
|
1094
1102
|
const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
|
|
@@ -1207,6 +1215,7 @@ function MejiroSpread({
|
|
|
1207
1215
|
onPointerUp: useCombined ? combinedPointerUp : void 0,
|
|
1208
1216
|
onPointerCancel: useCombined ? combinedPointerCancel : void 0,
|
|
1209
1217
|
onLostPointerCapture: selectionEnabled ? endSelection : void 0,
|
|
1218
|
+
onClickCapture: gestureEnabled ? suppressSwipeClick : void 0,
|
|
1210
1219
|
children: [
|
|
1211
1220
|
renderPage(singlePage ? singleSide : "right"),
|
|
1212
1221
|
!singlePage && renderPage("left"),
|
|
@@ -1366,6 +1375,9 @@ function createDraftChangeTracker(chapters, key) {
|
|
|
1366
1375
|
}
|
|
1367
1376
|
|
|
1368
1377
|
// src/useChapterLayout.ts
|
|
1378
|
+
import { useMemo as useMemo4, useRef as useRef7 } from "react";
|
|
1379
|
+
|
|
1380
|
+
// src/useSurfaceLayout.ts
|
|
1369
1381
|
import {
|
|
1370
1382
|
useCallback as useCallback2,
|
|
1371
1383
|
useEffect as useEffect4,
|
|
@@ -1373,18 +1385,18 @@ import {
|
|
|
1373
1385
|
useRef as useRef6,
|
|
1374
1386
|
useState as useState4
|
|
1375
1387
|
} from "react";
|
|
1376
|
-
function
|
|
1377
|
-
const enableResize = options
|
|
1378
|
-
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
1388
|
+
function useSurfaceLayout(source, surface, options) {
|
|
1389
|
+
const { enableResize, resizeDebounce } = options;
|
|
1379
1390
|
const [layout, setLayout] = useState4(null);
|
|
1380
1391
|
const [pageWidth, setPageWidth] = useState4(0);
|
|
1381
1392
|
const [pageHeight, setPageHeight] = useState4(0);
|
|
1382
1393
|
const [contentHeight, setContentHeight] = useState4(0);
|
|
1383
1394
|
const [elapsedMs, setElapsedMs] = useState4(0);
|
|
1384
1395
|
const layoutRef = useRef6(null);
|
|
1396
|
+
layoutRef.current = layout;
|
|
1385
1397
|
const requestIdRef = useRef6(0);
|
|
1386
1398
|
const pendingRestore = useRef6(null);
|
|
1387
|
-
|
|
1399
|
+
const measuredBoxRef = useRef6(null);
|
|
1388
1400
|
const optionsRef = useRef6(options);
|
|
1389
1401
|
optionsRef.current = options;
|
|
1390
1402
|
const recompute = useCallback2(
|
|
@@ -1392,8 +1404,9 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1392
1404
|
const blank = opts.blank ?? true;
|
|
1393
1405
|
const o = optionsRef.current;
|
|
1394
1406
|
const requestId = ++requestIdRef.current;
|
|
1395
|
-
const
|
|
1396
|
-
if (!(
|
|
1407
|
+
const el = surface.current;
|
|
1408
|
+
if (!(source && el)) {
|
|
1409
|
+
measuredBoxRef.current = null;
|
|
1397
1410
|
setLayout(null);
|
|
1398
1411
|
setPageWidth(0);
|
|
1399
1412
|
setPageHeight(0);
|
|
@@ -1401,22 +1414,36 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1401
1414
|
setElapsedMs(0);
|
|
1402
1415
|
return;
|
|
1403
1416
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
+
try {
|
|
1418
|
+
const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
|
|
1419
|
+
pendingRestore.current = captured;
|
|
1420
|
+
if (blank) setLayout(null);
|
|
1421
|
+
measuredBoxRef.current = { width: el.clientWidth, height: el.clientHeight };
|
|
1422
|
+
const dims = source.measure(el);
|
|
1423
|
+
setPageWidth(dims.pageWidth);
|
|
1424
|
+
setPageHeight(dims.pageHeight);
|
|
1425
|
+
setContentHeight(dims.contentHeight);
|
|
1426
|
+
const t0 = performance.now();
|
|
1427
|
+
const next = await source.layout();
|
|
1428
|
+
if (requestId !== requestIdRef.current) return;
|
|
1429
|
+
layoutRef.current = next;
|
|
1430
|
+
setLayout(next);
|
|
1431
|
+
setElapsedMs(performance.now() - t0);
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
if (requestId !== requestIdRef.current) return;
|
|
1434
|
+
const handler = optionsRef.current.onError;
|
|
1435
|
+
if (!handler) throw err;
|
|
1436
|
+
handler(toError(err));
|
|
1437
|
+
}
|
|
1417
1438
|
},
|
|
1418
|
-
[
|
|
1439
|
+
[source, surface]
|
|
1419
1440
|
);
|
|
1441
|
+
const recomputeRef = useRef6(recompute);
|
|
1442
|
+
recomputeRef.current = recompute;
|
|
1443
|
+
const trigger = useCallback2((opts) => {
|
|
1444
|
+
recomputeRef.current(opts).catch(() => {
|
|
1445
|
+
});
|
|
1446
|
+
}, []);
|
|
1420
1447
|
useLayoutEffect2(() => {
|
|
1421
1448
|
requestIdRef.current++;
|
|
1422
1449
|
pendingRestore.current = null;
|
|
@@ -1427,7 +1454,8 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1427
1454
|
setElapsedMs(0);
|
|
1428
1455
|
}, [recompute]);
|
|
1429
1456
|
useEffect4(() => {
|
|
1430
|
-
|
|
1457
|
+
recompute().catch(() => {
|
|
1458
|
+
});
|
|
1431
1459
|
return () => {
|
|
1432
1460
|
requestIdRef.current++;
|
|
1433
1461
|
};
|
|
@@ -1454,18 +1482,23 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1454
1482
|
const scheduleReflow = (immediate) => {
|
|
1455
1483
|
clear();
|
|
1456
1484
|
if (immediate) {
|
|
1457
|
-
|
|
1485
|
+
trigger({ blank: false });
|
|
1458
1486
|
return;
|
|
1459
1487
|
}
|
|
1460
1488
|
timer = setTimeout(() => {
|
|
1461
1489
|
timer = null;
|
|
1462
|
-
|
|
1490
|
+
trigger({ blank: false });
|
|
1463
1491
|
}, resizeDebounce);
|
|
1464
1492
|
};
|
|
1465
1493
|
if (typeof ResizeObserver !== "undefined") {
|
|
1466
1494
|
const observer = new ResizeObserver(() => {
|
|
1467
1495
|
const immediate = !observed;
|
|
1468
1496
|
observed = true;
|
|
1497
|
+
const box = measuredBoxRef.current;
|
|
1498
|
+
if (box && box.width === el.clientWidth && box.height === el.clientHeight) {
|
|
1499
|
+
clear();
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1469
1502
|
scheduleReflow(immediate);
|
|
1470
1503
|
});
|
|
1471
1504
|
observer.observe(el);
|
|
@@ -1480,35 +1513,46 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1480
1513
|
window.removeEventListener("resize", onWindowResize);
|
|
1481
1514
|
clear();
|
|
1482
1515
|
};
|
|
1483
|
-
}, [surface, enableResize, resizeDebounce,
|
|
1484
|
-
return {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1516
|
+
}, [surface, enableResize, resizeDebounce, trigger]);
|
|
1517
|
+
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/useChapterLayout.ts
|
|
1521
|
+
function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
1522
|
+
const optionsRef = useRef7(options);
|
|
1523
|
+
optionsRef.current = options;
|
|
1524
|
+
const source = useMemo4(() => {
|
|
1525
|
+
const chapter = epub?.chapters[chapterIndex];
|
|
1526
|
+
if (!chapter) return null;
|
|
1527
|
+
return {
|
|
1528
|
+
measure: (el) => book.computePageSize(el, optionsRef.current.pageGeometry?.()),
|
|
1529
|
+
layout: () => book.layoutChapter(chapter)
|
|
1530
|
+
};
|
|
1531
|
+
}, [book, epub, chapterIndex]);
|
|
1532
|
+
return useSurfaceLayout(source, surface, {
|
|
1533
|
+
...options,
|
|
1534
|
+
enableResize: options.enableResize ?? true,
|
|
1535
|
+
resizeDebounce: options.resizeDebounce ?? 120
|
|
1536
|
+
});
|
|
1493
1537
|
}
|
|
1494
1538
|
|
|
1495
1539
|
// src/useEpub.ts
|
|
1496
1540
|
import { parseEpub } from "@libraz/mejiro/epub";
|
|
1497
|
-
import { useCallback as useCallback3, useEffect as useEffect5, useRef as
|
|
1541
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef8, useState as useState5 } from "react";
|
|
1498
1542
|
function useEpub(options = {}) {
|
|
1499
1543
|
const [epub, setEpub] = useState5(null);
|
|
1500
1544
|
const [loading, setLoading] = useState5(false);
|
|
1501
1545
|
const [error, setError] = useState5(null);
|
|
1502
|
-
const requestIdRef =
|
|
1503
|
-
const onLoadRef =
|
|
1546
|
+
const requestIdRef = useRef8(0);
|
|
1547
|
+
const onLoadRef = useRef8(options.onLoad);
|
|
1504
1548
|
onLoadRef.current = options.onLoad;
|
|
1505
|
-
const onErrorRef =
|
|
1549
|
+
const onErrorRef = useRef8(options.onError);
|
|
1506
1550
|
onErrorRef.current = options.onError;
|
|
1507
|
-
const fetchOptionsRef =
|
|
1551
|
+
const fetchOptionsRef = useRef8(options.fetchOptions);
|
|
1508
1552
|
fetchOptionsRef.current = options.fetchOptions;
|
|
1509
|
-
const fetchEpubRef =
|
|
1553
|
+
const fetchEpubRef = useRef8(options.fetchEpub);
|
|
1510
1554
|
fetchEpubRef.current = options.fetchEpub;
|
|
1511
|
-
const limitsRef =
|
|
1555
|
+
const limitsRef = useRef8(options.limits);
|
|
1512
1556
|
limitsRef.current = options.limits;
|
|
1513
1557
|
const load = useCallback3(async (read) => {
|
|
1514
1558
|
const requestId = ++requestIdRef.current;
|
|
@@ -1544,8 +1588,21 @@ function useEpub(options = {}) {
|
|
|
1544
1588
|
);
|
|
1545
1589
|
const defaultUrl = options.defaultUrl;
|
|
1546
1590
|
useEffect5(() => {
|
|
1547
|
-
if (defaultUrl)
|
|
1591
|
+
if (!defaultUrl) return;
|
|
1592
|
+
void loadUrl(defaultUrl);
|
|
1593
|
+
const requestId = requestIdRef.current;
|
|
1594
|
+
return () => {
|
|
1595
|
+
if (requestIdRef.current !== requestId) return;
|
|
1596
|
+
requestIdRef.current++;
|
|
1597
|
+
setLoading(false);
|
|
1598
|
+
};
|
|
1548
1599
|
}, [defaultUrl, loadUrl]);
|
|
1600
|
+
useEffect5(
|
|
1601
|
+
() => () => {
|
|
1602
|
+
requestIdRef.current++;
|
|
1603
|
+
},
|
|
1604
|
+
[]
|
|
1605
|
+
);
|
|
1549
1606
|
const replaceEpub = useCallback3((book) => {
|
|
1550
1607
|
requestIdRef.current++;
|
|
1551
1608
|
setLoading(false);
|
|
@@ -1557,26 +1614,35 @@ function useEpub(options = {}) {
|
|
|
1557
1614
|
|
|
1558
1615
|
// src/useMejiroBook.ts
|
|
1559
1616
|
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1560
|
-
import { useCallback as useCallback4, useEffect as useEffect6, useRef as
|
|
1617
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState6 } from "react";
|
|
1561
1618
|
function useMejiroBook(initial, options = {}) {
|
|
1562
|
-
const bookRef =
|
|
1619
|
+
const bookRef = useRef9(null);
|
|
1563
1620
|
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1564
1621
|
const [snapshot, setLocal] = useState6(initial);
|
|
1565
|
-
const optionsRef =
|
|
1622
|
+
const optionsRef = useRef9(options);
|
|
1566
1623
|
optionsRef.current = options;
|
|
1567
|
-
const pendingRef =
|
|
1624
|
+
const pendingRef = useRef9(null);
|
|
1625
|
+
const flushGenerationRef = useRef9(0);
|
|
1568
1626
|
const flush = useCallback4(() => {
|
|
1569
1627
|
const pending = pendingRef.current;
|
|
1570
1628
|
if (!pending) return;
|
|
1571
1629
|
pendingRef.current = null;
|
|
1572
1630
|
if (pending.timer) clearTimeout(pending.timer);
|
|
1573
1631
|
const { waiters } = pending;
|
|
1632
|
+
const generation = ++flushGenerationRef.current;
|
|
1574
1633
|
const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
|
|
1634
|
+
const syncSnapshot = () => {
|
|
1635
|
+
const book = bookRef.current;
|
|
1636
|
+
if (!book || generation !== flushGenerationRef.current) return;
|
|
1637
|
+
setLocal((prev) => ({ ...prev, ...book.getOptions(), ...pendingRef.current?.patch }));
|
|
1638
|
+
};
|
|
1575
1639
|
void applied.then(
|
|
1576
1640
|
() => {
|
|
1641
|
+
syncSnapshot();
|
|
1577
1642
|
for (const waiter of waiters) waiter.resolve();
|
|
1578
1643
|
},
|
|
1579
1644
|
(err) => {
|
|
1645
|
+
syncSnapshot();
|
|
1580
1646
|
const error = toError(err);
|
|
1581
1647
|
const handler = optionsRef.current.onError;
|
|
1582
1648
|
if (handler) {
|
|
@@ -1627,7 +1693,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1627
1693
|
}
|
|
1628
1694
|
|
|
1629
1695
|
// src/useSpread.ts
|
|
1630
|
-
import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as
|
|
1696
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
|
|
1631
1697
|
function ignoreNavigationKey(e) {
|
|
1632
1698
|
if (e.defaultPrevented) return true;
|
|
1633
1699
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
|
|
@@ -1657,7 +1723,7 @@ function useSpread(layout, options = {}) {
|
|
|
1657
1723
|
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1658
1724
|
const turnDuration = options.turnDuration ?? 180;
|
|
1659
1725
|
const single = options.single ?? false;
|
|
1660
|
-
const onChangeRef =
|
|
1726
|
+
const onChangeRef = useRef10(options.onChange);
|
|
1661
1727
|
onChangeRef.current = options.onChange;
|
|
1662
1728
|
const [spreadIdx, setSpreadIdx] = useState7(0);
|
|
1663
1729
|
const [indexSingle, setIndexSingle] = useState7(single);
|
|
@@ -1667,15 +1733,16 @@ function useSpread(layout, options = {}) {
|
|
|
1667
1733
|
}
|
|
1668
1734
|
const [spread, setSpread] = useState7(null);
|
|
1669
1735
|
const [turning, setTurning] = useState7(false);
|
|
1670
|
-
const layoutRef =
|
|
1736
|
+
const layoutRef = useRef10(null);
|
|
1671
1737
|
layoutRef.current = layout;
|
|
1672
|
-
const spreadIdxRef =
|
|
1738
|
+
const spreadIdxRef = useRef10(0);
|
|
1673
1739
|
spreadIdxRef.current = spreadIdx;
|
|
1674
|
-
const singleRef =
|
|
1740
|
+
const singleRef = useRef10(single);
|
|
1675
1741
|
singleRef.current = single;
|
|
1676
|
-
const turnTimerRef =
|
|
1677
|
-
const
|
|
1678
|
-
const
|
|
1742
|
+
const turnTimerRef = useRef10(null);
|
|
1743
|
+
const turnTargetRef = useRef10(null);
|
|
1744
|
+
const layoutGenerationRef = useRef10(0);
|
|
1745
|
+
const lastEmittedRef = useRef10(null);
|
|
1679
1746
|
const totalPages = spread?.totalPages ?? 0;
|
|
1680
1747
|
const totalSpreads = navigationCount(totalPages, single);
|
|
1681
1748
|
const firstPage = firstPageOf(spreadIdx, single);
|
|
@@ -1695,6 +1762,7 @@ function useSpread(layout, options = {}) {
|
|
|
1695
1762
|
clearTimeout(turnTimerRef.current);
|
|
1696
1763
|
turnTimerRef.current = null;
|
|
1697
1764
|
}
|
|
1765
|
+
turnTargetRef.current = null;
|
|
1698
1766
|
setTurning(false);
|
|
1699
1767
|
setSpreadIdx(0);
|
|
1700
1768
|
if (!layout) {
|
|
@@ -1716,28 +1784,44 @@ function useSpread(layout, options = {}) {
|
|
|
1716
1784
|
}
|
|
1717
1785
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
1718
1786
|
}, [layout, spreadIdx, layoutSpreadIdx]);
|
|
1787
|
+
const navigationTarget = useCallback5(() => {
|
|
1788
|
+
const pending = turnTargetRef.current;
|
|
1789
|
+
if (!pending) return spreadIdxRef.current;
|
|
1790
|
+
return indexOfPageIn(firstPageOf(pending.index, pending.single), singleRef.current);
|
|
1791
|
+
}, []);
|
|
1719
1792
|
const goTo = useCallback5(
|
|
1720
1793
|
(index) => {
|
|
1721
1794
|
if (!layoutRef.current) return;
|
|
1722
1795
|
const targetSingle = singleRef.current;
|
|
1723
1796
|
const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
|
|
1724
1797
|
const target = Math.max(0, Math.min(max, index));
|
|
1798
|
+
if (turnTimerRef.current) {
|
|
1799
|
+
if (target === navigationTarget()) return;
|
|
1800
|
+
clearTimeout(turnTimerRef.current);
|
|
1801
|
+
turnTimerRef.current = null;
|
|
1802
|
+
const landed = navigationTarget();
|
|
1803
|
+
turnTargetRef.current = null;
|
|
1804
|
+
spreadIdxRef.current = landed;
|
|
1805
|
+
setSpreadIdx(landed);
|
|
1806
|
+
setTurning(false);
|
|
1807
|
+
}
|
|
1725
1808
|
if (target === spreadIdxRef.current) return;
|
|
1726
1809
|
if (turnDuration > 0) {
|
|
1727
1810
|
const generation = layoutGenerationRef.current;
|
|
1728
|
-
|
|
1811
|
+
turnTargetRef.current = { index: target, single: targetSingle };
|
|
1729
1812
|
setTurning(true);
|
|
1730
1813
|
turnTimerRef.current = setTimeout(() => {
|
|
1731
1814
|
if (generation !== layoutGenerationRef.current) return;
|
|
1732
|
-
setSpreadIdx(
|
|
1815
|
+
setSpreadIdx(navigationTarget());
|
|
1733
1816
|
setTurning(false);
|
|
1817
|
+
turnTargetRef.current = null;
|
|
1734
1818
|
turnTimerRef.current = null;
|
|
1735
1819
|
}, turnDuration);
|
|
1736
1820
|
} else {
|
|
1737
1821
|
setSpreadIdx(target);
|
|
1738
1822
|
}
|
|
1739
1823
|
},
|
|
1740
|
-
[turnDuration]
|
|
1824
|
+
[turnDuration, navigationTarget]
|
|
1741
1825
|
);
|
|
1742
1826
|
const setSpreadIndex = useCallback5((index) => {
|
|
1743
1827
|
if (!layoutRef.current) return;
|
|
@@ -1745,6 +1829,7 @@ function useSpread(layout, options = {}) {
|
|
|
1745
1829
|
clearTimeout(turnTimerRef.current);
|
|
1746
1830
|
turnTimerRef.current = null;
|
|
1747
1831
|
}
|
|
1832
|
+
turnTargetRef.current = null;
|
|
1748
1833
|
setTurning(false);
|
|
1749
1834
|
const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
|
|
1750
1835
|
setSpreadIdx(Math.max(0, Math.min(max, index)));
|
|
@@ -1757,8 +1842,8 @@ function useSpread(layout, options = {}) {
|
|
|
1757
1842
|
},
|
|
1758
1843
|
[layout, single]
|
|
1759
1844
|
);
|
|
1760
|
-
const next = useCallback5(() => goTo(
|
|
1761
|
-
const prev = useCallback5(() => goTo(
|
|
1845
|
+
const next = useCallback5(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
|
|
1846
|
+
const prev = useCallback5(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
|
|
1762
1847
|
useEffect7(() => {
|
|
1763
1848
|
if (!enableKeyboard) return;
|
|
1764
1849
|
const onKey = (e) => {
|
|
@@ -1823,10 +1908,16 @@ function scrollTargetPage(spread, userPage) {
|
|
|
1823
1908
|
return userPage != null && spread.indexOfPage(userPage) === spread.spreadIdx ? userPage : spread.firstPage;
|
|
1824
1909
|
}
|
|
1825
1910
|
function createLifecycleTracker(sink) {
|
|
1911
|
+
let lastBook = null;
|
|
1826
1912
|
let last = null;
|
|
1827
1913
|
let dwell = null;
|
|
1828
|
-
return (chapter, spreadIdx, view, controlledSpreadIdx) => {
|
|
1914
|
+
return (book, chapter, spreadIdx, view, controlledSpreadIdx) => {
|
|
1829
1915
|
if (!view) return;
|
|
1916
|
+
if (book !== lastBook) {
|
|
1917
|
+
lastBook = book;
|
|
1918
|
+
last = null;
|
|
1919
|
+
dwell = null;
|
|
1920
|
+
}
|
|
1830
1921
|
const totalSpreads = view.total;
|
|
1831
1922
|
if (controlledSpreadIdx != null) {
|
|
1832
1923
|
const target = Math.max(0, Math.min(totalSpreads - 1, controlledSpreadIdx));
|
|
@@ -1844,6 +1935,49 @@ function createLifecycleTracker(sink) {
|
|
|
1844
1935
|
if (spreadIdx === totalSpreads - 1) sink.chapterFinished(chapter);
|
|
1845
1936
|
};
|
|
1846
1937
|
}
|
|
1938
|
+
function clampChapter(index, book) {
|
|
1939
|
+
if (!book) return index;
|
|
1940
|
+
return Math.max(0, Math.min(book.chapters.length - 1, index));
|
|
1941
|
+
}
|
|
1942
|
+
function createAnchorResolver(nav) {
|
|
1943
|
+
let pending = null;
|
|
1944
|
+
const settle = () => {
|
|
1945
|
+
const settled = pending;
|
|
1946
|
+
pending = null;
|
|
1947
|
+
settled?.resolve();
|
|
1948
|
+
};
|
|
1949
|
+
const update = (target) => {
|
|
1950
|
+
const current = pending;
|
|
1951
|
+
if (!(current && target.book)) return;
|
|
1952
|
+
const { anchor } = current;
|
|
1953
|
+
if (!(anchor.chapter >= 0 && anchor.chapter < target.book.chapters.length)) {
|
|
1954
|
+
settle();
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (anchor.chapter !== target.chapter) {
|
|
1958
|
+
if (current.chapterRequested) return;
|
|
1959
|
+
current.chapterRequested = true;
|
|
1960
|
+
nav.goToChapter(anchor.chapter);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (!target.layout) return;
|
|
1964
|
+
const loc = target.layout.locateAnchor({
|
|
1965
|
+
paragraph: anchor.paragraph,
|
|
1966
|
+
charIndex: anchor.charIndex
|
|
1967
|
+
});
|
|
1968
|
+
if (loc) nav.goToPage(loc.pageIdx);
|
|
1969
|
+
settle();
|
|
1970
|
+
};
|
|
1971
|
+
return {
|
|
1972
|
+
request: (anchor, target) => new Promise((resolve) => {
|
|
1973
|
+
settle();
|
|
1974
|
+
pending = { anchor, resolve, chapterRequested: false };
|
|
1975
|
+
update(target);
|
|
1976
|
+
}),
|
|
1977
|
+
update,
|
|
1978
|
+
dispose: settle
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1847
1981
|
function MejiroReaderInner(props, ref) {
|
|
1848
1982
|
const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
|
|
1849
1983
|
const dialectProp = "dialect" in props ? props.dialect : void 0;
|
|
@@ -1890,12 +2024,11 @@ function MejiroReaderInner(props, ref) {
|
|
|
1890
2024
|
onPageRead,
|
|
1891
2025
|
onChapterCompleted
|
|
1892
2026
|
} = props;
|
|
1893
|
-
const surfaceRef =
|
|
1894
|
-
const fileRef =
|
|
2027
|
+
const surfaceRef = useRef11(null);
|
|
2028
|
+
const fileRef = useRef11(null);
|
|
1895
2029
|
const [settingsOpen, setSettingsOpen] = useState8(false);
|
|
1896
2030
|
const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
|
|
1897
2031
|
const chapterIsUncontrolled = chapterProp == null;
|
|
1898
|
-
const chapter = chapterProp ?? chapterState;
|
|
1899
2032
|
const [chromeHidden, setChromeHidden] = useState8(false);
|
|
1900
2033
|
const [userScroll, setUserScroll] = useState8(
|
|
1901
2034
|
null
|
|
@@ -1924,7 +2057,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1924
2057
|
return () => observer.disconnect();
|
|
1925
2058
|
}, [spreadMode]);
|
|
1926
2059
|
const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
|
|
1927
|
-
const resolvedGeometry =
|
|
2060
|
+
const resolvedGeometry = useMemo5(() => {
|
|
1928
2061
|
const columns = effectiveSingle ? 1 : 2;
|
|
1929
2062
|
const geometry = pageGeometryProp ?? {};
|
|
1930
2063
|
if (fit !== "width") return mergeDefined({ columns }, geometry);
|
|
@@ -1940,29 +2073,38 @@ function MejiroReaderInner(props, ref) {
|
|
|
1940
2073
|
geometry
|
|
1941
2074
|
);
|
|
1942
2075
|
}, [fit, pageGeometryProp, effectiveSingle]);
|
|
1943
|
-
const surfaceAspect =
|
|
2076
|
+
const surfaceAspect = useMemo5(() => {
|
|
1944
2077
|
const columns = effectiveSingle ? 1 : 2;
|
|
1945
2078
|
const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
|
|
1946
2079
|
return `${columns} / ${aspect}`;
|
|
1947
2080
|
}, [effectiveSingle, resolvedGeometry]);
|
|
1948
|
-
const resolvedOptions =
|
|
2081
|
+
const resolvedOptions = useMemo5(
|
|
1949
2082
|
() => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
|
|
1950
2083
|
[optionsProp]
|
|
1951
2084
|
);
|
|
1952
|
-
const onErrorRef =
|
|
2085
|
+
const onErrorRef = useRef11(onError);
|
|
1953
2086
|
onErrorRef.current = onError;
|
|
1954
2087
|
const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
|
|
1955
2088
|
const {
|
|
1956
2089
|
book,
|
|
1957
2090
|
options: bookOptions,
|
|
1958
|
-
setOptions
|
|
2091
|
+
setOptions: applyBookOptions
|
|
1959
2092
|
} = useMejiroBook(resolvedOptions, {
|
|
1960
2093
|
debounceMs: OPTIONS_DEBOUNCE_MS,
|
|
1961
2094
|
onError: reportError
|
|
1962
2095
|
});
|
|
2096
|
+
const lastApplyRef = useRef11(Promise.resolve());
|
|
2097
|
+
const setOptions = useCallback6(
|
|
2098
|
+
(partial) => {
|
|
2099
|
+
const applied = applyBookOptions(partial);
|
|
2100
|
+
lastApplyRef.current = applied;
|
|
2101
|
+
return applied;
|
|
2102
|
+
},
|
|
2103
|
+
[applyBookOptions]
|
|
2104
|
+
);
|
|
1963
2105
|
const optionsPropKey = JSON.stringify(resolvedOptions);
|
|
1964
|
-
const syncedOptionsPropKeyRef =
|
|
1965
|
-
const resolvedOptionsRef =
|
|
2106
|
+
const syncedOptionsPropKeyRef = useRef11(optionsPropKey);
|
|
2107
|
+
const resolvedOptionsRef = useRef11(resolvedOptions);
|
|
1966
2108
|
resolvedOptionsRef.current = resolvedOptions;
|
|
1967
2109
|
useEffect8(() => {
|
|
1968
2110
|
if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
|
|
@@ -1970,7 +2112,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1970
2112
|
void setOptions(resolvedOptionsRef.current);
|
|
1971
2113
|
}, [optionsPropKey, setOptions]);
|
|
1972
2114
|
const manuscriptSourceKey = manuscriptKey(manuscriptProp);
|
|
1973
|
-
const synthesizedEpub =
|
|
2115
|
+
const synthesizedEpub = useMemo5(() => {
|
|
1974
2116
|
if (manuscriptSourceKey === void 0) return null;
|
|
1975
2117
|
const chapters = JSON.parse(manuscriptSourceKey);
|
|
1976
2118
|
return manuscriptToEpubBook(chapters, { dialect: dialectProp });
|
|
@@ -1991,15 +2133,19 @@ function MejiroReaderInner(props, ref) {
|
|
|
1991
2133
|
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
1992
2134
|
const isManuscriptSource = manuscriptProp !== void 0;
|
|
1993
2135
|
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
2136
|
+
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
|
|
2137
|
+
const bookKey = controlled ? sourceKey : epubCtx.epub;
|
|
2138
|
+
const chapter = clampChapter(chapterProp ?? chapterState, e);
|
|
2139
|
+
const filePicker = enableDropZone && !controlled;
|
|
1994
2140
|
useEffect8(() => {
|
|
1995
2141
|
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
1996
2142
|
}, [epubCtx.error]);
|
|
1997
|
-
const pageGeometryRef =
|
|
2143
|
+
const pageGeometryRef = useRef11(resolvedGeometry);
|
|
1998
2144
|
pageGeometryRef.current = resolvedGeometry;
|
|
1999
|
-
const spreadAnchorRef =
|
|
2000
|
-
const spreadIdxPropRef =
|
|
2145
|
+
const spreadAnchorRef = useRef11(() => null);
|
|
2146
|
+
const spreadIdxPropRef = useRef11(spreadIdxProp);
|
|
2001
2147
|
spreadIdxPropRef.current = spreadIdxProp;
|
|
2002
|
-
const visibleAnchorRef =
|
|
2148
|
+
const visibleAnchorRef = useRef11(
|
|
2003
2149
|
null
|
|
2004
2150
|
);
|
|
2005
2151
|
const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
|
|
@@ -2012,10 +2158,13 @@ function MejiroReaderInner(props, ref) {
|
|
|
2012
2158
|
const visible = visibleAnchorRef.current;
|
|
2013
2159
|
if (visible?.layout === layout2) return visible.anchor;
|
|
2014
2160
|
return spreadAnchorRef.current();
|
|
2015
|
-
}
|
|
2161
|
+
},
|
|
2162
|
+
onError: reportError
|
|
2016
2163
|
});
|
|
2017
|
-
const
|
|
2164
|
+
const reportedSpreadIdxRef = useRef11(null);
|
|
2165
|
+
const spreadChangedRef = useRef11(void 0);
|
|
2018
2166
|
spreadChangedRef.current = (i) => {
|
|
2167
|
+
reportedSpreadIdxRef.current = i;
|
|
2019
2168
|
onSpreadChange?.(i);
|
|
2020
2169
|
onSpreadIdxChange?.(i);
|
|
2021
2170
|
};
|
|
@@ -2025,9 +2174,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2025
2174
|
onChange: (i) => spreadChangedRef.current?.(i)
|
|
2026
2175
|
});
|
|
2027
2176
|
spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
2028
|
-
const indexOfPageRef =
|
|
2177
|
+
const indexOfPageRef = useRef11(spreadCtx.indexOfPage);
|
|
2029
2178
|
indexOfPageRef.current = spreadCtx.indexOfPage;
|
|
2030
|
-
const anchorSingleRef =
|
|
2179
|
+
const anchorSingleRef = useRef11(effectiveSingle);
|
|
2031
2180
|
useLayoutEffect4(() => {
|
|
2032
2181
|
const l = layoutCtx.layout;
|
|
2033
2182
|
const flipped = anchorSingleRef.current !== effectiveSingle;
|
|
@@ -2035,7 +2184,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2035
2184
|
if (flipped && l && visibleAnchorRef.current?.layout === l) return;
|
|
2036
2185
|
visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2037
2186
|
}, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
|
|
2038
|
-
const setSpreadRef =
|
|
2187
|
+
const setSpreadRef = useRef11(spreadCtx.setSpread);
|
|
2039
2188
|
setSpreadRef.current = spreadCtx.setSpread;
|
|
2040
2189
|
useLayoutEffect4(() => {
|
|
2041
2190
|
const anchor = layoutCtx.pendingRestore.current;
|
|
@@ -2046,10 +2195,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2046
2195
|
visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
|
|
2047
2196
|
}, [layoutCtx.layout]);
|
|
2048
2197
|
const optionsKey = reflowOptionsKey(bookOptions);
|
|
2049
|
-
const optionsKeyRef =
|
|
2050
|
-
const
|
|
2051
|
-
bookOptionsRef.current = bookOptions;
|
|
2052
|
-
const recomputeRef = useRef10(layoutCtx.recompute);
|
|
2198
|
+
const optionsKeyRef = useRef11(optionsKey);
|
|
2199
|
+
const recomputeRef = useRef11(layoutCtx.recompute);
|
|
2053
2200
|
recomputeRef.current = layoutCtx.recompute;
|
|
2054
2201
|
useEffect8(() => {
|
|
2055
2202
|
if (optionsKeyRef.current === optionsKey) return;
|
|
@@ -2057,7 +2204,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2057
2204
|
const timer = setTimeout(() => {
|
|
2058
2205
|
void (async () => {
|
|
2059
2206
|
try {
|
|
2060
|
-
await
|
|
2207
|
+
await lastApplyRef.current;
|
|
2061
2208
|
await recomputeRef.current({ blank: false });
|
|
2062
2209
|
} catch (err) {
|
|
2063
2210
|
reportError(toError(err));
|
|
@@ -2065,9 +2212,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2065
2212
|
})();
|
|
2066
2213
|
}, OPTIONS_DEBOUNCE_MS);
|
|
2067
2214
|
return () => clearTimeout(timer);
|
|
2068
|
-
}, [optionsKey,
|
|
2215
|
+
}, [optionsKey, reportError]);
|
|
2069
2216
|
const geometryKey = JSON.stringify(resolvedGeometry ?? null);
|
|
2070
|
-
const geometryKeyRef =
|
|
2217
|
+
const geometryKeyRef = useRef11(geometryKey);
|
|
2071
2218
|
useEffect8(() => {
|
|
2072
2219
|
if (geometryKeyRef.current === geometryKey) return;
|
|
2073
2220
|
geometryKeyRef.current = geometryKey;
|
|
@@ -2077,7 +2224,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2077
2224
|
defaultX: IMAGE_DEFAULT_X,
|
|
2078
2225
|
onUpdate: () => spreadCtx.refresh()
|
|
2079
2226
|
});
|
|
2080
|
-
const annotationRects =
|
|
2227
|
+
const annotationRects = useMemo5(() => {
|
|
2081
2228
|
if (!(annotations && layoutCtx.layout)) return [];
|
|
2082
2229
|
const result = [];
|
|
2083
2230
|
for (const annotation of annotations) {
|
|
@@ -2090,13 +2237,12 @@ function MejiroReaderInner(props, ref) {
|
|
|
2090
2237
|
}
|
|
2091
2238
|
return result;
|
|
2092
2239
|
}, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
|
|
2093
|
-
const clearImagesRef =
|
|
2240
|
+
const clearImagesRef = useRef11(imageCtx.clearImages);
|
|
2094
2241
|
clearImagesRef.current = imageCtx.clearImages;
|
|
2095
|
-
const onLoadRef =
|
|
2242
|
+
const onLoadRef = useRef11(onLoad);
|
|
2096
2243
|
onLoadRef.current = onLoad;
|
|
2097
|
-
const eRef =
|
|
2244
|
+
const eRef = useRef11(e);
|
|
2098
2245
|
eRef.current = e;
|
|
2099
|
-
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
|
|
2100
2246
|
useEffect8(() => {
|
|
2101
2247
|
if (!controlled) return;
|
|
2102
2248
|
if (chapterIsUncontrolled) setChapterState(0);
|
|
@@ -2104,16 +2250,19 @@ function MejiroReaderInner(props, ref) {
|
|
|
2104
2250
|
book.clearCache();
|
|
2105
2251
|
if (eRef.current) onLoadRef.current?.(eRef.current);
|
|
2106
2252
|
}, [controlled, sourceKey, book, chapterIsUncontrolled]);
|
|
2107
|
-
const imagesChapterRef =
|
|
2253
|
+
const imagesChapterRef = useRef11(chapter);
|
|
2108
2254
|
useEffect8(() => {
|
|
2109
2255
|
if (imagesChapterRef.current === chapter) return;
|
|
2110
2256
|
imagesChapterRef.current = chapter;
|
|
2111
2257
|
clearImagesRef.current();
|
|
2112
2258
|
}, [chapter]);
|
|
2113
|
-
const spreadGoToRef =
|
|
2259
|
+
const spreadGoToRef = useRef11(spreadCtx.goTo);
|
|
2114
2260
|
spreadGoToRef.current = spreadCtx.goTo;
|
|
2115
|
-
const appliedSpreadIdxPropRef =
|
|
2261
|
+
const appliedSpreadIdxPropRef = useRef11(spreadIdxProp);
|
|
2262
|
+
const [hostTurn, setHostTurn] = useState8(0);
|
|
2116
2263
|
useEffect8(() => {
|
|
2264
|
+
const reported = reportedSpreadIdxRef.current;
|
|
2265
|
+
reportedSpreadIdxRef.current = null;
|
|
2117
2266
|
if (spreadIdxProp == null) {
|
|
2118
2267
|
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
2119
2268
|
return;
|
|
@@ -2122,22 +2271,26 @@ function MejiroReaderInner(props, ref) {
|
|
|
2122
2271
|
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
2123
2272
|
if (spreadCtx.spreadIdx === spreadIdxProp) return;
|
|
2124
2273
|
if (propChanged) spreadGoToRef.current(spreadIdxProp);
|
|
2274
|
+
else if (reported === spreadCtx.spreadIdx) setHostTurn((n) => n + 1);
|
|
2125
2275
|
else setSpreadRef.current(spreadIdxProp);
|
|
2126
|
-
}, [spreadIdxProp, spreadCtx.spreadIdx]);
|
|
2276
|
+
}, [spreadIdxProp, spreadCtx.spreadIdx, hostTurn]);
|
|
2127
2277
|
useLayoutEffect4(() => {
|
|
2128
2278
|
const next = spreadIdxPropRef.current;
|
|
2129
2279
|
if (next == null || !layoutCtx.layout) return;
|
|
2130
2280
|
setSpreadRef.current(next);
|
|
2131
2281
|
}, [layoutCtx.layout]);
|
|
2132
2282
|
const onChapter = useCallback6(
|
|
2133
|
-
(
|
|
2283
|
+
(index) => {
|
|
2284
|
+
const i = clampChapter(index, e);
|
|
2134
2285
|
if (i === chapter) return;
|
|
2135
2286
|
if (chapterProp == null) setChapterState(i);
|
|
2136
2287
|
onChapterChange?.(i);
|
|
2137
2288
|
},
|
|
2138
|
-
[chapter, chapterProp, onChapterChange]
|
|
2289
|
+
[chapter, chapterProp, onChapterChange, e]
|
|
2139
2290
|
);
|
|
2140
|
-
const
|
|
2291
|
+
const onChapterRef = useRef11(onChapter);
|
|
2292
|
+
onChapterRef.current = onChapter;
|
|
2293
|
+
const listenersRef = useRef11(/* @__PURE__ */ new Map());
|
|
2141
2294
|
const emit = useCallback6(
|
|
2142
2295
|
(event, payload) => {
|
|
2143
2296
|
const set = listenersRef.current.get(event);
|
|
@@ -2146,44 +2299,33 @@ function MejiroReaderInner(props, ref) {
|
|
|
2146
2299
|
},
|
|
2147
2300
|
[]
|
|
2148
2301
|
);
|
|
2149
|
-
const
|
|
2150
|
-
const layoutOwnerRef = useRef10({
|
|
2302
|
+
const layoutOwnerRef = useRef11({
|
|
2151
2303
|
layout: null,
|
|
2152
|
-
chapter
|
|
2304
|
+
chapter,
|
|
2305
|
+
book: bookKey
|
|
2153
2306
|
});
|
|
2154
2307
|
if (layoutOwnerRef.current.layout !== layoutCtx.layout) {
|
|
2155
|
-
layoutOwnerRef.current = { layout: layoutCtx.layout, chapter };
|
|
2308
|
+
layoutOwnerRef.current = { layout: layoutCtx.layout, chapter, book: bookKey };
|
|
2156
2309
|
}
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
if (!loc) return;
|
|
2168
|
-
pendingAnchorRef.current = null;
|
|
2169
|
-
spreadGoToRef.current(indexOfPageRef.current(loc.pageIdx));
|
|
2170
|
-
pending.resolve();
|
|
2171
|
-
}, [chapter, layout]);
|
|
2310
|
+
const owner = layoutOwnerRef.current;
|
|
2311
|
+
const layout = owner.chapter === chapter && owner.book === bookKey ? layoutCtx.layout : null;
|
|
2312
|
+
const anchorTarget = { book: epubCtx.loading ? null : e, chapter, layout };
|
|
2313
|
+
const anchorTargetRef = useRef11(anchorTarget);
|
|
2314
|
+
anchorTargetRef.current = anchorTarget;
|
|
2315
|
+
const anchorResolverRef = useRef11(null);
|
|
2316
|
+
anchorResolverRef.current ??= createAnchorResolver({
|
|
2317
|
+
goToChapter: (i) => onChapterRef.current(i),
|
|
2318
|
+
goToPage: (pageIdx) => spreadGoToRef.current(indexOfPageRef.current(pageIdx))
|
|
2319
|
+
});
|
|
2172
2320
|
useEffect8(() => {
|
|
2173
|
-
|
|
2174
|
-
}, [
|
|
2175
|
-
useEffect8(
|
|
2176
|
-
|
|
2177
|
-
pendingAnchorRef.current?.resolve();
|
|
2178
|
-
pendingAnchorRef.current = null;
|
|
2179
|
-
},
|
|
2180
|
-
[]
|
|
2181
|
-
);
|
|
2182
|
-
const onPageReadRef = useRef10(onPageRead);
|
|
2321
|
+
anchorResolverRef.current?.update(anchorTargetRef.current);
|
|
2322
|
+
}, [anchorTarget.book, chapter, layout]);
|
|
2323
|
+
useEffect8(() => () => anchorResolverRef.current?.dispose(), []);
|
|
2324
|
+
const onPageReadRef = useRef11(onPageRead);
|
|
2183
2325
|
onPageReadRef.current = onPageRead;
|
|
2184
|
-
const onChapterCompletedRef =
|
|
2326
|
+
const onChapterCompletedRef = useRef11(onChapterCompleted);
|
|
2185
2327
|
onChapterCompletedRef.current = onChapterCompleted;
|
|
2186
|
-
const trackLifecycleRef =
|
|
2328
|
+
const trackLifecycleRef = useRef11(null);
|
|
2187
2329
|
trackLifecycleRef.current ??= createLifecycleTracker({
|
|
2188
2330
|
spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
|
|
2189
2331
|
chapterFinished: (ch) => {
|
|
@@ -2194,8 +2336,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2194
2336
|
});
|
|
2195
2337
|
useEffect8(() => {
|
|
2196
2338
|
const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2197
|
-
trackLifecycleRef.current?.(chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
|
|
2339
|
+
trackLifecycleRef.current?.(bookKey, chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
|
|
2198
2340
|
}, [
|
|
2341
|
+
bookKey,
|
|
2199
2342
|
chapter,
|
|
2200
2343
|
spreadCtx.spreadIdx,
|
|
2201
2344
|
spreadCtx.totalSpreads,
|
|
@@ -2203,7 +2346,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2203
2346
|
layout,
|
|
2204
2347
|
spreadIdxProp
|
|
2205
2348
|
]);
|
|
2206
|
-
const prevTurningRef =
|
|
2349
|
+
const prevTurningRef = useRef11(false);
|
|
2207
2350
|
useEffect8(() => {
|
|
2208
2351
|
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
2209
2352
|
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
@@ -2225,12 +2368,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2225
2368
|
totalPages: spreadCtx.totalPages,
|
|
2226
2369
|
totalSpreads: spreadCtx.totalSpreads
|
|
2227
2370
|
}),
|
|
2228
|
-
goToAnchor: (anchor) =>
|
|
2229
|
-
pendingAnchorRef.current?.resolve();
|
|
2230
|
-
pendingAnchorRef.current = { anchor, resolve };
|
|
2231
|
-
if (anchor.chapter !== chapter) onChapter(anchor.chapter);
|
|
2232
|
-
tryApplyPendingAnchor();
|
|
2233
|
-
}),
|
|
2371
|
+
goToAnchor: (anchor) => anchorResolverRef.current?.request(anchor, anchorTargetRef.current) ?? Promise.resolve(),
|
|
2234
2372
|
getAnchor: () => {
|
|
2235
2373
|
if (!layout) return null;
|
|
2236
2374
|
const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
@@ -2269,8 +2407,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2269
2407
|
onChapter,
|
|
2270
2408
|
chapter,
|
|
2271
2409
|
layout,
|
|
2272
|
-
setOptions
|
|
2273
|
-
tryApplyPendingAnchor
|
|
2410
|
+
setOptions
|
|
2274
2411
|
]
|
|
2275
2412
|
);
|
|
2276
2413
|
const editable = {
|
|
@@ -2293,7 +2430,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2293
2430
|
const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
|
|
2294
2431
|
const themeName = typeof theme === "string" ? theme : theme.name;
|
|
2295
2432
|
const themeOverride = typeof theme === "string" ? void 0 : theme.override;
|
|
2296
|
-
const themeStyle =
|
|
2433
|
+
const themeStyle = useMemo5(() => {
|
|
2297
2434
|
const pad = pageGeometryProp?.padding;
|
|
2298
2435
|
const styleVars = {};
|
|
2299
2436
|
if (pad?.x != null) styleVars["--mejiro-page-pad-x"] = `${pad.x}px`;
|
|
@@ -2325,7 +2462,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2325
2462
|
fontLabel
|
|
2326
2463
|
}
|
|
2327
2464
|
),
|
|
2328
|
-
|
|
2465
|
+
filePicker && /* @__PURE__ */ jsx13(
|
|
2329
2466
|
"button",
|
|
2330
2467
|
{
|
|
2331
2468
|
type: "button",
|
|
@@ -2393,7 +2530,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2393
2530
|
/* @__PURE__ */ jsxs8("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
|
|
2394
2531
|
showChapterPanel && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
|
|
2395
2532
|
/* @__PURE__ */ jsxs8("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
|
|
2396
|
-
!(e || epubCtx.loading) &&
|
|
2533
|
+
!(e || epubCtx.loading) && filePicker && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
|
|
2397
2534
|
epubCtx.loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
|
|
2398
2535
|
!layoutReady && fallback && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-fallback", children: fallback }),
|
|
2399
2536
|
layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx13(
|
|
@@ -2476,6 +2613,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2476
2613
|
hidden: true,
|
|
2477
2614
|
onChange: (ev) => {
|
|
2478
2615
|
const file = ev.target.files?.[0];
|
|
2616
|
+
ev.target.value = "";
|
|
2479
2617
|
if (file) void epubCtx.loadFile(file);
|
|
2480
2618
|
}
|
|
2481
2619
|
}
|
|
@@ -2493,6 +2631,55 @@ MejiroReader.displayName = "MejiroReader";
|
|
|
2493
2631
|
|
|
2494
2632
|
// src/MejiroEditor.tsx
|
|
2495
2633
|
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2634
|
+
function clampRubyRange(start, end, length) {
|
|
2635
|
+
if (length <= 0) return null;
|
|
2636
|
+
const from = Math.max(0, Math.min(start, length - 1));
|
|
2637
|
+
return { start: from, end: Math.max(from + 1, Math.min(end, length)) };
|
|
2638
|
+
}
|
|
2639
|
+
function commitPendingEdit(editor, chapterIndex, paragraphIndex, text, ruby) {
|
|
2640
|
+
const paragraphs = editor.book.chapters[chapterIndex]?.paragraphs;
|
|
2641
|
+
if (!paragraphs?.[paragraphIndex]) return false;
|
|
2642
|
+
const textChanged = text !== paragraphs[paragraphIndex].text;
|
|
2643
|
+
const rubyText = ruby?.rubyText.trim();
|
|
2644
|
+
const range = ruby ? clampRubyRange(ruby.start, ruby.end, [...text].length) : null;
|
|
2645
|
+
if (!(rubyText && range)) {
|
|
2646
|
+
if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
|
|
2647
|
+
return textChanged;
|
|
2648
|
+
}
|
|
2649
|
+
editor.transaction(() => {
|
|
2650
|
+
if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
|
|
2651
|
+
const committed = editor.book.chapters[chapterIndex].paragraphs[paragraphIndex];
|
|
2652
|
+
const newRuby = {
|
|
2653
|
+
kind: "ruby",
|
|
2654
|
+
startIndex: range.start,
|
|
2655
|
+
endIndex: range.end,
|
|
2656
|
+
rubyText,
|
|
2657
|
+
type: range.end - range.start === 1 ? "mono" : "group"
|
|
2658
|
+
};
|
|
2659
|
+
const inlineAnnotations = [
|
|
2660
|
+
...committed.inlineAnnotations.filter(
|
|
2661
|
+
(ann) => ann.kind !== "ruby" || ann.endIndex <= range.start || ann.startIndex >= range.end
|
|
2662
|
+
),
|
|
2663
|
+
newRuby
|
|
2664
|
+
].sort((a, b) => a.startIndex - b.startIndex);
|
|
2665
|
+
editor.updateParagraph(chapterIndex, paragraphIndex, { inlineAnnotations });
|
|
2666
|
+
});
|
|
2667
|
+
return true;
|
|
2668
|
+
}
|
|
2669
|
+
function downloadEpub(buffer, filename) {
|
|
2670
|
+
const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
|
|
2671
|
+
const a = document.createElement("a");
|
|
2672
|
+
a.href = url;
|
|
2673
|
+
a.download = filename;
|
|
2674
|
+
a.hidden = true;
|
|
2675
|
+
document.body.append(a);
|
|
2676
|
+
try {
|
|
2677
|
+
a.click();
|
|
2678
|
+
} finally {
|
|
2679
|
+
a.remove();
|
|
2680
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2496
2683
|
function utf16ToCodepoint(text, utf16Offset) {
|
|
2497
2684
|
let cp = 0;
|
|
2498
2685
|
let i = 0;
|
|
@@ -2544,9 +2731,9 @@ function MejiroEditor({
|
|
|
2544
2731
|
onError
|
|
2545
2732
|
}) {
|
|
2546
2733
|
const messages = useI18n();
|
|
2547
|
-
const imageInputRef =
|
|
2548
|
-
const textareaRef =
|
|
2549
|
-
const readerRef =
|
|
2734
|
+
const imageInputRef = useRef12(null);
|
|
2735
|
+
const textareaRef = useRef12(null);
|
|
2736
|
+
const readerRef = useRef12(null);
|
|
2550
2737
|
const [editor, setEditor] = useState9(null);
|
|
2551
2738
|
const [loading, setLoading] = useState9(false);
|
|
2552
2739
|
const [error, setError] = useState9(null);
|
|
@@ -2556,16 +2743,17 @@ function MejiroEditor({
|
|
|
2556
2743
|
const [rubyStart, setRubyStart] = useState9(0);
|
|
2557
2744
|
const [rubyEnd, setRubyEnd] = useState9(1);
|
|
2558
2745
|
const [rubyText, setRubyText] = useState9("");
|
|
2559
|
-
const loadRequestIdRef =
|
|
2560
|
-
const onLoadRef =
|
|
2561
|
-
const onErrorRef =
|
|
2562
|
-
const limitsRef =
|
|
2746
|
+
const loadRequestIdRef = useRef12(0);
|
|
2747
|
+
const onLoadRef = useRef12(onLoad);
|
|
2748
|
+
const onErrorRef = useRef12(onError);
|
|
2749
|
+
const limitsRef = useRef12(limits);
|
|
2563
2750
|
limitsRef.current = limits;
|
|
2564
2751
|
const book = editor?.book ?? null;
|
|
2565
2752
|
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
2566
2753
|
const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
|
|
2567
|
-
const
|
|
2568
|
-
const
|
|
2754
|
+
const sceneBreak = chapter ? paragraphBlock(chapter, selection.paragraph)?.paragraphKind === "sceneBreak" : false;
|
|
2755
|
+
const rubyRange = clampRubyRange(rubyStart, rubyEnd, [...text].length);
|
|
2756
|
+
const previewBook = useMemo6(() => {
|
|
2569
2757
|
void revision;
|
|
2570
2758
|
return book ? cloneEditableEpubBook(book) : null;
|
|
2571
2759
|
}, [book, revision]);
|
|
@@ -2632,7 +2820,7 @@ function MejiroEditor({
|
|
|
2632
2820
|
cancelled = true;
|
|
2633
2821
|
};
|
|
2634
2822
|
}, [epubUrl, loadBufferForRequest, reportError]);
|
|
2635
|
-
const syncedRef =
|
|
2823
|
+
const syncedRef = useRef12(
|
|
2636
2824
|
null
|
|
2637
2825
|
);
|
|
2638
2826
|
useEffect9(() => {
|
|
@@ -2659,7 +2847,7 @@ function MejiroEditor({
|
|
|
2659
2847
|
}, [previewBook, selection.chapter, selection.paragraph]);
|
|
2660
2848
|
function selectParagraph(chapterIndex, paragraphIndex) {
|
|
2661
2849
|
if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
|
|
2662
|
-
|
|
2850
|
+
commitEdit();
|
|
2663
2851
|
setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
|
|
2664
2852
|
}
|
|
2665
2853
|
function captureRubyRange() {
|
|
@@ -2672,41 +2860,22 @@ function MejiroEditor({
|
|
|
2672
2860
|
setRubyStart(start);
|
|
2673
2861
|
setRubyEnd(Math.max(start + 1, end));
|
|
2674
2862
|
}
|
|
2675
|
-
function
|
|
2863
|
+
function commitEdit(ruby) {
|
|
2676
2864
|
if (!editor) return;
|
|
2677
|
-
editor
|
|
2678
|
-
|
|
2865
|
+
if (commitPendingEdit(editor, selection.chapter, selection.paragraph, text, ruby)) {
|
|
2866
|
+
setRevision((value) => value + 1);
|
|
2867
|
+
}
|
|
2679
2868
|
}
|
|
2680
2869
|
function applyRuby() {
|
|
2681
|
-
if (!(
|
|
2682
|
-
|
|
2683
|
-
const start = Math.max(0, Math.min(rubyStart, len));
|
|
2684
|
-
const end = Math.max(start + 1, Math.min(rubyEnd, len));
|
|
2685
|
-
const newRuby = {
|
|
2686
|
-
kind: "ruby",
|
|
2687
|
-
startIndex: start,
|
|
2688
|
-
endIndex: end,
|
|
2689
|
-
rubyText: rubyText.trim(),
|
|
2690
|
-
type: end - start === 1 ? "mono" : "group"
|
|
2691
|
-
};
|
|
2692
|
-
const nextInline = [
|
|
2693
|
-
...paragraph.inlineAnnotations.filter(
|
|
2694
|
-
(ann) => ann.endIndex <= start || ann.startIndex >= end
|
|
2695
|
-
),
|
|
2696
|
-
newRuby
|
|
2697
|
-
].sort((a, b) => a.startIndex - b.startIndex);
|
|
2698
|
-
editor.updateParagraph(selection.chapter, selection.paragraph, {
|
|
2699
|
-
text,
|
|
2700
|
-
inlineAnnotations: nextInline
|
|
2701
|
-
});
|
|
2870
|
+
if (!(rubyRange && rubyText.trim())) return;
|
|
2871
|
+
commitEdit({ start: rubyStart, end: rubyEnd, rubyText });
|
|
2702
2872
|
setRubyText("");
|
|
2703
|
-
setRevision((value) => value + 1);
|
|
2704
2873
|
}
|
|
2705
2874
|
async function addImage(file) {
|
|
2706
2875
|
await withErrorReporting(async () => {
|
|
2707
2876
|
if (!(editor && chapter)) return;
|
|
2708
2877
|
const chapterIndex = selection.chapter;
|
|
2709
|
-
const afterBlockId =
|
|
2878
|
+
const afterBlockId = paragraphBlock(chapter, selection.paragraph)?.id;
|
|
2710
2879
|
const data = await file.arrayBuffer();
|
|
2711
2880
|
editor.addImage(chapterIndex, {
|
|
2712
2881
|
filename: file.name,
|
|
@@ -2721,6 +2890,7 @@ function MejiroEditor({
|
|
|
2721
2890
|
async function exportEpub() {
|
|
2722
2891
|
await withErrorReporting(async () => {
|
|
2723
2892
|
if (!editor) return;
|
|
2893
|
+
commitEdit();
|
|
2724
2894
|
const watermark = exportPolicy?.watermark;
|
|
2725
2895
|
const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
|
|
2726
2896
|
let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
|
|
@@ -2729,12 +2899,7 @@ function MejiroEditor({
|
|
|
2729
2899
|
onExport?.(buffer);
|
|
2730
2900
|
const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
|
|
2731
2901
|
if (!allowDownload) return;
|
|
2732
|
-
|
|
2733
|
-
const a = document.createElement("a");
|
|
2734
|
-
a.href = url;
|
|
2735
|
-
a.download = `${editor.title || "edited"}.epub`;
|
|
2736
|
-
a.click();
|
|
2737
|
-
URL.revokeObjectURL(url);
|
|
2902
|
+
downloadEpub(buffer, `${editor.title || "edited"}.epub`);
|
|
2738
2903
|
}, reportError);
|
|
2739
2904
|
}
|
|
2740
2905
|
return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
|
|
@@ -2788,19 +2953,29 @@ function MejiroEditor({
|
|
|
2788
2953
|
{
|
|
2789
2954
|
ref: textareaRef,
|
|
2790
2955
|
value: text,
|
|
2956
|
+
readOnly: sceneBreak,
|
|
2791
2957
|
onChange: (event) => setText(event.target.value),
|
|
2792
2958
|
onSelect: captureRubyRange
|
|
2793
2959
|
}
|
|
2794
2960
|
),
|
|
2795
|
-
/* @__PURE__ */ jsx14(
|
|
2961
|
+
/* @__PURE__ */ jsx14(
|
|
2962
|
+
"button",
|
|
2963
|
+
{
|
|
2964
|
+
type: "button",
|
|
2965
|
+
className: "mejiro-editor-primary",
|
|
2966
|
+
disabled: sceneBreak,
|
|
2967
|
+
onClick: () => commitEdit(),
|
|
2968
|
+
children: messages.editorApplyText
|
|
2969
|
+
}
|
|
2970
|
+
)
|
|
2796
2971
|
] }),
|
|
2797
2972
|
enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
|
|
2798
2973
|
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
|
|
2799
2974
|
/* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
|
|
2800
2975
|
/* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
|
|
2801
|
-
start:
|
|
2802
|
-
end:
|
|
2803
|
-
count:
|
|
2976
|
+
start: rubyRange?.start ?? 0,
|
|
2977
|
+
end: rubyRange?.end ?? 0,
|
|
2978
|
+
count: rubyRange ? rubyRange.end - rubyRange.start : 0
|
|
2804
2979
|
}) }),
|
|
2805
2980
|
/* @__PURE__ */ jsx14(
|
|
2806
2981
|
"input",
|
|
@@ -2815,7 +2990,7 @@ function MejiroEditor({
|
|
|
2815
2990
|
{
|
|
2816
2991
|
type: "button",
|
|
2817
2992
|
className: "mejiro-editor-primary",
|
|
2818
|
-
disabled:
|
|
2993
|
+
disabled: !(rubyRange && rubyText.trim()),
|
|
2819
2994
|
onClick: applyRuby,
|
|
2820
2995
|
children: messages.editorApplyRuby
|
|
2821
2996
|
}
|
|
@@ -2833,6 +3008,7 @@ function MejiroEditor({
|
|
|
2833
3008
|
hidden: true,
|
|
2834
3009
|
onChange: (event) => {
|
|
2835
3010
|
const file = event.target.files?.[0];
|
|
3011
|
+
event.target.value = "";
|
|
2836
3012
|
if (file) void addImage(file);
|
|
2837
3013
|
}
|
|
2838
3014
|
}
|
|
@@ -2851,11 +3027,11 @@ function MejiroEditor({
|
|
|
2851
3027
|
] })
|
|
2852
3028
|
] });
|
|
2853
3029
|
}
|
|
2854
|
-
function
|
|
3030
|
+
function paragraphBlock(chapter, paragraphIndex) {
|
|
2855
3031
|
let current = 0;
|
|
2856
3032
|
for (const block of chapter.blocks) {
|
|
2857
3033
|
if (block.kind !== "paragraph") continue;
|
|
2858
|
-
if (current === paragraphIndex) return block
|
|
3034
|
+
if (current === paragraphIndex) return block;
|
|
2859
3035
|
current++;
|
|
2860
3036
|
}
|
|
2861
3037
|
return void 0;
|
|
@@ -2863,15 +3039,15 @@ function paragraphBlockId(chapter, paragraphIndex) {
|
|
|
2863
3039
|
|
|
2864
3040
|
// src/MejiroManuscriptEditor.tsx
|
|
2865
3041
|
import { EpubProject } from "@libraz/mejiro/epub";
|
|
2866
|
-
import { useCallback as useCallback9, useEffect as useEffect11, useMemo as
|
|
3042
|
+
import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState12 } from "react";
|
|
2867
3043
|
|
|
2868
3044
|
// src/MejiroNotationHighlighter.tsx
|
|
2869
3045
|
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
2870
3046
|
import {
|
|
2871
3047
|
forwardRef as forwardRef2,
|
|
2872
3048
|
useImperativeHandle as useImperativeHandle2,
|
|
2873
|
-
useMemo as
|
|
2874
|
-
useRef as
|
|
3049
|
+
useMemo as useMemo7,
|
|
3050
|
+
useRef as useRef13,
|
|
2875
3051
|
useState as useState10
|
|
2876
3052
|
} from "react";
|
|
2877
3053
|
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
@@ -2888,12 +3064,13 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
2888
3064
|
onCompositionEnd,
|
|
2889
3065
|
...rest
|
|
2890
3066
|
}, ref) {
|
|
2891
|
-
const textareaRef =
|
|
2892
|
-
const overlayRef =
|
|
3067
|
+
const textareaRef = useRef13(null);
|
|
3068
|
+
const overlayRef = useRef13(null);
|
|
2893
3069
|
useImperativeHandle2(ref, () => textareaRef.current, []);
|
|
2894
3070
|
const [isComposing, setIsComposing] = useState10(false);
|
|
2895
|
-
const
|
|
2896
|
-
const
|
|
3071
|
+
const [composingValue, setComposingValue] = useState10("");
|
|
3072
|
+
const lastSegmentsRef = useRef13(null);
|
|
3073
|
+
const segments = useMemo7(() => {
|
|
2897
3074
|
if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
|
|
2898
3075
|
const next = buildSegments(value, dialect);
|
|
2899
3076
|
lastSegmentsRef.current = next;
|
|
@@ -2932,14 +3109,19 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
2932
3109
|
ref: textareaRef,
|
|
2933
3110
|
className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
|
|
2934
3111
|
style,
|
|
2935
|
-
value,
|
|
2936
|
-
onChange: (event) =>
|
|
3112
|
+
value: isComposing ? composingValue : value,
|
|
3113
|
+
onChange: (event) => {
|
|
3114
|
+
if (isComposing) setComposingValue(event.target.value);
|
|
3115
|
+
onChange(event.target.value);
|
|
3116
|
+
},
|
|
2937
3117
|
onCompositionStart: (event) => {
|
|
3118
|
+
setComposingValue(event.currentTarget.value);
|
|
2938
3119
|
setIsComposing(true);
|
|
2939
3120
|
onCompositionStart?.(event);
|
|
2940
3121
|
},
|
|
2941
3122
|
onCompositionEnd: (event) => {
|
|
2942
3123
|
setIsComposing(false);
|
|
3124
|
+
onChange(event.currentTarget.value);
|
|
2943
3125
|
onCompositionEnd?.(event);
|
|
2944
3126
|
},
|
|
2945
3127
|
onScroll: (event) => {
|
|
@@ -2981,7 +3163,7 @@ function buildSegments(text, dialect) {
|
|
|
2981
3163
|
}
|
|
2982
3164
|
|
|
2983
3165
|
// src/useManuscriptDraft.ts
|
|
2984
|
-
import { useCallback as useCallback8, useEffect as useEffect10, useRef as
|
|
3166
|
+
import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef14, useState as useState11 } from "react";
|
|
2985
3167
|
var DEFAULT_DELAY = 800;
|
|
2986
3168
|
function defaultChapter(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
|
|
2987
3169
|
const index = existing.length;
|
|
@@ -2996,46 +3178,63 @@ function useManuscriptDraft(options = {}) {
|
|
|
2996
3178
|
defaultChapterTitle,
|
|
2997
3179
|
defaultChapterBody
|
|
2998
3180
|
} = options;
|
|
2999
|
-
const titleForRef =
|
|
3181
|
+
const titleForRef = useRef14(defaultChapterTitle);
|
|
3000
3182
|
titleForRef.current = defaultChapterTitle;
|
|
3001
|
-
const bodyForRef =
|
|
3183
|
+
const bodyForRef = useRef14(defaultChapterBody);
|
|
3002
3184
|
bodyForRef.current = defaultChapterBody;
|
|
3003
3185
|
const [chapters, setChaptersState] = useState11(
|
|
3004
3186
|
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
|
|
3005
3187
|
);
|
|
3006
3188
|
const [selected, setSelectedState] = useState11(0);
|
|
3007
3189
|
const [autosaveError, setAutosaveError] = useState11(null);
|
|
3008
|
-
const saveRef =
|
|
3190
|
+
const saveRef = useRef14(onAutosave);
|
|
3009
3191
|
saveRef.current = onAutosave;
|
|
3010
|
-
const payloadRef =
|
|
3192
|
+
const payloadRef = useRef14(autosavePayload);
|
|
3011
3193
|
payloadRef.current = autosavePayload;
|
|
3012
|
-
const chaptersRef =
|
|
3194
|
+
const chaptersRef = useRef14(chapters);
|
|
3013
3195
|
chaptersRef.current = chapters;
|
|
3014
|
-
const selectedRef =
|
|
3196
|
+
const selectedRef = useRef14(selected);
|
|
3015
3197
|
selectedRef.current = selected;
|
|
3016
|
-
const trackerRef =
|
|
3198
|
+
const trackerRef = useRef14(null);
|
|
3017
3199
|
if (!trackerRef.current)
|
|
3018
3200
|
trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
|
|
3019
|
-
const dirtyRef =
|
|
3020
|
-
const mountedRef =
|
|
3021
|
-
const revisionRef =
|
|
3022
|
-
const
|
|
3201
|
+
const dirtyRef = useRef14(false);
|
|
3202
|
+
const mountedRef = useRef14(true);
|
|
3203
|
+
const revisionRef = useRef14(0);
|
|
3204
|
+
const inFlightRef = useRef14(false);
|
|
3205
|
+
const flushQueuedRef = useRef14(false);
|
|
3023
3206
|
const flushAutosave = useCallback8(() => {
|
|
3024
3207
|
const callback = saveRef.current;
|
|
3025
|
-
if (!(callback && dirtyRef.current)
|
|
3208
|
+
if (!(callback && dirtyRef.current)) return;
|
|
3209
|
+
if (inFlightRef.current) {
|
|
3210
|
+
flushQueuedRef.current = true;
|
|
3026
3211
|
return;
|
|
3027
3212
|
}
|
|
3028
3213
|
const revision = revisionRef.current;
|
|
3029
|
-
inFlightRevisionRef.current = revision;
|
|
3030
3214
|
const plain = snapshotChapters(chaptersRef.current);
|
|
3031
3215
|
const payload = payloadRef.current ? payloadRef.current(plain) : plain;
|
|
3032
|
-
|
|
3216
|
+
const markSaved = () => {
|
|
3033
3217
|
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
3034
|
-
}
|
|
3218
|
+
};
|
|
3219
|
+
let saved;
|
|
3220
|
+
try {
|
|
3221
|
+
saved = callback(payload);
|
|
3222
|
+
} catch (err) {
|
|
3223
|
+
saved = Promise.reject(err);
|
|
3224
|
+
}
|
|
3225
|
+
if (saved === void 0) {
|
|
3226
|
+
markSaved();
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
inFlightRef.current = true;
|
|
3230
|
+
void Promise.resolve(saved).then(markSaved).catch((err) => {
|
|
3035
3231
|
if (!mountedRef.current) return;
|
|
3036
3232
|
setAutosaveError(toError(err));
|
|
3037
3233
|
}).finally(() => {
|
|
3038
|
-
|
|
3234
|
+
inFlightRef.current = false;
|
|
3235
|
+
if (!flushQueuedRef.current) return;
|
|
3236
|
+
flushQueuedRef.current = false;
|
|
3237
|
+
flushAutosave();
|
|
3039
3238
|
});
|
|
3040
3239
|
}, []);
|
|
3041
3240
|
useEffect10(() => {
|
|
@@ -3153,14 +3352,6 @@ function useManuscriptDraft(options = {}) {
|
|
|
3153
3352
|
|
|
3154
3353
|
// src/MejiroManuscriptEditor.tsx
|
|
3155
3354
|
import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3156
|
-
function downloadEpub(buffer, title) {
|
|
3157
|
-
const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
|
|
3158
|
-
const a = document.createElement("a");
|
|
3159
|
-
a.href = url;
|
|
3160
|
-
a.download = `${title || "book"}.epub`;
|
|
3161
|
-
a.click();
|
|
3162
|
-
URL.revokeObjectURL(url);
|
|
3163
|
-
}
|
|
3164
3355
|
function coverExtension(mediaType) {
|
|
3165
3356
|
switch (mediaType) {
|
|
3166
3357
|
case "image/gif":
|
|
@@ -3207,8 +3398,8 @@ function MejiroManuscriptEditor({
|
|
|
3207
3398
|
panelSide = "right"
|
|
3208
3399
|
}) {
|
|
3209
3400
|
const messages = useI18n();
|
|
3210
|
-
const coverInputRef =
|
|
3211
|
-
const bodyTextareaRef =
|
|
3401
|
+
const coverInputRef = useRef15(null);
|
|
3402
|
+
const bodyTextareaRef = useRef15(null);
|
|
3212
3403
|
const titleControlled = onTitleChange !== void 0;
|
|
3213
3404
|
const authorControlled = onAuthorChange !== void 0;
|
|
3214
3405
|
const coverControlled = onCoverChange !== void 0;
|
|
@@ -3308,7 +3499,7 @@ function MejiroManuscriptEditor({
|
|
|
3308
3499
|
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
3309
3500
|
setExportError(null);
|
|
3310
3501
|
onExport?.(buffer);
|
|
3311
|
-
downloadEpub(buffer, title);
|
|
3502
|
+
downloadEpub(buffer, `${title || "book"}.epub`);
|
|
3312
3503
|
},
|
|
3313
3504
|
(error) => {
|
|
3314
3505
|
setExportError(error);
|
|
@@ -3327,7 +3518,7 @@ function MejiroManuscriptEditor({
|
|
|
3327
3518
|
onExport,
|
|
3328
3519
|
title
|
|
3329
3520
|
]);
|
|
3330
|
-
const previewManuscript =
|
|
3521
|
+
const previewManuscript = useMemo8(
|
|
3331
3522
|
() => chapters.map((chapter) => ({
|
|
3332
3523
|
id: chapter.id,
|
|
3333
3524
|
title: chapter.title || messages.untitled,
|
|
@@ -3441,7 +3632,7 @@ function MejiroManuscriptEditor({
|
|
|
3441
3632
|
onChange: (event) => patchChapter(selected, { title: event.target.value })
|
|
3442
3633
|
}
|
|
3443
3634
|
),
|
|
3444
|
-
/* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
|
|
3635
|
+
dialect === "mejiro" && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
|
|
3445
3636
|
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
|
|
3446
3637
|
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
|
|
3447
3638
|
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
|
|
@@ -3501,7 +3692,7 @@ function MejiroShelf({
|
|
|
3501
3692
|
}
|
|
3502
3693
|
|
|
3503
3694
|
// src/MejiroToc.tsx
|
|
3504
|
-
import { useMemo as
|
|
3695
|
+
import { useMemo as useMemo9, useState as useState13 } from "react";
|
|
3505
3696
|
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3506
3697
|
function buildEntries(epub, messages) {
|
|
3507
3698
|
return epub.chapters.map((ch, i) => {
|
|
@@ -3525,8 +3716,8 @@ function MejiroToc({
|
|
|
3525
3716
|
const resolvedTitle = title ?? messages.tocTitle;
|
|
3526
3717
|
const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
|
|
3527
3718
|
const [query, setQuery] = useState13("");
|
|
3528
|
-
const entries =
|
|
3529
|
-
const filtered =
|
|
3719
|
+
const entries = useMemo9(() => buildEntries(epub, messages), [epub, messages]);
|
|
3720
|
+
const filtered = useMemo9(() => {
|
|
3530
3721
|
if (!query) return entries;
|
|
3531
3722
|
const needle = query.toLowerCase();
|
|
3532
3723
|
return entries.filter(
|
|
@@ -3576,7 +3767,7 @@ import {
|
|
|
3576
3767
|
serializeAnnotations,
|
|
3577
3768
|
sortAnnotations
|
|
3578
3769
|
} from "@libraz/mejiro";
|
|
3579
|
-
import { useCallback as useCallback10, useEffect as useEffect12, useRef as
|
|
3770
|
+
import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState14 } from "react";
|
|
3580
3771
|
function parseSorted(raw) {
|
|
3581
3772
|
return sortAnnotations(parseAnnotations(raw));
|
|
3582
3773
|
}
|
|
@@ -3586,14 +3777,14 @@ function useAnnotations(options) {
|
|
|
3586
3777
|
const [annotations, setAnnotations] = useState14(
|
|
3587
3778
|
() => readStorage(storage, key, parseSorted, [])
|
|
3588
3779
|
);
|
|
3589
|
-
const storageRef =
|
|
3780
|
+
const storageRef = useRef16(storage);
|
|
3590
3781
|
storageRef.current = storage;
|
|
3591
|
-
const annotationsRef =
|
|
3782
|
+
const annotationsRef = useRef16(annotations);
|
|
3592
3783
|
const applyNext = useCallback10((next) => {
|
|
3593
3784
|
annotationsRef.current = next;
|
|
3594
3785
|
setAnnotations(next);
|
|
3595
3786
|
}, []);
|
|
3596
|
-
const pendingRef =
|
|
3787
|
+
const pendingRef = useRef16(null);
|
|
3597
3788
|
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
3598
3789
|
const pending = pendingRef.current;
|
|
3599
3790
|
useEffect12(() => flushOnPageHide(pending.flush), [pending]);
|
|
@@ -3601,7 +3792,7 @@ function useAnnotations(options) {
|
|
|
3601
3792
|
applyNext(readStorage(storageRef.current, key, parseSorted, []));
|
|
3602
3793
|
return pending.flush;
|
|
3603
3794
|
}, [key, applyNext, pending]);
|
|
3604
|
-
const onChangeRef =
|
|
3795
|
+
const onChangeRef = useRef16(options.onChange);
|
|
3605
3796
|
onChangeRef.current = options.onChange;
|
|
3606
3797
|
const commit = useCallback10(
|
|
3607
3798
|
(next) => {
|
|
@@ -3669,7 +3860,7 @@ import {
|
|
|
3669
3860
|
cloneEditableEpubBook as cloneEditableEpubBook2,
|
|
3670
3861
|
EditableEpub as EditableEpub2
|
|
3671
3862
|
} from "@libraz/mejiro/epub";
|
|
3672
|
-
import { useCallback as useCallback11, useEffect as useEffect13, useMemo as
|
|
3863
|
+
import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo10, useRef as useRef17, useState as useState15 } from "react";
|
|
3673
3864
|
function useEditableEpub(options = {}) {
|
|
3674
3865
|
const [editor, setEditor] = useState15(null);
|
|
3675
3866
|
const [loading, setLoading] = useState15(false);
|
|
@@ -3680,20 +3871,20 @@ function useEditableEpub(options = {}) {
|
|
|
3680
3871
|
chapter: 0,
|
|
3681
3872
|
paragraph: 0
|
|
3682
3873
|
});
|
|
3683
|
-
const requestIdRef =
|
|
3684
|
-
const onLoadRef =
|
|
3685
|
-
const onErrorRef =
|
|
3686
|
-
const onExportRef =
|
|
3687
|
-
const limitsRef =
|
|
3688
|
-
const assetResolverRef =
|
|
3874
|
+
const requestIdRef = useRef17(0);
|
|
3875
|
+
const onLoadRef = useRef17(options.onLoad);
|
|
3876
|
+
const onErrorRef = useRef17(options.onError);
|
|
3877
|
+
const onExportRef = useRef17(options.onExport);
|
|
3878
|
+
const limitsRef = useRef17(options.limits);
|
|
3879
|
+
const assetResolverRef = useRef17(options.assetResolver);
|
|
3689
3880
|
onLoadRef.current = options.onLoad;
|
|
3690
3881
|
onErrorRef.current = options.onError;
|
|
3691
3882
|
onExportRef.current = options.onExport;
|
|
3692
3883
|
limitsRef.current = options.limits;
|
|
3693
3884
|
assetResolverRef.current = options.assetResolver;
|
|
3694
|
-
const editorRef =
|
|
3885
|
+
const editorRef = useRef17(editor);
|
|
3695
3886
|
editorRef.current = editor;
|
|
3696
|
-
const selectionRef =
|
|
3887
|
+
const selectionRef = useRef17(selection);
|
|
3697
3888
|
selectionRef.current = selection;
|
|
3698
3889
|
const loadBufferWithRequest = useCallback11(
|
|
3699
3890
|
async (buffer, requestId) => {
|
|
@@ -3773,7 +3964,7 @@ function useEditableEpub(options = {}) {
|
|
|
3773
3964
|
}, [options.defaultUrl, loadUrl]);
|
|
3774
3965
|
const book = editor?.book ?? null;
|
|
3775
3966
|
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3776
|
-
const previewBook =
|
|
3967
|
+
const previewBook = useMemo10(() => {
|
|
3777
3968
|
void revision;
|
|
3778
3969
|
return book ? cloneEditableEpubBook2(book) : null;
|
|
3779
3970
|
}, [book, revision]);
|
|
@@ -3870,7 +4061,7 @@ import {
|
|
|
3870
4061
|
EpubProject as EpubProject2,
|
|
3871
4062
|
parseEpub as parseEpub2
|
|
3872
4063
|
} from "@libraz/mejiro/epub";
|
|
3873
|
-
import { useCallback as useCallback12, useEffect as useEffect14, useMemo as
|
|
4064
|
+
import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef18, useState as useState16 } from "react";
|
|
3874
4065
|
function useEpubProject(options = {}) {
|
|
3875
4066
|
const defaultTitle = options.defaultChapterTitle;
|
|
3876
4067
|
const defaultBody = options.defaultChapterBody;
|
|
@@ -3886,22 +4077,22 @@ function useEpubProject(options = {}) {
|
|
|
3886
4077
|
const [previewBook, setPreviewBook] = useState16(null);
|
|
3887
4078
|
const [previewError, setPreviewError] = useState16(null);
|
|
3888
4079
|
const [previewing, setPreviewing] = useState16(false);
|
|
3889
|
-
const previewRequestIdRef =
|
|
3890
|
-
const onPreviewRef =
|
|
3891
|
-
const onExportRef =
|
|
4080
|
+
const previewRequestIdRef = useRef18(0);
|
|
4081
|
+
const onPreviewRef = useRef18(options.onPreview);
|
|
4082
|
+
const onExportRef = useRef18(options.onExport);
|
|
3892
4083
|
onPreviewRef.current = options.onPreview;
|
|
3893
4084
|
onExportRef.current = options.onExport;
|
|
3894
|
-
const assetResolverRef =
|
|
4085
|
+
const assetResolverRef = useRef18(options.assetResolver);
|
|
3895
4086
|
assetResolverRef.current = options.assetResolver;
|
|
3896
|
-
const metadataRef =
|
|
4087
|
+
const metadataRef = useRef18(metadata);
|
|
3897
4088
|
metadataRef.current = metadata;
|
|
3898
|
-
const chaptersRef =
|
|
4089
|
+
const chaptersRef = useRef18(chapters);
|
|
3899
4090
|
chaptersRef.current = chapters;
|
|
3900
|
-
const selectedRef =
|
|
4091
|
+
const selectedRef = useRef18(selectedChapter);
|
|
3901
4092
|
selectedRef.current = selectedChapter;
|
|
3902
|
-
const coverRef =
|
|
4093
|
+
const coverRef = useRef18(cover);
|
|
3903
4094
|
coverRef.current = cover;
|
|
3904
|
-
const assetsRef =
|
|
4095
|
+
const assetsRef = useRef18(assets);
|
|
3905
4096
|
assetsRef.current = assets;
|
|
3906
4097
|
const buildProject = useCallback12(() => {
|
|
3907
4098
|
const currentCover = coverRef.current;
|
|
@@ -3956,7 +4147,7 @@ function useEpubProject(options = {}) {
|
|
|
3956
4147
|
}, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
|
|
3957
4148
|
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
3958
4149
|
const setMetadata = useCallback12((patch) => {
|
|
3959
|
-
const next =
|
|
4150
|
+
const next = mergeMetadata(metadataRef.current, patch);
|
|
3960
4151
|
metadataRef.current = next;
|
|
3961
4152
|
setMetadataState(next);
|
|
3962
4153
|
}, []);
|
|
@@ -4051,7 +4242,7 @@ function useEpubProject(options = {}) {
|
|
|
4051
4242
|
onExportRef.current?.(buffer);
|
|
4052
4243
|
return buffer;
|
|
4053
4244
|
}, [buildProject, exportOptions]);
|
|
4054
|
-
return
|
|
4245
|
+
return useMemo11(
|
|
4055
4246
|
() => ({
|
|
4056
4247
|
metadata,
|
|
4057
4248
|
chapters,
|
|
@@ -4098,6 +4289,11 @@ function useEpubProject(options = {}) {
|
|
|
4098
4289
|
]
|
|
4099
4290
|
);
|
|
4100
4291
|
}
|
|
4292
|
+
function mergeMetadata(current, patch) {
|
|
4293
|
+
const next = { ...current, ...patch };
|
|
4294
|
+
if (!next.identifier?.trim()) next.identifier = current.identifier;
|
|
4295
|
+
return next;
|
|
4296
|
+
}
|
|
4101
4297
|
function initialMetadata(patch = {}) {
|
|
4102
4298
|
const metadata = mergeDefined(
|
|
4103
4299
|
{ title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
|
|
@@ -4116,7 +4312,7 @@ import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointer
|
|
|
4116
4312
|
import {
|
|
4117
4313
|
useCallback as useCallback13,
|
|
4118
4314
|
useEffect as useEffect15,
|
|
4119
|
-
useRef as
|
|
4315
|
+
useRef as useRef19,
|
|
4120
4316
|
useState as useState17
|
|
4121
4317
|
} from "react";
|
|
4122
4318
|
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
@@ -4126,15 +4322,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4126
4322
|
const defY = options?.defaultY ?? 100;
|
|
4127
4323
|
const margin = options?.margin;
|
|
4128
4324
|
const [imageRect, setImageRect] = useState17(null);
|
|
4129
|
-
const rectRef =
|
|
4325
|
+
const rectRef = useRef19(imageRect);
|
|
4130
4326
|
rectRef.current = imageRect;
|
|
4131
|
-
const layoutRef =
|
|
4327
|
+
const layoutRef = useRef19(layout);
|
|
4132
4328
|
layoutRef.current = layout;
|
|
4133
|
-
const spreadRef =
|
|
4329
|
+
const spreadRef = useRef19(spreadIdx);
|
|
4134
4330
|
spreadRef.current = spreadIdx;
|
|
4135
|
-
const onUpdateRef =
|
|
4331
|
+
const onUpdateRef = useRef19(onUpdate);
|
|
4136
4332
|
onUpdateRef.current = onUpdate;
|
|
4137
|
-
const
|
|
4333
|
+
const marginRef = useRef19(margin);
|
|
4334
|
+
marginRef.current = margin;
|
|
4335
|
+
const activeDragCleanupsRef = useRef19(/* @__PURE__ */ new Set());
|
|
4138
4336
|
useEffect15(
|
|
4139
4337
|
() => () => {
|
|
4140
4338
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
@@ -4142,17 +4340,14 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4142
4340
|
},
|
|
4143
4341
|
[]
|
|
4144
4342
|
);
|
|
4145
|
-
const syncToLayout = useCallback13(
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
[margin]
|
|
4154
|
-
);
|
|
4155
|
-
const syncedRef = useRef18({ layout, spreadIdx, margin });
|
|
4343
|
+
const syncToLayout = useCallback13((rect) => {
|
|
4344
|
+
const lo = layoutRef.current;
|
|
4345
|
+
if (!lo) return;
|
|
4346
|
+
const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: marginRef.current }] : void 0;
|
|
4347
|
+
const spread = lo.syncImages(spreadRef.current, images);
|
|
4348
|
+
onUpdateRef.current(spread);
|
|
4349
|
+
}, []);
|
|
4350
|
+
const syncedRef = useRef19({ layout, spreadIdx, margin });
|
|
4156
4351
|
useEffect15(() => {
|
|
4157
4352
|
const prev = syncedRef.current;
|
|
4158
4353
|
syncedRef.current = { layout, spreadIdx, margin };
|
|
@@ -4165,16 +4360,21 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4165
4360
|
}, [layout, spreadIdx, margin, syncToLayout]);
|
|
4166
4361
|
const toggleImage = useCallback13(() => {
|
|
4167
4362
|
if (rectRef.current) {
|
|
4363
|
+
for (const cancel of [...activeDragCleanupsRef.current]) cancel();
|
|
4364
|
+
rectRef.current = null;
|
|
4168
4365
|
setImageRect(null);
|
|
4169
4366
|
syncToLayout(null);
|
|
4170
4367
|
} else {
|
|
4171
4368
|
const r = { x: defX, y: defY, w: defW, h: defH };
|
|
4369
|
+
rectRef.current = r;
|
|
4172
4370
|
setImageRect(r);
|
|
4173
4371
|
syncToLayout(r);
|
|
4174
4372
|
}
|
|
4175
4373
|
}, [defX, defY, defW, defH, syncToLayout]);
|
|
4176
4374
|
const applyRect = useCallback13(
|
|
4177
4375
|
(rect) => {
|
|
4376
|
+
if (rectRef.current === null) return;
|
|
4377
|
+
rectRef.current = rect;
|
|
4178
4378
|
setImageRect(rect);
|
|
4179
4379
|
syncToLayout(rect);
|
|
4180
4380
|
},
|
|
@@ -4235,7 +4435,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4235
4435
|
}
|
|
4236
4436
|
|
|
4237
4437
|
// src/useLibrary.ts
|
|
4238
|
-
import { useCallback as useCallback14, useEffect as useEffect16, useMemo as
|
|
4438
|
+
import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo12, useState as useState18 } from "react";
|
|
4239
4439
|
function resolveCurrentId(volumes, currentId, preferredId) {
|
|
4240
4440
|
if (volumes.length === 0) return null;
|
|
4241
4441
|
if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
|
|
@@ -4255,11 +4455,11 @@ function useLibrary(options) {
|
|
|
4255
4455
|
const volume = volumes.find((v) => v.id === resolvedId);
|
|
4256
4456
|
if (volume) onChange?.(volume);
|
|
4257
4457
|
}, [currentId, resolvedId, volumes, onChange]);
|
|
4258
|
-
const index =
|
|
4458
|
+
const index = useMemo12(
|
|
4259
4459
|
() => resolvedId == null ? -1 : volumes.findIndex((v) => v.id === resolvedId),
|
|
4260
4460
|
[resolvedId, volumes]
|
|
4261
4461
|
);
|
|
4262
|
-
const current =
|
|
4462
|
+
const current = useMemo12(
|
|
4263
4463
|
() => index >= 0 && index < volumes.length ? volumes[index] : null,
|
|
4264
4464
|
[index, volumes]
|
|
4265
4465
|
);
|
|
@@ -4295,122 +4495,24 @@ function useLibrary(options) {
|
|
|
4295
4495
|
}
|
|
4296
4496
|
|
|
4297
4497
|
// src/useManuscriptLayout.ts
|
|
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";
|
|
4498
|
+
import { useMemo as useMemo13 } from "react";
|
|
4305
4499
|
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
4306
4500
|
const dialect = options.dialect ?? "mejiro";
|
|
4307
|
-
const
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
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
|
-
);
|
|
4349
|
-
useLayoutEffect5(() => {
|
|
4350
|
-
requestIdRef.current++;
|
|
4351
|
-
pendingRestore.current = null;
|
|
4352
|
-
setLayout(null);
|
|
4353
|
-
setPageWidth(0);
|
|
4354
|
-
setPageHeight(0);
|
|
4355
|
-
setContentHeight(0);
|
|
4356
|
-
setElapsedMs(0);
|
|
4357
|
-
}, [recompute]);
|
|
4358
|
-
useEffect17(() => {
|
|
4359
|
-
void recompute();
|
|
4360
|
-
return () => {
|
|
4361
|
-
requestIdRef.current++;
|
|
4362
|
-
};
|
|
4363
|
-
}, [recompute]);
|
|
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(() => {
|
|
4372
|
-
if (!enableResize) return;
|
|
4373
|
-
const el = surface.current;
|
|
4374
|
-
if (!el) return;
|
|
4375
|
-
let timer = null;
|
|
4376
|
-
let observed = false;
|
|
4377
|
-
const clear = () => {
|
|
4378
|
-
if (timer) {
|
|
4379
|
-
clearTimeout(timer);
|
|
4380
|
-
timer = null;
|
|
4381
|
-
}
|
|
4382
|
-
};
|
|
4383
|
-
const scheduleReflow = (immediate) => {
|
|
4384
|
-
clear();
|
|
4385
|
-
if (immediate) {
|
|
4386
|
-
void recompute({ blank: false });
|
|
4387
|
-
return;
|
|
4501
|
+
const source = useMemo13(() => {
|
|
4502
|
+
if (!chapter) return null;
|
|
4503
|
+
return {
|
|
4504
|
+
measure: (el) => book.computePageSize(el),
|
|
4505
|
+
layout: async () => {
|
|
4506
|
+
const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
|
|
4507
|
+
return layouts.values().next().value ?? null;
|
|
4388
4508
|
}
|
|
4389
|
-
timer = setTimeout(() => {
|
|
4390
|
-
timer = null;
|
|
4391
|
-
void recompute({ blank: false });
|
|
4392
|
-
}, resizeDebounce);
|
|
4393
|
-
};
|
|
4394
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
4395
|
-
const observer = new ResizeObserver(() => {
|
|
4396
|
-
const immediate = !observed;
|
|
4397
|
-
observed = true;
|
|
4398
|
-
scheduleReflow(immediate);
|
|
4399
|
-
});
|
|
4400
|
-
observer.observe(el);
|
|
4401
|
-
return () => {
|
|
4402
|
-
observer.disconnect();
|
|
4403
|
-
clear();
|
|
4404
|
-
};
|
|
4405
|
-
}
|
|
4406
|
-
const onWindowResize = () => scheduleReflow(false);
|
|
4407
|
-
window.addEventListener("resize", onWindowResize);
|
|
4408
|
-
return () => {
|
|
4409
|
-
window.removeEventListener("resize", onWindowResize);
|
|
4410
|
-
clear();
|
|
4411
4509
|
};
|
|
4412
|
-
}, [
|
|
4413
|
-
return
|
|
4510
|
+
}, [book, chapter, dialect]);
|
|
4511
|
+
return useSurfaceLayout(source, surface, {
|
|
4512
|
+
...options,
|
|
4513
|
+
enableResize: options.enableResize ?? true,
|
|
4514
|
+
resizeDebounce: options.resizeDebounce ?? 120
|
|
4515
|
+
});
|
|
4414
4516
|
}
|
|
4415
4517
|
|
|
4416
4518
|
// src/useReadingPosition.ts
|
|
@@ -4418,11 +4520,11 @@ import {
|
|
|
4418
4520
|
parseReadingPosition,
|
|
4419
4521
|
serializeReadingPosition
|
|
4420
4522
|
} from "@libraz/mejiro";
|
|
4421
|
-
import { useCallback as
|
|
4523
|
+
import { useCallback as useCallback15, useEffect as useEffect17, useRef as useRef20, useState as useState19 } from "react";
|
|
4422
4524
|
function useReadingPosition(options) {
|
|
4423
4525
|
const { key, throttleMs = 250 } = options;
|
|
4424
4526
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
4425
|
-
const [position, setPosition] =
|
|
4527
|
+
const [position, setPosition] = useState19(
|
|
4426
4528
|
() => readStorage(storage, key, parseReadingPosition, null)
|
|
4427
4529
|
);
|
|
4428
4530
|
const storageRef = useRef20(storage);
|
|
@@ -4430,14 +4532,14 @@ function useReadingPosition(options) {
|
|
|
4430
4532
|
const pendingRef = useRef20(null);
|
|
4431
4533
|
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
4432
4534
|
const pending = pendingRef.current;
|
|
4433
|
-
|
|
4434
|
-
|
|
4535
|
+
useEffect17(() => flushOnPageHide(pending.flush), [pending]);
|
|
4536
|
+
useEffect17(() => {
|
|
4435
4537
|
setPosition(readStorage(storageRef.current, key, parseReadingPosition, null));
|
|
4436
4538
|
return pending.flush;
|
|
4437
4539
|
}, [key, pending]);
|
|
4438
4540
|
const onChangeRef = useRef20(options.onChange);
|
|
4439
4541
|
onChangeRef.current = options.onChange;
|
|
4440
|
-
const save =
|
|
4542
|
+
const save = useCallback15(
|
|
4441
4543
|
(next) => {
|
|
4442
4544
|
setPosition(next);
|
|
4443
4545
|
const currentStorage = storageRef.current;
|
|
@@ -4450,7 +4552,7 @@ function useReadingPosition(options) {
|
|
|
4450
4552
|
},
|
|
4451
4553
|
[key, throttleMs, pending]
|
|
4452
4554
|
);
|
|
4453
|
-
const clear =
|
|
4555
|
+
const clear = useCallback15(() => {
|
|
4454
4556
|
setPosition(null);
|
|
4455
4557
|
pending.cancel();
|
|
4456
4558
|
removeStorage(storageRef.current, key);
|