@libraz/mejiro-react 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/index.d.ts +231 -51
- package/dist/index.js +617 -442
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -114,6 +114,11 @@ function MejiroDropZone({
|
|
|
114
114
|
className: rootClass,
|
|
115
115
|
style,
|
|
116
116
|
onClick: () => inputRef.current?.click(),
|
|
117
|
+
onKeyDown: (e) => {
|
|
118
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
inputRef.current?.click();
|
|
121
|
+
},
|
|
117
122
|
onDragOver: (e) => {
|
|
118
123
|
e.preventDefault();
|
|
119
124
|
setDragover(true);
|
|
@@ -150,8 +155,12 @@ function MejiroDropZone({
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
// src/MejiroEditor.tsx
|
|
153
|
-
import {
|
|
154
|
-
|
|
158
|
+
import {
|
|
159
|
+
cloneEditableEpubBook,
|
|
160
|
+
EditableEpub,
|
|
161
|
+
exportEditableEpub
|
|
162
|
+
} from "@libraz/mejiro/epub";
|
|
163
|
+
import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef10, useState as useState8 } from "react";
|
|
155
164
|
|
|
156
165
|
// src/MejiroReader.tsx
|
|
157
166
|
import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
|
|
@@ -160,7 +169,7 @@ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
|
|
|
160
169
|
import {
|
|
161
170
|
forwardRef,
|
|
162
171
|
useCallback as useCallback6,
|
|
163
|
-
useEffect as
|
|
172
|
+
useEffect as useEffect7,
|
|
164
173
|
useImperativeHandle,
|
|
165
174
|
useLayoutEffect as useLayoutEffect4,
|
|
166
175
|
useMemo as useMemo4,
|
|
@@ -190,6 +199,9 @@ import {
|
|
|
190
199
|
// src/MejiroPageView.tsx
|
|
191
200
|
import { normalizeFontFamily } from "@libraz/mejiro/browser";
|
|
192
201
|
|
|
202
|
+
// src/MejiroPage.tsx
|
|
203
|
+
import { paragraphClassName } from "@libraz/mejiro/render";
|
|
204
|
+
|
|
193
205
|
// src/renderInlineNode.tsx
|
|
194
206
|
import {
|
|
195
207
|
segmentToInlineNode
|
|
@@ -228,12 +240,10 @@ function renderLine(line, lineIndex) {
|
|
|
228
240
|
function MejiroPage({ page, className, style }) {
|
|
229
241
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
230
242
|
return /* @__PURE__ */ jsx5("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
paraClass += " mejiro-paragraph--heading";
|
|
236
|
-
}
|
|
243
|
+
const paraClass = paragraphClassName(
|
|
244
|
+
paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
|
|
245
|
+
paragraph.headingLevel
|
|
246
|
+
);
|
|
237
247
|
return (
|
|
238
248
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
239
249
|
/* @__PURE__ */ jsx5("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
@@ -289,6 +299,7 @@ function MejiroScrollView({
|
|
|
289
299
|
fontFamily,
|
|
290
300
|
fontSize,
|
|
291
301
|
lineSpacing,
|
|
302
|
+
slotMode,
|
|
292
303
|
onVisiblePageChange,
|
|
293
304
|
scrollToPage,
|
|
294
305
|
pageGap = 24
|
|
@@ -303,7 +314,6 @@ function MejiroScrollView({
|
|
|
303
314
|
return Array.from({ length: total }, (_, i) => layout.getPage(i));
|
|
304
315
|
}, [layout]);
|
|
305
316
|
const pageCount = pages.length;
|
|
306
|
-
const useSlot = true;
|
|
307
317
|
const contentStyle = { height: contentHeight };
|
|
308
318
|
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
|
|
309
319
|
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
@@ -383,7 +393,7 @@ function MejiroScrollView({
|
|
|
383
393
|
MejiroPageView,
|
|
384
394
|
{
|
|
385
395
|
result,
|
|
386
|
-
slotMode
|
|
396
|
+
slotMode,
|
|
387
397
|
fontFamily,
|
|
388
398
|
lineSpacing,
|
|
389
399
|
className: "mejiro-reader-page-content",
|
|
@@ -441,7 +451,14 @@ function MejiroSettingsPanel({
|
|
|
441
451
|
type: "button",
|
|
442
452
|
className: "mejiro-reader-btn mejiro-reader-btn--icon",
|
|
443
453
|
"aria-label": messages.settingsSizeDown,
|
|
444
|
-
onClick: () => patch({
|
|
454
|
+
onClick: () => patch({
|
|
455
|
+
fontSize: clampNumber(
|
|
456
|
+
settings.fontSize - 1,
|
|
457
|
+
minFontSize,
|
|
458
|
+
maxFontSize,
|
|
459
|
+
minFontSize
|
|
460
|
+
)
|
|
461
|
+
}),
|
|
445
462
|
children: "A\u2212"
|
|
446
463
|
}
|
|
447
464
|
),
|
|
@@ -469,7 +486,14 @@ function MejiroSettingsPanel({
|
|
|
469
486
|
type: "button",
|
|
470
487
|
className: "mejiro-reader-btn mejiro-reader-btn--icon",
|
|
471
488
|
"aria-label": messages.settingsSizeUp,
|
|
472
|
-
onClick: () => patch({
|
|
489
|
+
onClick: () => patch({
|
|
490
|
+
fontSize: clampNumber(
|
|
491
|
+
settings.fontSize + 1,
|
|
492
|
+
minFontSize,
|
|
493
|
+
maxFontSize,
|
|
494
|
+
minFontSize
|
|
495
|
+
)
|
|
496
|
+
}),
|
|
473
497
|
children: "A+"
|
|
474
498
|
}
|
|
475
499
|
)
|
|
@@ -627,6 +651,7 @@ function MejiroSelectionLayer({
|
|
|
627
651
|
width: r.width,
|
|
628
652
|
height: r.height,
|
|
629
653
|
...posStyle,
|
|
654
|
+
...r.color != null ? { backgroundColor: r.color } : null,
|
|
630
655
|
...rectStyle
|
|
631
656
|
}
|
|
632
657
|
},
|
|
@@ -657,6 +682,7 @@ function MejiroSpread({
|
|
|
657
682
|
rightHeader = {},
|
|
658
683
|
leftHeader = {},
|
|
659
684
|
images = [],
|
|
685
|
+
slotMode,
|
|
660
686
|
renderPageHeader,
|
|
661
687
|
indicator,
|
|
662
688
|
onPrev,
|
|
@@ -665,6 +691,7 @@ function MejiroSpread({
|
|
|
665
691
|
onImageResizePointerDown,
|
|
666
692
|
onImageClose,
|
|
667
693
|
anchorAtCoord,
|
|
694
|
+
spreadIdx,
|
|
668
695
|
selectionRects,
|
|
669
696
|
onSelectionChange,
|
|
670
697
|
singlePage = false,
|
|
@@ -673,7 +700,6 @@ function MejiroSpread({
|
|
|
673
700
|
}) {
|
|
674
701
|
const messages = useI18n();
|
|
675
702
|
const hasImages = images.length > 0;
|
|
676
|
-
const useSlot = true;
|
|
677
703
|
const selectionStartRef = useRef3(null);
|
|
678
704
|
const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
|
|
679
705
|
const gestureRef = useRef3(null);
|
|
@@ -747,6 +773,7 @@ function MejiroSpread({
|
|
|
747
773
|
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily4(fontFamily);
|
|
748
774
|
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
749
775
|
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
776
|
+
const visibleRects = selectionRects && spreadIdx != null ? selectionRects.filter((r) => r.spreadIdx === spreadIdx) : selectionRects;
|
|
750
777
|
const renderPage = (side) => {
|
|
751
778
|
const isRight = side === "right";
|
|
752
779
|
const result = isRight ? spread.right : spread.left;
|
|
@@ -770,7 +797,7 @@ function MejiroSpread({
|
|
|
770
797
|
MejiroPageView,
|
|
771
798
|
{
|
|
772
799
|
result,
|
|
773
|
-
slotMode
|
|
800
|
+
slotMode,
|
|
774
801
|
fontFamily,
|
|
775
802
|
lineSpacing,
|
|
776
803
|
className: "mejiro-reader-page-content",
|
|
@@ -778,7 +805,7 @@ function MejiroSpread({
|
|
|
778
805
|
},
|
|
779
806
|
pageKey
|
|
780
807
|
),
|
|
781
|
-
|
|
808
|
+
visibleRects && visibleRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: visibleRects, side })
|
|
782
809
|
] }) }),
|
|
783
810
|
isRight && images.map((item) => /* @__PURE__ */ jsx11(
|
|
784
811
|
MejiroImageOverlay,
|
|
@@ -855,7 +882,10 @@ function MejiroStats({
|
|
|
855
882
|
readingTimeLocale = "ja"
|
|
856
883
|
}) {
|
|
857
884
|
if (!chapter) return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats" });
|
|
858
|
-
const totalChars = chapter.paragraphs.reduce(
|
|
885
|
+
const totalChars = chapter.paragraphs.reduce(
|
|
886
|
+
(s, p) => p.headingLevel != null ? s : s + [...p.text].length,
|
|
887
|
+
0
|
|
888
|
+
);
|
|
859
889
|
const totalRuby = chapter.paragraphs.reduce(
|
|
860
890
|
(s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
|
|
861
891
|
0
|
|
@@ -1008,12 +1038,14 @@ function useEpub(options = {}) {
|
|
|
1008
1038
|
fetchOptionsRef.current = options.fetchOptions;
|
|
1009
1039
|
const fetchEpubRef = useRef5(options.fetchEpub);
|
|
1010
1040
|
fetchEpubRef.current = options.fetchEpub;
|
|
1041
|
+
const limitsRef = useRef5(options.limits);
|
|
1042
|
+
limitsRef.current = options.limits;
|
|
1011
1043
|
const loadBuffer = useCallback2(async (buffer) => {
|
|
1012
1044
|
const requestId = ++requestIdRef.current;
|
|
1013
1045
|
setLoading(true);
|
|
1014
1046
|
setError(null);
|
|
1015
1047
|
try {
|
|
1016
|
-
const book = await parseEpub(buffer);
|
|
1048
|
+
const book = await parseEpub(buffer, { limits: limitsRef.current });
|
|
1017
1049
|
if (requestId !== requestIdRef.current) return null;
|
|
1018
1050
|
setEpub(book);
|
|
1019
1051
|
onLoadRef.current?.(book);
|
|
@@ -1034,7 +1066,7 @@ function useEpub(options = {}) {
|
|
|
1034
1066
|
setLoading(true);
|
|
1035
1067
|
setError(null);
|
|
1036
1068
|
try {
|
|
1037
|
-
const book = await parseEpub(await file.arrayBuffer());
|
|
1069
|
+
const book = await parseEpub(await file.arrayBuffer(), { limits: limitsRef.current });
|
|
1038
1070
|
if (requestId !== requestIdRef.current) return null;
|
|
1039
1071
|
setEpub(book);
|
|
1040
1072
|
onLoadRef.current?.(book);
|
|
@@ -1064,7 +1096,7 @@ function useEpub(options = {}) {
|
|
|
1064
1096
|
if (!res.ok) return null;
|
|
1065
1097
|
buffer = await res.arrayBuffer();
|
|
1066
1098
|
}
|
|
1067
|
-
const book = await parseEpub(buffer);
|
|
1099
|
+
const book = await parseEpub(buffer, { limits: limitsRef.current });
|
|
1068
1100
|
if (requestId !== requestIdRef.current) return null;
|
|
1069
1101
|
setEpub(book);
|
|
1070
1102
|
onLoadRef.current?.(book);
|
|
@@ -1095,23 +1127,79 @@ function useEpub(options = {}) {
|
|
|
1095
1127
|
|
|
1096
1128
|
// src/useMejiroBook.ts
|
|
1097
1129
|
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1098
|
-
import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
|
|
1099
|
-
function useMejiroBook(initial) {
|
|
1130
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState4 } from "react";
|
|
1131
|
+
function useMejiroBook(initial, options = {}) {
|
|
1100
1132
|
const bookRef = useRef6(null);
|
|
1101
1133
|
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1102
|
-
const [
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1134
|
+
const [snapshot, setLocal] = useState4(initial);
|
|
1135
|
+
const optionsRef = useRef6(options);
|
|
1136
|
+
optionsRef.current = options;
|
|
1137
|
+
const pendingRef = useRef6(null);
|
|
1138
|
+
const flush = useCallback3(() => {
|
|
1139
|
+
const pending = pendingRef.current;
|
|
1140
|
+
if (!pending) return;
|
|
1141
|
+
pendingRef.current = null;
|
|
1142
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1143
|
+
const { waiters } = pending;
|
|
1144
|
+
const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
|
|
1145
|
+
void applied.then(
|
|
1146
|
+
() => {
|
|
1147
|
+
for (const waiter of waiters) waiter.resolve();
|
|
1148
|
+
},
|
|
1149
|
+
(err) => {
|
|
1150
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1151
|
+
const handler = optionsRef.current.onError;
|
|
1152
|
+
if (handler) {
|
|
1153
|
+
handler(error);
|
|
1154
|
+
for (const waiter of waiters) waiter.resolve();
|
|
1155
|
+
} else {
|
|
1156
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1106
1160
|
}, []);
|
|
1107
|
-
|
|
1161
|
+
const setOptions = useCallback3(
|
|
1162
|
+
(next) => {
|
|
1163
|
+
setLocal((prev) => ({ ...prev, ...next }));
|
|
1164
|
+
const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
|
|
1165
|
+
pendingRef.current = pending;
|
|
1166
|
+
Object.assign(pending.patch, next);
|
|
1167
|
+
const settled = new Promise((resolve, reject) => {
|
|
1168
|
+
pending.waiters.push({ resolve, reject });
|
|
1169
|
+
});
|
|
1170
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1171
|
+
const debounceMs = optionsRef.current.debounceMs ?? 0;
|
|
1172
|
+
if (debounceMs > 0) {
|
|
1173
|
+
pending.timer = setTimeout(() => {
|
|
1174
|
+
pending.timer = null;
|
|
1175
|
+
flush();
|
|
1176
|
+
}, debounceMs);
|
|
1177
|
+
} else {
|
|
1178
|
+
pending.timer = null;
|
|
1179
|
+
flush();
|
|
1180
|
+
}
|
|
1181
|
+
return settled;
|
|
1182
|
+
},
|
|
1183
|
+
[flush]
|
|
1184
|
+
);
|
|
1185
|
+
useEffect4(
|
|
1186
|
+
() => () => {
|
|
1187
|
+
const pending = pendingRef.current;
|
|
1188
|
+
if (!pending) return;
|
|
1189
|
+
pendingRef.current = null;
|
|
1190
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1191
|
+
for (const waiter of pending.waiters) waiter.resolve();
|
|
1192
|
+
},
|
|
1193
|
+
[]
|
|
1194
|
+
);
|
|
1195
|
+
return { book: bookRef.current, options: snapshot, setOptions };
|
|
1108
1196
|
}
|
|
1109
1197
|
|
|
1110
1198
|
// src/useMultiImageOverlay.ts
|
|
1111
|
-
import {
|
|
1199
|
+
import { createOverlayDragSession } from "@libraz/mejiro/browser";
|
|
1112
1200
|
import {
|
|
1113
1201
|
useCallback as useCallback4,
|
|
1114
|
-
useEffect as
|
|
1202
|
+
useEffect as useEffect5,
|
|
1115
1203
|
useMemo as useMemo3,
|
|
1116
1204
|
useRef as useRef7,
|
|
1117
1205
|
useState as useState5
|
|
@@ -1133,7 +1221,7 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
1133
1221
|
const onUpdateRef = useRef7(options.onUpdate);
|
|
1134
1222
|
onUpdateRef.current = options.onUpdate;
|
|
1135
1223
|
const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1136
|
-
|
|
1224
|
+
useEffect5(
|
|
1137
1225
|
() => () => {
|
|
1138
1226
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
1139
1227
|
activeDragCleanupsRef.current.clear();
|
|
@@ -1247,32 +1335,19 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
1247
1335
|
e.preventDefault();
|
|
1248
1336
|
const found = findById(id);
|
|
1249
1337
|
if (!found) return;
|
|
1250
|
-
const startX = e.clientX;
|
|
1251
|
-
const startY = e.clientY;
|
|
1252
|
-
const start = { ...found.item.rect };
|
|
1253
1338
|
const target = e.currentTarget;
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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);
|
|
1339
|
+
createOverlayDragSession({
|
|
1340
|
+
mode: "move",
|
|
1341
|
+
rect: found.item.rect,
|
|
1342
|
+
startX: e.clientX,
|
|
1343
|
+
startY: e.clientY,
|
|
1344
|
+
pointerId: e.nativeEvent.pointerId,
|
|
1345
|
+
captureElement: target,
|
|
1346
|
+
activeElement: target,
|
|
1347
|
+
dragClass: "is-dragging",
|
|
1348
|
+
registry: activeDragCleanupsRef.current,
|
|
1349
|
+
onChange: (rect) => updateImage(id, rect)
|
|
1350
|
+
});
|
|
1276
1351
|
},
|
|
1277
1352
|
[findById, updateImage]
|
|
1278
1353
|
);
|
|
@@ -1282,36 +1357,23 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
1282
1357
|
e.stopPropagation();
|
|
1283
1358
|
const found = findById(id);
|
|
1284
1359
|
if (!found) return;
|
|
1285
|
-
const startX = e.clientX;
|
|
1286
|
-
const startY = e.clientY;
|
|
1287
|
-
const start = { ...found.item.rect };
|
|
1288
1360
|
const target = e.currentTarget;
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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);
|
|
1361
|
+
createOverlayDragSession({
|
|
1362
|
+
mode: "resize",
|
|
1363
|
+
rect: found.item.rect,
|
|
1364
|
+
startX: e.clientX,
|
|
1365
|
+
startY: e.clientY,
|
|
1366
|
+
pointerId: e.nativeEvent.pointerId,
|
|
1367
|
+
captureElement: target,
|
|
1368
|
+
activeElement: target.parentElement,
|
|
1369
|
+
dragClass: "is-dragging",
|
|
1370
|
+
registry: activeDragCleanupsRef.current,
|
|
1371
|
+
onChange: (rect) => updateImage(id, rect)
|
|
1372
|
+
});
|
|
1311
1373
|
},
|
|
1312
1374
|
[findById, updateImage]
|
|
1313
1375
|
);
|
|
1314
|
-
|
|
1376
|
+
useEffect5(() => {
|
|
1315
1377
|
if (!layout) return;
|
|
1316
1378
|
for (const si of imagesRef.current.keys()) syncSpread(si);
|
|
1317
1379
|
}, [layout, syncSpread]);
|
|
@@ -1331,7 +1393,16 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
1331
1393
|
}
|
|
1332
1394
|
|
|
1333
1395
|
// src/useSpread.ts
|
|
1334
|
-
import { useCallback as useCallback5, useEffect as
|
|
1396
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState6 } from "react";
|
|
1397
|
+
function ignoreNavigationKey(e) {
|
|
1398
|
+
if (e.defaultPrevented) return true;
|
|
1399
|
+
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
|
|
1400
|
+
const target = e.target;
|
|
1401
|
+
if (!target) return false;
|
|
1402
|
+
if (target.isContentEditable) return true;
|
|
1403
|
+
const tag = target.tagName;
|
|
1404
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
1405
|
+
}
|
|
1335
1406
|
function useSpread(layout, options = {}) {
|
|
1336
1407
|
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1337
1408
|
const turnDuration = options.turnDuration ?? 180;
|
|
@@ -1370,7 +1441,7 @@ function useSpread(layout, options = {}) {
|
|
|
1370
1441
|
}
|
|
1371
1442
|
setSpread(layout.getSpread(0));
|
|
1372
1443
|
}, [layout]);
|
|
1373
|
-
|
|
1444
|
+
useEffect6(() => {
|
|
1374
1445
|
if (!layout) return;
|
|
1375
1446
|
setSpread(layout.getSpread(spreadIdx));
|
|
1376
1447
|
const last = lastEmittedRef.current;
|
|
@@ -1417,17 +1488,18 @@ function useSpread(layout, options = {}) {
|
|
|
1417
1488
|
}, []);
|
|
1418
1489
|
const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
|
|
1419
1490
|
const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
|
|
1420
|
-
|
|
1491
|
+
useEffect6(() => {
|
|
1421
1492
|
if (!enableKeyboard) return;
|
|
1422
1493
|
const onKey = (e) => {
|
|
1423
1494
|
if (!layoutRef.current) return;
|
|
1495
|
+
if (ignoreNavigationKey(e)) return;
|
|
1424
1496
|
if (e.key === "ArrowLeft") next();
|
|
1425
1497
|
else if (e.key === "ArrowRight") prev();
|
|
1426
1498
|
};
|
|
1427
1499
|
window.addEventListener("keydown", onKey);
|
|
1428
1500
|
return () => window.removeEventListener("keydown", onKey);
|
|
1429
1501
|
}, [enableKeyboard, next, prev]);
|
|
1430
|
-
|
|
1502
|
+
useEffect6(
|
|
1431
1503
|
() => () => {
|
|
1432
1504
|
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1433
1505
|
},
|
|
@@ -1449,6 +1521,7 @@ function useSpread(layout, options = {}) {
|
|
|
1449
1521
|
|
|
1450
1522
|
// src/MejiroReader.tsx
|
|
1451
1523
|
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1524
|
+
var OPTIONS_DEBOUNCE_MS = 60;
|
|
1452
1525
|
function MejiroReaderInner(props, ref) {
|
|
1453
1526
|
const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
|
|
1454
1527
|
const dialectProp = "dialect" in props ? props.dialect : void 0;
|
|
@@ -1467,6 +1540,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1467
1540
|
enableSurfaceTap = true,
|
|
1468
1541
|
fallback,
|
|
1469
1542
|
fetchOptions,
|
|
1543
|
+
limits,
|
|
1470
1544
|
fetchEpub: fetchEpubFn,
|
|
1471
1545
|
locale,
|
|
1472
1546
|
messages,
|
|
@@ -1502,7 +1576,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1502
1576
|
const chapter = chapterProp ?? chapterState;
|
|
1503
1577
|
const [chromeHidden, setChromeHidden] = useState7(false);
|
|
1504
1578
|
const [autoSingle, setAutoSingle] = useState7(false);
|
|
1505
|
-
|
|
1579
|
+
useEffect7(() => {
|
|
1506
1580
|
if (!settingsOpen) return;
|
|
1507
1581
|
const onKey = (e2) => {
|
|
1508
1582
|
if (e2.key === "Escape") setSettingsOpen(false);
|
|
@@ -1510,7 +1584,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1510
1584
|
window.addEventListener("keydown", onKey);
|
|
1511
1585
|
return () => window.removeEventListener("keydown", onKey);
|
|
1512
1586
|
}, [settingsOpen]);
|
|
1513
|
-
|
|
1587
|
+
useEffect7(() => {
|
|
1514
1588
|
if (spreadMode !== "auto") return;
|
|
1515
1589
|
const surface = surfaceRef.current;
|
|
1516
1590
|
if (!surface) return;
|
|
@@ -1547,15 +1621,26 @@ function MejiroReaderInner(props, ref) {
|
|
|
1547
1621
|
() => ({ ...DEFAULT_BOOK_OPTIONS, ...optionsProp ?? {} }),
|
|
1548
1622
|
[optionsProp]
|
|
1549
1623
|
);
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1624
|
+
const onErrorRef = useRef9(onError);
|
|
1625
|
+
onErrorRef.current = onError;
|
|
1626
|
+
const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
|
|
1627
|
+
const {
|
|
1628
|
+
book,
|
|
1629
|
+
options: bookOptions,
|
|
1630
|
+
setOptions
|
|
1631
|
+
} = useMejiroBook(resolvedOptions, {
|
|
1632
|
+
debounceMs: OPTIONS_DEBOUNCE_MS,
|
|
1633
|
+
onError: reportError
|
|
1634
|
+
});
|
|
1635
|
+
const optionsPropKey = JSON.stringify(resolvedOptions);
|
|
1636
|
+
const syncedOptionsPropKeyRef = useRef9(optionsPropKey);
|
|
1637
|
+
const resolvedOptionsRef = useRef9(resolvedOptions);
|
|
1638
|
+
resolvedOptionsRef.current = resolvedOptions;
|
|
1639
|
+
useEffect7(() => {
|
|
1640
|
+
if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
|
|
1641
|
+
syncedOptionsPropKeyRef.current = optionsPropKey;
|
|
1642
|
+
void setOptions(resolvedOptionsRef.current);
|
|
1643
|
+
}, [optionsPropKey, setOptions]);
|
|
1559
1644
|
const synthesizedEpub = useMemo4(() => {
|
|
1560
1645
|
if (manuscriptProp === void 0) return null;
|
|
1561
1646
|
return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
|
|
@@ -1565,6 +1650,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1565
1650
|
// book (or a synthesized manuscript book) is supplied.
|
|
1566
1651
|
defaultUrl: epubProp !== void 0 || manuscriptProp !== void 0 ? void 0 : epubUrl,
|
|
1567
1652
|
fetchOptions,
|
|
1653
|
+
limits,
|
|
1568
1654
|
fetchEpub: fetchEpubFn,
|
|
1569
1655
|
onLoad: (b) => {
|
|
1570
1656
|
if (chapterProp == null) setChapterState(0);
|
|
@@ -1574,9 +1660,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1574
1660
|
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
1575
1661
|
const isManuscriptSource = manuscriptProp !== void 0;
|
|
1576
1662
|
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
1577
|
-
|
|
1578
|
-
onErrorRef.current = onError;
|
|
1579
|
-
useEffect6(() => {
|
|
1663
|
+
useEffect7(() => {
|
|
1580
1664
|
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
1581
1665
|
}, [epubCtx.error]);
|
|
1582
1666
|
const pageGeometryRef = useRef9(resolvedGeometry);
|
|
@@ -1622,20 +1706,24 @@ function MejiroReaderInner(props, ref) {
|
|
|
1622
1706
|
bookOptionsRef.current = bookOptions;
|
|
1623
1707
|
const recomputeRef = useRef9(layoutCtx.recompute);
|
|
1624
1708
|
recomputeRef.current = layoutCtx.recompute;
|
|
1625
|
-
|
|
1709
|
+
useEffect7(() => {
|
|
1626
1710
|
if (optionsKeyRef.current === optionsKey) return;
|
|
1627
1711
|
optionsKeyRef.current = optionsKey;
|
|
1628
1712
|
const timer = setTimeout(() => {
|
|
1629
1713
|
void (async () => {
|
|
1630
|
-
|
|
1631
|
-
|
|
1714
|
+
try {
|
|
1715
|
+
await setOptions({ ...bookOptionsRef.current });
|
|
1716
|
+
await recomputeRef.current({ blank: false });
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
1719
|
+
}
|
|
1632
1720
|
})();
|
|
1633
|
-
},
|
|
1721
|
+
}, OPTIONS_DEBOUNCE_MS);
|
|
1634
1722
|
return () => clearTimeout(timer);
|
|
1635
|
-
}, [optionsKey, setOptions]);
|
|
1723
|
+
}, [optionsKey, setOptions, reportError]);
|
|
1636
1724
|
const geometryKey = JSON.stringify(resolvedGeometry ?? null);
|
|
1637
1725
|
const geometryKeyRef = useRef9(geometryKey);
|
|
1638
|
-
|
|
1726
|
+
useEffect7(() => {
|
|
1639
1727
|
if (geometryKeyRef.current === geometryKey) return;
|
|
1640
1728
|
geometryKeyRef.current = geometryKey;
|
|
1641
1729
|
void recomputeRef.current({ blank: false });
|
|
@@ -1652,7 +1740,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1652
1740
|
start: annotation.start,
|
|
1653
1741
|
end: annotation.end
|
|
1654
1742
|
});
|
|
1655
|
-
for (const rect of rects) result.push(rect);
|
|
1743
|
+
for (const rect of rects) result.push({ ...rect, color: annotation.color });
|
|
1656
1744
|
}
|
|
1657
1745
|
return result;
|
|
1658
1746
|
}, [annotations, layoutCtx.layout, chapter]);
|
|
@@ -1660,7 +1748,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1660
1748
|
clearImagesRef.current = imageCtx.clearImages;
|
|
1661
1749
|
const onLoadRef = useRef9(onLoad);
|
|
1662
1750
|
onLoadRef.current = onLoad;
|
|
1663
|
-
|
|
1751
|
+
useEffect7(() => {
|
|
1664
1752
|
if (!controlled) return;
|
|
1665
1753
|
if (chapterIsUncontrolled) setChapterState(0);
|
|
1666
1754
|
clearImagesRef.current();
|
|
@@ -1669,10 +1757,18 @@ function MejiroReaderInner(props, ref) {
|
|
|
1669
1757
|
}, [controlled, e, book, chapterIsUncontrolled]);
|
|
1670
1758
|
const spreadGoToRef = useRef9(spreadCtx.goTo);
|
|
1671
1759
|
spreadGoToRef.current = spreadCtx.goTo;
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1760
|
+
const appliedSpreadIdxPropRef = useRef9(spreadIdxProp);
|
|
1761
|
+
useEffect7(() => {
|
|
1762
|
+
if (spreadIdxProp == null) {
|
|
1763
|
+
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const propChanged = appliedSpreadIdxPropRef.current !== spreadIdxProp;
|
|
1767
|
+
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
1768
|
+
if (spreadCtx.spreadIdx === spreadIdxProp) return;
|
|
1769
|
+
if (propChanged) spreadGoToRef.current(spreadIdxProp);
|
|
1770
|
+
else setSpreadRef.current(spreadIdxProp);
|
|
1771
|
+
}, [spreadIdxProp, spreadCtx.spreadIdx]);
|
|
1676
1772
|
useLayoutEffect4(() => {
|
|
1677
1773
|
const next = spreadIdxPropRef.current;
|
|
1678
1774
|
if (next == null || !layoutCtx.layout) return;
|
|
@@ -1711,10 +1807,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
1711
1807
|
spreadGoToRef.current(loc.spreadIdx);
|
|
1712
1808
|
pending.resolve();
|
|
1713
1809
|
}, [chapter, layout]);
|
|
1714
|
-
|
|
1810
|
+
useEffect7(() => {
|
|
1715
1811
|
tryApplyPendingAnchor();
|
|
1716
1812
|
}, [tryApplyPendingAnchor]);
|
|
1717
|
-
|
|
1813
|
+
useEffect7(
|
|
1718
1814
|
() => () => {
|
|
1719
1815
|
pendingAnchorRef.current?.resolve();
|
|
1720
1816
|
pendingAnchorRef.current = null;
|
|
@@ -1727,7 +1823,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1727
1823
|
onPageReadRef.current = onPageRead;
|
|
1728
1824
|
const onChapterCompletedRef = useRef9(onChapterCompleted);
|
|
1729
1825
|
onChapterCompletedRef.current = onChapterCompleted;
|
|
1730
|
-
|
|
1826
|
+
useEffect7(() => {
|
|
1731
1827
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1732
1828
|
if (didMountRef.current && dwellRef.current) {
|
|
1733
1829
|
onPageReadRef.current?.(dwellRef.current.anchor, now - dwellRef.current.ts);
|
|
@@ -1749,7 +1845,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1749
1845
|
}
|
|
1750
1846
|
}, [chapter, spreadCtx.spreadIdx, spreadCtx.totalSpreads, layout, emit]);
|
|
1751
1847
|
const prevTurningRef = useRef9(false);
|
|
1752
|
-
|
|
1848
|
+
useEffect7(() => {
|
|
1753
1849
|
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
1754
1850
|
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
1755
1851
|
} else if (!spreadCtx.turning && prevTurningRef.current) {
|
|
@@ -1998,6 +2094,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
1998
2094
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
1999
2095
|
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
2000
2096
|
onImageClose: imageCtx.removeImage,
|
|
2097
|
+
spreadIdx: spreadCtx.spreadIdx,
|
|
2001
2098
|
selectionRects: annotationRects.length ? annotationRects : void 0
|
|
2002
2099
|
},
|
|
2003
2100
|
`${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
|
|
@@ -2030,28 +2127,6 @@ MejiroReader.displayName = "MejiroReader";
|
|
|
2030
2127
|
|
|
2031
2128
|
// src/MejiroEditor.tsx
|
|
2032
2129
|
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
2130
|
function utf16ToCodepoint(text, utf16Offset) {
|
|
2056
2131
|
let cp = 0;
|
|
2057
2132
|
let i = 0;
|
|
@@ -2063,15 +2138,29 @@ function utf16ToCodepoint(text, utf16Offset) {
|
|
|
2063
2138
|
}
|
|
2064
2139
|
return cp;
|
|
2065
2140
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2141
|
+
var WATERMARK_PREFIX = "[mejiro-watermark]";
|
|
2142
|
+
var WATERMARK_BLOCK_ID = "mejiro-watermark";
|
|
2143
|
+
function watermarkedBook(book, watermark) {
|
|
2144
|
+
const copy = cloneEditableEpubBook(book);
|
|
2145
|
+
const text = `${WATERMARK_PREFIX} ${watermark.text}`;
|
|
2146
|
+
for (const chapter of copy.chapters) {
|
|
2147
|
+
chapter.blocks.unshift({
|
|
2148
|
+
kind: "paragraph",
|
|
2149
|
+
id: watermarkBlockId(chapter),
|
|
2150
|
+
text,
|
|
2151
|
+
inlineAnnotations: []
|
|
2152
|
+
});
|
|
2153
|
+
chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
|
|
2154
|
+
chapter.isDirty = true;
|
|
2155
|
+
}
|
|
2156
|
+
return copy;
|
|
2157
|
+
}
|
|
2158
|
+
function watermarkBlockId(chapter) {
|
|
2159
|
+
const used = new Set(chapter.blocks.map((block) => block.id));
|
|
2160
|
+
let id = WATERMARK_BLOCK_ID;
|
|
2161
|
+
let suffix = 2;
|
|
2162
|
+
while (used.has(id)) id = `${WATERMARK_BLOCK_ID}-${suffix++}`;
|
|
2163
|
+
return id;
|
|
2075
2164
|
}
|
|
2076
2165
|
function MejiroEditor({
|
|
2077
2166
|
epubUrl,
|
|
@@ -2083,6 +2172,7 @@ function MejiroEditor({
|
|
|
2083
2172
|
onBeforeExport,
|
|
2084
2173
|
exportPolicy,
|
|
2085
2174
|
assetResolver,
|
|
2175
|
+
limits,
|
|
2086
2176
|
onLoad,
|
|
2087
2177
|
onExport,
|
|
2088
2178
|
onError
|
|
@@ -2102,25 +2192,27 @@ function MejiroEditor({
|
|
|
2102
2192
|
const loadRequestIdRef = useRef10(0);
|
|
2103
2193
|
const onLoadRef = useRef10(onLoad);
|
|
2104
2194
|
const onErrorRef = useRef10(onError);
|
|
2195
|
+
const limitsRef = useRef10(limits);
|
|
2196
|
+
limitsRef.current = limits;
|
|
2105
2197
|
const book = editor?.book ?? null;
|
|
2106
2198
|
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
2107
2199
|
const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
|
|
2108
2200
|
const textDirty = paragraph ? text !== paragraph.text : false;
|
|
2109
2201
|
const previewBook = useMemo5(() => {
|
|
2110
2202
|
void revision;
|
|
2111
|
-
return book ?
|
|
2203
|
+
return book ? cloneEditableEpubBook(book) : null;
|
|
2112
2204
|
}, [book, revision]);
|
|
2113
|
-
|
|
2205
|
+
useEffect8(() => {
|
|
2114
2206
|
onLoadRef.current = onLoad;
|
|
2115
2207
|
}, [onLoad]);
|
|
2116
|
-
|
|
2208
|
+
useEffect8(() => {
|
|
2117
2209
|
onErrorRef.current = onError;
|
|
2118
2210
|
}, [onError]);
|
|
2119
2211
|
const loadBufferForRequest = useCallback7(async (buffer, requestId) => {
|
|
2120
2212
|
setLoading(true);
|
|
2121
2213
|
setError(null);
|
|
2122
2214
|
try {
|
|
2123
|
-
const next = await EditableEpub.load(buffer);
|
|
2215
|
+
const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
|
|
2124
2216
|
if (requestId !== loadRequestIdRef.current) return;
|
|
2125
2217
|
setEditor(next);
|
|
2126
2218
|
setSelection({ chapter: 0, paragraph: 0 });
|
|
@@ -2151,7 +2243,7 @@ function MejiroEditor({
|
|
|
2151
2243
|
}
|
|
2152
2244
|
}
|
|
2153
2245
|
}
|
|
2154
|
-
|
|
2246
|
+
useEffect8(() => {
|
|
2155
2247
|
if (!epubUrl) return;
|
|
2156
2248
|
let cancelled = false;
|
|
2157
2249
|
const requestId = ++loadRequestIdRef.current;
|
|
@@ -2178,13 +2270,15 @@ function MejiroEditor({
|
|
|
2178
2270
|
cancelled = true;
|
|
2179
2271
|
};
|
|
2180
2272
|
}, [epubUrl, loadBufferForRequest]);
|
|
2181
|
-
|
|
2273
|
+
useEffect8(() => {
|
|
2182
2274
|
setText(paragraph?.text ?? "");
|
|
2183
2275
|
setRubyStart(0);
|
|
2184
2276
|
setRubyEnd(Math.min(1, [...paragraph?.text ?? ""].length));
|
|
2185
2277
|
setRubyText("");
|
|
2186
2278
|
}, [paragraph]);
|
|
2187
2279
|
function selectParagraph(chapterIndex, paragraphIndex) {
|
|
2280
|
+
if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
|
|
2281
|
+
if (textDirty) applyText();
|
|
2188
2282
|
setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
|
|
2189
2283
|
}
|
|
2190
2284
|
function captureRubyRange() {
|
|
@@ -2240,8 +2334,9 @@ function MejiroEditor({
|
|
|
2240
2334
|
}
|
|
2241
2335
|
async function exportEpub() {
|
|
2242
2336
|
if (!editor) return;
|
|
2243
|
-
|
|
2244
|
-
|
|
2337
|
+
const watermark = exportPolicy?.watermark;
|
|
2338
|
+
const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
|
|
2339
|
+
let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
|
|
2245
2340
|
if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
|
|
2246
2341
|
const decision = await onBeforeExport?.(buffer);
|
|
2247
2342
|
onExport?.(buffer);
|
|
@@ -2377,7 +2472,7 @@ function paragraphBlockId(chapter, paragraphIndex) {
|
|
|
2377
2472
|
|
|
2378
2473
|
// src/MejiroManuscriptEditor.tsx
|
|
2379
2474
|
import { EpubProject } from "@libraz/mejiro/epub";
|
|
2380
|
-
import { useCallback as useCallback9, useEffect as
|
|
2475
|
+
import { useCallback as useCallback9, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
|
|
2381
2476
|
|
|
2382
2477
|
// src/MejiroNotationHighlighter.tsx
|
|
2383
2478
|
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
@@ -2495,7 +2590,7 @@ function buildSegments(text, dialect) {
|
|
|
2495
2590
|
}
|
|
2496
2591
|
|
|
2497
2592
|
// src/useManuscriptDraft.ts
|
|
2498
|
-
import { useCallback as useCallback8, useEffect as
|
|
2593
|
+
import { useCallback as useCallback8, useEffect as useEffect9, useRef as useRef12, useState as useState10 } from "react";
|
|
2499
2594
|
var DEFAULT_DELAY = 800;
|
|
2500
2595
|
function defaultChapter(index, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
|
|
2501
2596
|
return { id: `chapter-${Date.now()}-${index}`, title: titleFor(index), body: bodyFor(index) };
|
|
@@ -2524,33 +2619,45 @@ function useManuscriptDraft(options = {}) {
|
|
|
2524
2619
|
payloadRef.current = autosavePayload;
|
|
2525
2620
|
const chaptersRef = useRef12(chapters);
|
|
2526
2621
|
chaptersRef.current = chapters;
|
|
2622
|
+
const selectedRef = useRef12(selected);
|
|
2623
|
+
selectedRef.current = selected;
|
|
2527
2624
|
const didMountRef = useRef12(false);
|
|
2528
2625
|
const dirtyRef = useRef12(false);
|
|
2529
2626
|
const mountedRef = useRef12(true);
|
|
2627
|
+
const revisionRef = useRef12(0);
|
|
2628
|
+
const inFlightRevisionRef = useRef12(-1);
|
|
2530
2629
|
const flushAutosave = useCallback8(() => {
|
|
2531
2630
|
const callback = saveRef.current;
|
|
2532
|
-
if (!(callback && dirtyRef.current))
|
|
2533
|
-
|
|
2631
|
+
if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const revision = revisionRef.current;
|
|
2635
|
+
inFlightRevisionRef.current = revision;
|
|
2534
2636
|
const payload = payloadRef.current ? payloadRef.current(chaptersRef.current) : chaptersRef.current;
|
|
2535
|
-
void Promise.resolve(callback(payload)).
|
|
2637
|
+
void Promise.resolve(callback(payload)).then(() => {
|
|
2638
|
+
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
2639
|
+
}).catch((err) => {
|
|
2536
2640
|
if (!mountedRef.current) return;
|
|
2537
2641
|
setAutosaveError(err instanceof Error ? err : new Error(String(err)));
|
|
2642
|
+
}).finally(() => {
|
|
2643
|
+
if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
|
|
2538
2644
|
});
|
|
2539
2645
|
}, []);
|
|
2540
|
-
|
|
2646
|
+
useEffect9(() => {
|
|
2541
2647
|
if (!didMountRef.current) {
|
|
2542
2648
|
didMountRef.current = true;
|
|
2543
2649
|
return void 0;
|
|
2544
2650
|
}
|
|
2545
2651
|
if (!saveRef.current) return void 0;
|
|
2546
2652
|
dirtyRef.current = true;
|
|
2653
|
+
revisionRef.current += 1;
|
|
2547
2654
|
setAutosaveError(null);
|
|
2548
2655
|
const timer = setTimeout(() => {
|
|
2549
2656
|
flushAutosave();
|
|
2550
2657
|
}, autosaveDelay);
|
|
2551
2658
|
return () => clearTimeout(timer);
|
|
2552
2659
|
}, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
|
|
2553
|
-
|
|
2660
|
+
useEffect9(() => {
|
|
2554
2661
|
mountedRef.current = true;
|
|
2555
2662
|
const handleBeforeUnload = () => flushAutosave();
|
|
2556
2663
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
@@ -2560,31 +2667,40 @@ function useManuscriptDraft(options = {}) {
|
|
|
2560
2667
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
2561
2668
|
};
|
|
2562
2669
|
}, [flushAutosave]);
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
);
|
|
2670
|
+
const commit = useCallback8((nextChapters, nextSelected) => {
|
|
2671
|
+
chaptersRef.current = nextChapters;
|
|
2672
|
+
selectedRef.current = nextSelected;
|
|
2673
|
+
setChaptersState(nextChapters);
|
|
2674
|
+
setSelectedState(nextSelected);
|
|
2675
|
+
}, []);
|
|
2676
|
+
const setSelected = useCallback8((index) => {
|
|
2677
|
+
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
2678
|
+
selectedRef.current = next;
|
|
2679
|
+
setSelectedState(next);
|
|
2680
|
+
}, []);
|
|
2569
2681
|
const setChapters = useCallback8(
|
|
2570
2682
|
(next) => {
|
|
2571
2683
|
const normalized = next.length ? next : [defaultChapter(0, titleForRef.current, bodyForRef.current)];
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2684
|
+
const prev = selectedRef.current;
|
|
2685
|
+
const selectedId = chaptersRef.current[prev]?.id;
|
|
2686
|
+
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
2687
|
+
commit(
|
|
2688
|
+
normalized,
|
|
2689
|
+
nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
|
|
2690
|
+
);
|
|
2578
2691
|
},
|
|
2579
|
-
[
|
|
2692
|
+
[commit]
|
|
2580
2693
|
);
|
|
2581
2694
|
const patchChapter = useCallback8((index, patch) => {
|
|
2582
|
-
|
|
2583
|
-
(
|
|
2695
|
+
const next = chaptersRef.current.map(
|
|
2696
|
+
(chapter, i) => i === index ? { ...chapter, ...patch } : chapter
|
|
2584
2697
|
);
|
|
2698
|
+
chaptersRef.current = next;
|
|
2699
|
+
setChaptersState(next);
|
|
2585
2700
|
}, []);
|
|
2586
|
-
const addChapter = useCallback8(
|
|
2587
|
-
|
|
2701
|
+
const addChapter = useCallback8(
|
|
2702
|
+
(chapter = {}) => {
|
|
2703
|
+
const current = chaptersRef.current;
|
|
2588
2704
|
const generated = defaultChapter(current.length, titleForRef.current, bodyForRef.current);
|
|
2589
2705
|
const next = [
|
|
2590
2706
|
...current,
|
|
@@ -2594,38 +2710,41 @@ function useManuscriptDraft(options = {}) {
|
|
|
2594
2710
|
body: chapter.body ?? generated.body
|
|
2595
2711
|
}
|
|
2596
2712
|
];
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
const removeChapter = useCallback8(
|
|
2602
|
-
|
|
2603
|
-
|
|
2713
|
+
commit(next, next.length - 1);
|
|
2714
|
+
},
|
|
2715
|
+
[commit]
|
|
2716
|
+
);
|
|
2717
|
+
const removeChapter = useCallback8(
|
|
2718
|
+
(index) => {
|
|
2719
|
+
const current = chaptersRef.current;
|
|
2720
|
+
if (current.length <= 1) return;
|
|
2604
2721
|
const next = current.filter((_, i) => i !== index);
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2722
|
+
const prev = selectedRef.current;
|
|
2723
|
+
let nextSelected;
|
|
2724
|
+
if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
|
|
2725
|
+
else if (index < prev) nextSelected = prev - 1;
|
|
2726
|
+
else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
|
|
2727
|
+
commit(next, nextSelected);
|
|
2728
|
+
},
|
|
2729
|
+
[commit]
|
|
2730
|
+
);
|
|
2731
|
+
const reorderChapters = useCallback8(
|
|
2732
|
+
(from, to) => {
|
|
2733
|
+
const current = chaptersRef.current;
|
|
2734
|
+
if (from < 0 || from >= current.length) return;
|
|
2616
2735
|
const next = [...current];
|
|
2617
2736
|
const [moved] = next.splice(from, 1);
|
|
2618
2737
|
const target = Math.max(0, Math.min(next.length, to));
|
|
2619
2738
|
next.splice(target, 0, moved);
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2739
|
+
const prev = selectedRef.current;
|
|
2740
|
+
let nextSelected = prev;
|
|
2741
|
+
if (prev === from) nextSelected = target;
|
|
2742
|
+
else if (from < prev && target >= prev) nextSelected = prev - 1;
|
|
2743
|
+
else if (from > prev && target <= prev) nextSelected = prev + 1;
|
|
2744
|
+
commit(next, nextSelected);
|
|
2745
|
+
},
|
|
2746
|
+
[commit]
|
|
2747
|
+
);
|
|
2629
2748
|
return {
|
|
2630
2749
|
chapters,
|
|
2631
2750
|
selected,
|
|
@@ -2686,11 +2805,14 @@ function MejiroManuscriptEditor({
|
|
|
2686
2805
|
cover: coverProp,
|
|
2687
2806
|
onCoverChange,
|
|
2688
2807
|
chapters: initialChapters,
|
|
2808
|
+
dialect = "mejiro",
|
|
2689
2809
|
onAutosave,
|
|
2690
2810
|
autosaveDelay,
|
|
2691
2811
|
previewProps,
|
|
2692
2812
|
assetResolver,
|
|
2693
|
-
onExport
|
|
2813
|
+
onExport,
|
|
2814
|
+
onError,
|
|
2815
|
+
panelSide = "right"
|
|
2694
2816
|
}) {
|
|
2695
2817
|
const messages = useI18n();
|
|
2696
2818
|
const coverInputRef = useRef13(null);
|
|
@@ -2704,13 +2826,13 @@ function MejiroManuscriptEditor({
|
|
|
2704
2826
|
const title = titleControlled ? titleProp ?? "" : titleState;
|
|
2705
2827
|
const author = authorControlled ? authorProp ?? "" : authorState;
|
|
2706
2828
|
const cover = coverControlled ? coverProp ?? null : coverState;
|
|
2707
|
-
|
|
2829
|
+
useEffect10(() => {
|
|
2708
2830
|
if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
|
|
2709
2831
|
}, [titleControlled, titleProp]);
|
|
2710
|
-
|
|
2832
|
+
useEffect10(() => {
|
|
2711
2833
|
if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
|
|
2712
2834
|
}, [authorControlled, authorProp]);
|
|
2713
|
-
|
|
2835
|
+
useEffect10(() => {
|
|
2714
2836
|
if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
|
|
2715
2837
|
}, [coverControlled, coverProp]);
|
|
2716
2838
|
const setTitle = useCallback9(
|
|
@@ -2753,6 +2875,7 @@ function MejiroManuscriptEditor({
|
|
|
2753
2875
|
} = draft;
|
|
2754
2876
|
const current = chapters[selected] ?? chapters[0];
|
|
2755
2877
|
const [draggingIndex, setDraggingIndex] = useState11(null);
|
|
2878
|
+
const [exportError, setExportError] = useState11(null);
|
|
2756
2879
|
function wrapSelection(open, close) {
|
|
2757
2880
|
const el = bodyTextareaRef.current;
|
|
2758
2881
|
if (!el) return;
|
|
@@ -2771,26 +2894,44 @@ function MejiroManuscriptEditor({
|
|
|
2771
2894
|
});
|
|
2772
2895
|
}
|
|
2773
2896
|
const exportEpub = useCallback9(async () => {
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
project.setCover({
|
|
2784
|
-
href: coverAssetHref(cover),
|
|
2785
|
-
mediaType: cover.type || void 0,
|
|
2786
|
-
data: await cover.arrayBuffer()
|
|
2897
|
+
try {
|
|
2898
|
+
const project = EpubProject.fromManuscript({
|
|
2899
|
+
metadata: { title, author: author || void 0 },
|
|
2900
|
+
dialect,
|
|
2901
|
+
chapters: chapters.map((chapter) => ({
|
|
2902
|
+
id: chapter.id,
|
|
2903
|
+
title: chapter.title || messages.untitled,
|
|
2904
|
+
body: chapter.body
|
|
2905
|
+
}))
|
|
2787
2906
|
});
|
|
2907
|
+
if (cover) {
|
|
2908
|
+
project.setCover({
|
|
2909
|
+
href: coverAssetHref(cover),
|
|
2910
|
+
mediaType: cover.type || void 0,
|
|
2911
|
+
data: await cover.arrayBuffer()
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
2915
|
+
setExportError(null);
|
|
2916
|
+
onExport?.(buffer);
|
|
2917
|
+
downloadEpub(buffer, title);
|
|
2918
|
+
} catch (cause) {
|
|
2919
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2920
|
+
setExportError(error);
|
|
2921
|
+
onError?.(error);
|
|
2788
2922
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2923
|
+
}, [
|
|
2924
|
+
assetResolver,
|
|
2925
|
+
author,
|
|
2926
|
+
chapters,
|
|
2927
|
+
cover,
|
|
2928
|
+
dialect,
|
|
2929
|
+
messages.untitled,
|
|
2930
|
+
onError,
|
|
2931
|
+
onExport,
|
|
2932
|
+
title
|
|
2933
|
+
]);
|
|
2934
|
+
return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", "data-panel-side": panelSide, children: [
|
|
2794
2935
|
/* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
|
|
2795
2936
|
MejiroReader,
|
|
2796
2937
|
{
|
|
@@ -2803,6 +2944,7 @@ function MejiroManuscriptEditor({
|
|
|
2803
2944
|
title: chapter.title || messages.untitled,
|
|
2804
2945
|
body: chapter.body
|
|
2805
2946
|
})),
|
|
2947
|
+
dialect,
|
|
2806
2948
|
fonts,
|
|
2807
2949
|
chapter: selected,
|
|
2808
2950
|
onChapterChange: setSelected,
|
|
@@ -2816,6 +2958,7 @@ function MejiroManuscriptEditor({
|
|
|
2816
2958
|
/* @__PURE__ */ jsx16("small", { children: messages.manuscriptRubyHint })
|
|
2817
2959
|
] }),
|
|
2818
2960
|
draft.autosaveError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: draft.autosaveError.message }),
|
|
2961
|
+
exportError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: exportError.message }),
|
|
2819
2962
|
/* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
|
|
2820
2963
|
/* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
|
|
2821
2964
|
/* @__PURE__ */ jsx16("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
|
|
@@ -2905,6 +3048,7 @@ function MejiroManuscriptEditor({
|
|
|
2905
3048
|
{
|
|
2906
3049
|
ref: bodyTextareaRef,
|
|
2907
3050
|
className: "mejiro-editor-manuscript",
|
|
3051
|
+
dialect,
|
|
2908
3052
|
value: current.body,
|
|
2909
3053
|
onChange: (body) => patchChapter(selected, { body })
|
|
2910
3054
|
}
|
|
@@ -3025,7 +3169,7 @@ import {
|
|
|
3025
3169
|
serializeAnnotations,
|
|
3026
3170
|
sortAnnotations
|
|
3027
3171
|
} from "@libraz/mejiro";
|
|
3028
|
-
import { useCallback as useCallback10, useEffect as
|
|
3172
|
+
import { useCallback as useCallback10, useEffect as useEffect11, useRef as useRef14, useState as useState13 } from "react";
|
|
3029
3173
|
function resolveDefaultStorage() {
|
|
3030
3174
|
if (typeof globalThis === "undefined") return null;
|
|
3031
3175
|
const w = globalThis.localStorage;
|
|
@@ -3042,40 +3186,63 @@ function useAnnotations(options) {
|
|
|
3042
3186
|
return [];
|
|
3043
3187
|
}
|
|
3044
3188
|
});
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3189
|
+
const storageRef = useRef14(storage);
|
|
3190
|
+
storageRef.current = storage;
|
|
3191
|
+
const annotationsRef = useRef14(annotations);
|
|
3192
|
+
const applyNext = useCallback10((next) => {
|
|
3193
|
+
annotationsRef.current = next;
|
|
3194
|
+
setAnnotations(next);
|
|
3195
|
+
}, []);
|
|
3196
|
+
const timerRef = useRef14(null);
|
|
3197
|
+
const pendingWriteRef = useRef14(null);
|
|
3198
|
+
const flushPending = useCallback10(() => {
|
|
3199
|
+
if (timerRef.current) {
|
|
3200
|
+
clearTimeout(timerRef.current);
|
|
3201
|
+
timerRef.current = null;
|
|
3049
3202
|
}
|
|
3203
|
+
const write = pendingWriteRef.current;
|
|
3204
|
+
pendingWriteRef.current = null;
|
|
3205
|
+
if (!write) return;
|
|
3050
3206
|
try {
|
|
3051
|
-
|
|
3207
|
+
write();
|
|
3052
3208
|
} catch {
|
|
3053
|
-
setAnnotations([]);
|
|
3054
3209
|
}
|
|
3055
|
-
}, [
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
);
|
|
3210
|
+
}, []);
|
|
3211
|
+
const cancelPending = useCallback10(() => {
|
|
3212
|
+
if (timerRef.current) {
|
|
3213
|
+
clearTimeout(timerRef.current);
|
|
3214
|
+
timerRef.current = null;
|
|
3215
|
+
}
|
|
3216
|
+
pendingWriteRef.current = null;
|
|
3217
|
+
}, []);
|
|
3218
|
+
useEffect11(() => {
|
|
3219
|
+
const currentStorage = storageRef.current;
|
|
3220
|
+
if (currentStorage) {
|
|
3221
|
+
try {
|
|
3222
|
+
applyNext(sortAnnotations(parseAnnotations(currentStorage.getItem(key))));
|
|
3223
|
+
} catch {
|
|
3224
|
+
applyNext([]);
|
|
3225
|
+
}
|
|
3226
|
+
} else {
|
|
3227
|
+
applyNext([]);
|
|
3228
|
+
}
|
|
3229
|
+
return flushPending;
|
|
3230
|
+
}, [key, applyNext, flushPending]);
|
|
3063
3231
|
const onChangeRef = useRef14(options.onChange);
|
|
3064
3232
|
onChangeRef.current = options.onChange;
|
|
3065
3233
|
const commit = useCallback10(
|
|
3066
3234
|
(next) => {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
}, throttleMs);
|
|
3235
|
+
const currentStorage = storageRef.current;
|
|
3236
|
+
if (currentStorage) {
|
|
3237
|
+
cancelPending();
|
|
3238
|
+
pendingWriteRef.current = () => {
|
|
3239
|
+
currentStorage.setItem(key, serializeAnnotations(next));
|
|
3240
|
+
};
|
|
3241
|
+
timerRef.current = setTimeout(flushPending, throttleMs);
|
|
3075
3242
|
}
|
|
3076
3243
|
onChangeRef.current?.(next);
|
|
3077
3244
|
},
|
|
3078
|
-
[
|
|
3245
|
+
[key, throttleMs, cancelPending, flushPending]
|
|
3079
3246
|
);
|
|
3080
3247
|
const add = useCallback10(
|
|
3081
3248
|
(input) => {
|
|
@@ -3084,61 +3251,60 @@ function useAnnotations(options) {
|
|
|
3084
3251
|
id: input.id ?? createAnnotationId(),
|
|
3085
3252
|
createdAt: input.createdAt ?? Date.now()
|
|
3086
3253
|
};
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
return next;
|
|
3091
|
-
});
|
|
3254
|
+
const next = sortAnnotations([...annotationsRef.current, annotation]);
|
|
3255
|
+
applyNext(next);
|
|
3256
|
+
commit(next);
|
|
3092
3257
|
return annotation;
|
|
3093
3258
|
},
|
|
3094
|
-
[commit]
|
|
3259
|
+
[commit, applyNext]
|
|
3095
3260
|
);
|
|
3096
3261
|
const remove = useCallback10(
|
|
3097
3262
|
(id) => {
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3263
|
+
const current = annotationsRef.current;
|
|
3264
|
+
const next = current.filter((annotation) => annotation.id !== id);
|
|
3265
|
+
if (next.length === current.length) return;
|
|
3266
|
+
applyNext(next);
|
|
3267
|
+
commit(next);
|
|
3103
3268
|
},
|
|
3104
|
-
[commit]
|
|
3269
|
+
[commit, applyNext]
|
|
3105
3270
|
);
|
|
3106
3271
|
const update = useCallback10(
|
|
3107
3272
|
(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;
|
|
3273
|
+
let changed = false;
|
|
3274
|
+
const next = annotationsRef.current.map((annotation) => {
|
|
3275
|
+
if (annotation.id !== id) return annotation;
|
|
3276
|
+
changed = true;
|
|
3277
|
+
return { ...annotation, ...patch, id };
|
|
3119
3278
|
});
|
|
3279
|
+
if (!changed) return;
|
|
3280
|
+
const sorted = sortAnnotations(next);
|
|
3281
|
+
applyNext(sorted);
|
|
3282
|
+
commit(sorted);
|
|
3120
3283
|
},
|
|
3121
|
-
[commit]
|
|
3284
|
+
[commit, applyNext]
|
|
3122
3285
|
);
|
|
3123
3286
|
const clear = useCallback10(() => {
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3287
|
+
applyNext([]);
|
|
3288
|
+
const currentStorage = storageRef.current;
|
|
3289
|
+
if (currentStorage) {
|
|
3290
|
+
cancelPending();
|
|
3127
3291
|
try {
|
|
3128
|
-
|
|
3292
|
+
currentStorage.removeItem(key);
|
|
3129
3293
|
} catch {
|
|
3130
3294
|
}
|
|
3131
3295
|
}
|
|
3132
3296
|
onChangeRef.current?.([]);
|
|
3133
|
-
}, [
|
|
3297
|
+
}, [key, cancelPending, applyNext]);
|
|
3134
3298
|
return { annotations, add, remove, update, clear };
|
|
3135
3299
|
}
|
|
3136
3300
|
|
|
3137
3301
|
// src/useEditableEpub.ts
|
|
3138
3302
|
import {
|
|
3303
|
+
clampEditableEpubSelection,
|
|
3304
|
+
cloneEditableEpubBook as cloneEditableEpubBook2,
|
|
3139
3305
|
EditableEpub as EditableEpub2
|
|
3140
3306
|
} from "@libraz/mejiro/epub";
|
|
3141
|
-
import { useCallback as useCallback11, useEffect as
|
|
3307
|
+
import { useCallback as useCallback11, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef15, useState as useState14 } from "react";
|
|
3142
3308
|
function useEditableEpub(options = {}) {
|
|
3143
3309
|
const [editor, setEditor] = useState14(null);
|
|
3144
3310
|
const [loading, setLoading] = useState14(false);
|
|
@@ -3153,15 +3319,17 @@ function useEditableEpub(options = {}) {
|
|
|
3153
3319
|
const onLoadRef = useRef15(options.onLoad);
|
|
3154
3320
|
const onErrorRef = useRef15(options.onError);
|
|
3155
3321
|
const onExportRef = useRef15(options.onExport);
|
|
3322
|
+
const limitsRef = useRef15(options.limits);
|
|
3156
3323
|
onLoadRef.current = options.onLoad;
|
|
3157
3324
|
onErrorRef.current = options.onError;
|
|
3158
3325
|
onExportRef.current = options.onExport;
|
|
3326
|
+
limitsRef.current = options.limits;
|
|
3159
3327
|
const loadBufferWithRequest = useCallback11(
|
|
3160
3328
|
async (buffer, requestId) => {
|
|
3161
3329
|
setLoading(true);
|
|
3162
3330
|
setError(null);
|
|
3163
3331
|
try {
|
|
3164
|
-
const next = await EditableEpub2.load(buffer);
|
|
3332
|
+
const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
|
|
3165
3333
|
if (requestId !== requestIdRef.current) return null;
|
|
3166
3334
|
setEditor(next);
|
|
3167
3335
|
setSelectionState({ chapter: 0, paragraph: 0 });
|
|
@@ -3228,19 +3396,19 @@ function useEditableEpub(options = {}) {
|
|
|
3228
3396
|
},
|
|
3229
3397
|
[loadBufferWithRequest]
|
|
3230
3398
|
);
|
|
3231
|
-
|
|
3399
|
+
useEffect12(() => {
|
|
3232
3400
|
if (options.defaultUrl) void loadUrl(options.defaultUrl);
|
|
3233
3401
|
}, [options.defaultUrl, loadUrl]);
|
|
3234
3402
|
const book = editor?.book ?? null;
|
|
3235
3403
|
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3236
3404
|
const previewBook = useMemo8(() => {
|
|
3237
3405
|
void revision;
|
|
3238
|
-
return book ?
|
|
3406
|
+
return book ? cloneEditableEpubBook2(book) : null;
|
|
3239
3407
|
}, [book, revision]);
|
|
3240
3408
|
const history = editor?.history ?? null;
|
|
3241
3409
|
const setSelection = useCallback11(
|
|
3242
3410
|
(nextSelection) => {
|
|
3243
|
-
setSelectionState(
|
|
3411
|
+
setSelectionState(clampEditableEpubSelection(book, nextSelection));
|
|
3244
3412
|
},
|
|
3245
3413
|
[book]
|
|
3246
3414
|
);
|
|
@@ -3320,46 +3488,13 @@ function useEditableEpub(options = {}) {
|
|
|
3320
3488
|
exportEpub
|
|
3321
3489
|
};
|
|
3322
3490
|
}
|
|
3323
|
-
function cloneBook2(book) {
|
|
3324
|
-
return {
|
|
3325
|
-
...book,
|
|
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)));
|
|
3355
|
-
}
|
|
3356
3491
|
|
|
3357
3492
|
// src/useEpubProject.ts
|
|
3358
3493
|
import {
|
|
3359
3494
|
EpubProject as EpubProject2,
|
|
3360
3495
|
parseEpub as parseEpub2
|
|
3361
3496
|
} from "@libraz/mejiro/epub";
|
|
3362
|
-
import { useCallback as useCallback12, useEffect as
|
|
3497
|
+
import { useCallback as useCallback12, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
|
|
3363
3498
|
function useEpubProject(options = {}) {
|
|
3364
3499
|
const defaultTitle = options.defaultChapterTitle;
|
|
3365
3500
|
const defaultBody = options.defaultChapterBody;
|
|
@@ -3372,6 +3507,8 @@ function useEpubProject(options = {}) {
|
|
|
3372
3507
|
options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
|
|
3373
3508
|
);
|
|
3374
3509
|
const [selectedChapter, setSelectedChapterState] = useState15(0);
|
|
3510
|
+
const [cover, setCoverState] = useState15(options.cover ?? null);
|
|
3511
|
+
const [assets, setAssetsState] = useState15(options.assets ?? []);
|
|
3375
3512
|
const [previewBook, setPreviewBook] = useState15(null);
|
|
3376
3513
|
const [previewError, setPreviewError] = useState15(null);
|
|
3377
3514
|
const [previewing, setPreviewing] = useState15(false);
|
|
@@ -3380,8 +3517,8 @@ function useEpubProject(options = {}) {
|
|
|
3380
3517
|
const onExportRef = useRef16(options.onExport);
|
|
3381
3518
|
onPreviewRef.current = options.onPreview;
|
|
3382
3519
|
onExportRef.current = options.onExport;
|
|
3383
|
-
const buildProject = useCallback12(
|
|
3384
|
-
|
|
3520
|
+
const buildProject = useCallback12(() => {
|
|
3521
|
+
const project = EpubProject2.fromManuscript({
|
|
3385
3522
|
metadata,
|
|
3386
3523
|
includeTitlePage: false,
|
|
3387
3524
|
includeTitleInFirstChapter: true,
|
|
@@ -3389,13 +3526,15 @@ function useEpubProject(options = {}) {
|
|
|
3389
3526
|
id: chapter.id,
|
|
3390
3527
|
title: chapter.title || "Untitled",
|
|
3391
3528
|
body: chapter.body
|
|
3392
|
-
}))
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3529
|
+
})),
|
|
3530
|
+
...cover ? { cover } : {}
|
|
3531
|
+
});
|
|
3532
|
+
for (const asset of assets) project.addAsset(asset);
|
|
3533
|
+
return project;
|
|
3534
|
+
}, [assets, chapters, cover, metadata]);
|
|
3396
3535
|
const assetResolverRef = useRef16(options.assetResolver);
|
|
3397
3536
|
assetResolverRef.current = options.assetResolver;
|
|
3398
|
-
|
|
3537
|
+
useEffect13(() => {
|
|
3399
3538
|
const requestId = ++previewRequestIdRef.current;
|
|
3400
3539
|
let cancelled = false;
|
|
3401
3540
|
setPreviewing(true);
|
|
@@ -3441,6 +3580,12 @@ function useEpubProject(options = {}) {
|
|
|
3441
3580
|
},
|
|
3442
3581
|
[chapters, defaultBody, defaultTitle]
|
|
3443
3582
|
);
|
|
3583
|
+
const setCover = useCallback12((asset) => {
|
|
3584
|
+
setCoverState(asset);
|
|
3585
|
+
}, []);
|
|
3586
|
+
const setAssets = useCallback12((next) => {
|
|
3587
|
+
setAssetsState(next);
|
|
3588
|
+
}, []);
|
|
3444
3589
|
const setSelectedChapter = useCallback12(
|
|
3445
3590
|
(index) => {
|
|
3446
3591
|
setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
|
|
@@ -3515,12 +3660,16 @@ function useEpubProject(options = {}) {
|
|
|
3515
3660
|
chapters,
|
|
3516
3661
|
selectedChapter,
|
|
3517
3662
|
currentChapter,
|
|
3663
|
+
cover,
|
|
3664
|
+
assets,
|
|
3518
3665
|
previewBook,
|
|
3519
3666
|
previewError,
|
|
3520
3667
|
previewing,
|
|
3521
3668
|
setMetadata,
|
|
3522
3669
|
setChapters,
|
|
3523
3670
|
setSelectedChapter,
|
|
3671
|
+
setCover,
|
|
3672
|
+
setAssets,
|
|
3524
3673
|
patchChapter,
|
|
3525
3674
|
addChapter,
|
|
3526
3675
|
removeChapter,
|
|
@@ -3530,8 +3679,10 @@ function useEpubProject(options = {}) {
|
|
|
3530
3679
|
}),
|
|
3531
3680
|
[
|
|
3532
3681
|
addChapter,
|
|
3682
|
+
assets,
|
|
3533
3683
|
buildProject,
|
|
3534
3684
|
chapters,
|
|
3685
|
+
cover,
|
|
3535
3686
|
currentChapter,
|
|
3536
3687
|
exportEpub,
|
|
3537
3688
|
metadata,
|
|
@@ -3542,7 +3693,9 @@ function useEpubProject(options = {}) {
|
|
|
3542
3693
|
reorderChapters,
|
|
3543
3694
|
removeChapter,
|
|
3544
3695
|
selectedChapter,
|
|
3696
|
+
setAssets,
|
|
3545
3697
|
setChapters,
|
|
3698
|
+
setCover,
|
|
3546
3699
|
setMetadata,
|
|
3547
3700
|
setSelectedChapter
|
|
3548
3701
|
]
|
|
@@ -3557,10 +3710,10 @@ function defaultChapter3(index, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71"
|
|
|
3557
3710
|
}
|
|
3558
3711
|
|
|
3559
3712
|
// src/useImageOverlay.ts
|
|
3560
|
-
import {
|
|
3713
|
+
import { createOverlayDragSession as createOverlayDragSession2 } from "@libraz/mejiro/browser";
|
|
3561
3714
|
import {
|
|
3562
3715
|
useCallback as useCallback13,
|
|
3563
|
-
useEffect as
|
|
3716
|
+
useEffect as useEffect14,
|
|
3564
3717
|
useRef as useRef17,
|
|
3565
3718
|
useState as useState16
|
|
3566
3719
|
} from "react";
|
|
@@ -3580,7 +3733,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3580
3733
|
const onUpdateRef = useRef17(onUpdate);
|
|
3581
3734
|
onUpdateRef.current = onUpdate;
|
|
3582
3735
|
const activeDragCleanupsRef = useRef17(/* @__PURE__ */ new Set());
|
|
3583
|
-
|
|
3736
|
+
useEffect14(
|
|
3584
3737
|
() => () => {
|
|
3585
3738
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
3586
3739
|
activeDragCleanupsRef.current.clear();
|
|
@@ -3597,6 +3750,20 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3597
3750
|
},
|
|
3598
3751
|
[margin]
|
|
3599
3752
|
);
|
|
3753
|
+
const syncedRef = useRef17({
|
|
3754
|
+
layout,
|
|
3755
|
+
spreadIdx
|
|
3756
|
+
});
|
|
3757
|
+
useEffect14(() => {
|
|
3758
|
+
const prev = syncedRef.current;
|
|
3759
|
+
syncedRef.current = { layout, spreadIdx };
|
|
3760
|
+
if (prev.layout === layout && prev.spreadIdx === spreadIdx) return;
|
|
3761
|
+
if (rectRef.current === null) return;
|
|
3762
|
+
if (prev.layout && prev.spreadIdx !== spreadIdx) {
|
|
3763
|
+
prev.layout.syncImages(prev.spreadIdx, void 0);
|
|
3764
|
+
}
|
|
3765
|
+
syncToLayout(rectRef.current);
|
|
3766
|
+
}, [layout, spreadIdx, syncToLayout]);
|
|
3600
3767
|
const toggleImage = useCallback13(() => {
|
|
3601
3768
|
if (rectRef.current) {
|
|
3602
3769
|
setImageRect(null);
|
|
@@ -3607,42 +3774,33 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3607
3774
|
syncToLayout(r);
|
|
3608
3775
|
}
|
|
3609
3776
|
}, [defX, defY, defW, defH, syncToLayout]);
|
|
3777
|
+
const applyRect = useCallback13(
|
|
3778
|
+
(rect) => {
|
|
3779
|
+
setImageRect(rect);
|
|
3780
|
+
syncToLayout(rect);
|
|
3781
|
+
},
|
|
3782
|
+
[syncToLayout]
|
|
3783
|
+
);
|
|
3610
3784
|
const onOverlayPointerDown = useCallback13(
|
|
3611
3785
|
(e) => {
|
|
3612
3786
|
e.preventDefault();
|
|
3613
3787
|
const current = rectRef.current;
|
|
3614
3788
|
if (!current) return;
|
|
3615
|
-
const startX = e.clientX;
|
|
3616
|
-
const startY = e.clientY;
|
|
3617
|
-
const start = { ...current };
|
|
3618
3789
|
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);
|
|
3790
|
+
createOverlayDragSession2({
|
|
3791
|
+
mode: "move",
|
|
3792
|
+
rect: current,
|
|
3793
|
+
startX: e.clientX,
|
|
3794
|
+
startY: e.clientY,
|
|
3795
|
+
pointerId: e.nativeEvent.pointerId,
|
|
3796
|
+
captureElement: target,
|
|
3797
|
+
activeElement: target,
|
|
3798
|
+
dragClass: "dragging",
|
|
3799
|
+
registry: activeDragCleanupsRef.current,
|
|
3800
|
+
onChange: applyRect
|
|
3801
|
+
});
|
|
3644
3802
|
},
|
|
3645
|
-
[
|
|
3803
|
+
[applyRect]
|
|
3646
3804
|
);
|
|
3647
3805
|
const onResizePointerDown = useCallback13(
|
|
3648
3806
|
(e) => {
|
|
@@ -3650,37 +3808,21 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3650
3808
|
e.stopPropagation();
|
|
3651
3809
|
const current = rectRef.current;
|
|
3652
3810
|
if (!current) return;
|
|
3653
|
-
const startX = e.clientX;
|
|
3654
|
-
const startY = e.clientY;
|
|
3655
|
-
const start = { ...current };
|
|
3656
3811
|
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);
|
|
3812
|
+
createOverlayDragSession2({
|
|
3813
|
+
mode: "resize",
|
|
3814
|
+
rect: current,
|
|
3815
|
+
startX: e.clientX,
|
|
3816
|
+
startY: e.clientY,
|
|
3817
|
+
pointerId: e.nativeEvent.pointerId,
|
|
3818
|
+
captureElement: target,
|
|
3819
|
+
activeElement: target.parentElement,
|
|
3820
|
+
dragClass: "dragging",
|
|
3821
|
+
registry: activeDragCleanupsRef.current,
|
|
3822
|
+
onChange: applyRect
|
|
3823
|
+
});
|
|
3682
3824
|
},
|
|
3683
|
-
[
|
|
3825
|
+
[applyRect]
|
|
3684
3826
|
);
|
|
3685
3827
|
return {
|
|
3686
3828
|
imageRect,
|
|
@@ -3692,7 +3834,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
3692
3834
|
}
|
|
3693
3835
|
|
|
3694
3836
|
// src/useLibrary.ts
|
|
3695
|
-
import { useCallback as useCallback14, useEffect as
|
|
3837
|
+
import { useCallback as useCallback14, useEffect as useEffect15, useMemo as useMemo10, useState as useState17 } from "react";
|
|
3696
3838
|
function useLibrary(options) {
|
|
3697
3839
|
const { volumes, initialVolumeId, onChange } = options;
|
|
3698
3840
|
const initialIndex = initialVolumeId != null ? Math.max(
|
|
@@ -3702,7 +3844,7 @@ function useLibrary(options) {
|
|
|
3702
3844
|
const [currentId, setCurrentId] = useState17(
|
|
3703
3845
|
volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
|
|
3704
3846
|
);
|
|
3705
|
-
|
|
3847
|
+
useEffect15(() => {
|
|
3706
3848
|
if (volumes.length === 0) {
|
|
3707
3849
|
if (currentId !== null) setCurrentId(null);
|
|
3708
3850
|
return;
|
|
@@ -3760,7 +3902,13 @@ function useLibrary(options) {
|
|
|
3760
3902
|
}
|
|
3761
3903
|
|
|
3762
3904
|
// src/useManuscriptLayout.ts
|
|
3763
|
-
import {
|
|
3905
|
+
import {
|
|
3906
|
+
useCallback as useCallback15,
|
|
3907
|
+
useEffect as useEffect16,
|
|
3908
|
+
useLayoutEffect as useLayoutEffect5,
|
|
3909
|
+
useRef as useRef18,
|
|
3910
|
+
useState as useState18
|
|
3911
|
+
} from "react";
|
|
3764
3912
|
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
3765
3913
|
const dialect = options.dialect ?? "mejiro";
|
|
3766
3914
|
const enableResize = options.enableResize ?? true;
|
|
@@ -3771,43 +3919,56 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
|
3771
3919
|
const [contentHeight, setContentHeight] = useState18(0);
|
|
3772
3920
|
const [elapsedMs, setElapsedMs] = useState18(0);
|
|
3773
3921
|
const requestIdRef = useRef18(0);
|
|
3774
|
-
const
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3922
|
+
const layoutRef = useRef18(null);
|
|
3923
|
+
layoutRef.current = layout;
|
|
3924
|
+
const pendingRestore = useRef18(null);
|
|
3925
|
+
const optionsRef = useRef18(options);
|
|
3926
|
+
optionsRef.current = options;
|
|
3927
|
+
const recompute = useCallback15(
|
|
3928
|
+
async (opts = {}) => {
|
|
3929
|
+
const blank = opts.blank ?? true;
|
|
3930
|
+
const requestId = ++requestIdRef.current;
|
|
3931
|
+
if (!(chapter && surface.current)) {
|
|
3932
|
+
setLayout(null);
|
|
3933
|
+
setPageWidth(0);
|
|
3934
|
+
setPageHeight(0);
|
|
3935
|
+
setContentHeight(0);
|
|
3936
|
+
setElapsedMs(0);
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
const captured = !blank && layoutRef.current ? optionsRef.current.capturePosition?.(layoutRef.current) ?? null : null;
|
|
3940
|
+
pendingRestore.current = captured;
|
|
3941
|
+
if (blank) setLayout(null);
|
|
3942
|
+
const dims = book.computePageSize(surface.current);
|
|
3943
|
+
setPageWidth(dims.pageWidth);
|
|
3944
|
+
setPageHeight(dims.pageHeight);
|
|
3945
|
+
setContentHeight(dims.contentHeight);
|
|
3946
|
+
const t0 = performance.now();
|
|
3947
|
+
const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
|
|
3948
|
+
if (requestId !== requestIdRef.current) return;
|
|
3949
|
+
const next = layouts.values().next().value ?? null;
|
|
3950
|
+
layoutRef.current = next;
|
|
3951
|
+
setLayout(next);
|
|
3952
|
+
setElapsedMs(performance.now() - t0);
|
|
3953
|
+
},
|
|
3954
|
+
[book, chapter, dialect, surface]
|
|
3955
|
+
);
|
|
3796
3956
|
useLayoutEffect5(() => {
|
|
3797
3957
|
requestIdRef.current++;
|
|
3958
|
+
pendingRestore.current = null;
|
|
3798
3959
|
setLayout(null);
|
|
3799
3960
|
setPageWidth(0);
|
|
3800
3961
|
setPageHeight(0);
|
|
3801
3962
|
setContentHeight(0);
|
|
3802
3963
|
setElapsedMs(0);
|
|
3803
3964
|
}, [recompute]);
|
|
3804
|
-
|
|
3965
|
+
useEffect16(() => {
|
|
3805
3966
|
void recompute();
|
|
3806
3967
|
return () => {
|
|
3807
3968
|
requestIdRef.current++;
|
|
3808
3969
|
};
|
|
3809
3970
|
}, [recompute]);
|
|
3810
|
-
|
|
3971
|
+
useEffect16(() => {
|
|
3811
3972
|
if (!enableResize) return;
|
|
3812
3973
|
const el = surface.current;
|
|
3813
3974
|
if (!el) return;
|
|
@@ -3822,12 +3983,12 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
|
3822
3983
|
const scheduleReflow = (immediate) => {
|
|
3823
3984
|
clear();
|
|
3824
3985
|
if (immediate) {
|
|
3825
|
-
void recompute();
|
|
3986
|
+
void recompute({ blank: false });
|
|
3826
3987
|
return;
|
|
3827
3988
|
}
|
|
3828
3989
|
timer = setTimeout(() => {
|
|
3829
3990
|
timer = null;
|
|
3830
|
-
void recompute();
|
|
3991
|
+
void recompute({ blank: false });
|
|
3831
3992
|
}, resizeDebounce);
|
|
3832
3993
|
};
|
|
3833
3994
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -3849,7 +4010,7 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
|
3849
4010
|
clear();
|
|
3850
4011
|
};
|
|
3851
4012
|
}, [surface, enableResize, resizeDebounce, recompute]);
|
|
3852
|
-
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
|
|
4013
|
+
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
|
|
3853
4014
|
}
|
|
3854
4015
|
|
|
3855
4016
|
// src/useReadingPosition.ts
|
|
@@ -3857,7 +4018,7 @@ import {
|
|
|
3857
4018
|
parseReadingPosition,
|
|
3858
4019
|
serializeReadingPosition
|
|
3859
4020
|
} from "@libraz/mejiro";
|
|
3860
|
-
import { useCallback as useCallback16, useEffect as
|
|
4021
|
+
import { useCallback as useCallback16, useEffect as useEffect17, useRef as useRef19, useState as useState19 } from "react";
|
|
3861
4022
|
function resolveDefaultStorage2() {
|
|
3862
4023
|
if (typeof globalThis === "undefined") return null;
|
|
3863
4024
|
const w = globalThis.localStorage;
|
|
@@ -3876,25 +4037,41 @@ function useReadingPosition(options) {
|
|
|
3876
4037
|
});
|
|
3877
4038
|
const storageRef = useRef19(storage);
|
|
3878
4039
|
storageRef.current = storage;
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
4040
|
+
const timerRef = useRef19(null);
|
|
4041
|
+
const pendingWriteRef = useRef19(null);
|
|
4042
|
+
const flushPending = useCallback16(() => {
|
|
4043
|
+
if (timerRef.current) {
|
|
4044
|
+
clearTimeout(timerRef.current);
|
|
4045
|
+
timerRef.current = null;
|
|
3884
4046
|
}
|
|
4047
|
+
const write = pendingWriteRef.current;
|
|
4048
|
+
pendingWriteRef.current = null;
|
|
4049
|
+
if (!write) return;
|
|
3885
4050
|
try {
|
|
3886
|
-
|
|
4051
|
+
write();
|
|
3887
4052
|
} catch {
|
|
4053
|
+
}
|
|
4054
|
+
}, []);
|
|
4055
|
+
const cancelPending = useCallback16(() => {
|
|
4056
|
+
if (timerRef.current) {
|
|
4057
|
+
clearTimeout(timerRef.current);
|
|
4058
|
+
timerRef.current = null;
|
|
4059
|
+
}
|
|
4060
|
+
pendingWriteRef.current = null;
|
|
4061
|
+
}, []);
|
|
4062
|
+
useEffect17(() => {
|
|
4063
|
+
const currentStorage = storageRef.current;
|
|
4064
|
+
if (currentStorage) {
|
|
4065
|
+
try {
|
|
4066
|
+
setPosition(parseReadingPosition(currentStorage.getItem(key)));
|
|
4067
|
+
} catch {
|
|
4068
|
+
setPosition(null);
|
|
4069
|
+
}
|
|
4070
|
+
} else {
|
|
3888
4071
|
setPosition(null);
|
|
3889
4072
|
}
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
useEffect16(
|
|
3893
|
-
() => () => {
|
|
3894
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3895
|
-
},
|
|
3896
|
-
[]
|
|
3897
|
-
);
|
|
4073
|
+
return flushPending;
|
|
4074
|
+
}, [key, flushPending]);
|
|
3898
4075
|
const onChangeRef = useRef19(options.onChange);
|
|
3899
4076
|
onChangeRef.current = options.onChange;
|
|
3900
4077
|
const save = useCallback16(
|
|
@@ -3902,21 +4079,19 @@ function useReadingPosition(options) {
|
|
|
3902
4079
|
setPosition(next);
|
|
3903
4080
|
const currentStorage = storageRef.current;
|
|
3904
4081
|
if (currentStorage) {
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
}
|
|
3911
|
-
}, throttleMs);
|
|
4082
|
+
cancelPending();
|
|
4083
|
+
pendingWriteRef.current = () => {
|
|
4084
|
+
currentStorage.setItem(key, serializeReadingPosition(next));
|
|
4085
|
+
};
|
|
4086
|
+
timerRef.current = setTimeout(flushPending, throttleMs);
|
|
3912
4087
|
}
|
|
3913
4088
|
onChangeRef.current?.(next);
|
|
3914
4089
|
},
|
|
3915
|
-
[key, throttleMs]
|
|
4090
|
+
[key, throttleMs, cancelPending, flushPending]
|
|
3916
4091
|
);
|
|
3917
4092
|
const clear = useCallback16(() => {
|
|
3918
4093
|
setPosition(null);
|
|
3919
|
-
|
|
4094
|
+
cancelPending();
|
|
3920
4095
|
const currentStorage = storageRef.current;
|
|
3921
4096
|
if (currentStorage) {
|
|
3922
4097
|
try {
|
|
@@ -3925,7 +4100,7 @@ function useReadingPosition(options) {
|
|
|
3925
4100
|
}
|
|
3926
4101
|
}
|
|
3927
4102
|
onChangeRef.current?.(null);
|
|
3928
|
-
}, [key]);
|
|
4103
|
+
}, [key, cancelPending]);
|
|
3929
4104
|
return { position, save, clear };
|
|
3930
4105
|
}
|
|
3931
4106
|
|