@libraz/mejiro-react 0.9.1 → 0.10.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 +3 -1
- package/dist/index.d.ts +79 -34
- package/dist/index.js +1338 -1012
- package/package.json +4 -4
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,29 @@ import {
|
|
|
173
176
|
EditableEpub,
|
|
174
177
|
exportEditableEpub
|
|
175
178
|
} from "@libraz/mejiro/epub";
|
|
176
|
-
import { useCallback as
|
|
179
|
+
import { useCallback as useCallback8, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef13, useState as useState9 } from "react";
|
|
180
|
+
|
|
181
|
+
// src/bookIdentity.ts
|
|
182
|
+
var identities = /* @__PURE__ */ new WeakMap();
|
|
183
|
+
function markBookIdentity(derived, identity) {
|
|
184
|
+
identities.set(derived, identity);
|
|
185
|
+
return derived;
|
|
186
|
+
}
|
|
187
|
+
function bookIdentity(book) {
|
|
188
|
+
return book && identities.get(book) || book;
|
|
189
|
+
}
|
|
190
|
+
function chapterContentKey(chapter) {
|
|
191
|
+
return JSON.stringify([chapter.title, chapter.paragraphs]);
|
|
192
|
+
}
|
|
193
|
+
function resolveLayoutEpub(prev, epub, book, chapter) {
|
|
194
|
+
const source = epub?.chapters[chapter];
|
|
195
|
+
if (prev && prev.book === book && prev.chapter === chapter && prev.source === source) return prev;
|
|
196
|
+
const key = source ? chapterContentKey(source) : null;
|
|
197
|
+
if (prev?.epub && prev.book === book && prev.chapter === chapter && prev.key === key) {
|
|
198
|
+
return { ...prev, source };
|
|
199
|
+
}
|
|
200
|
+
return { book, chapter, source, key, epub };
|
|
201
|
+
}
|
|
177
202
|
|
|
178
203
|
// src/errors.ts
|
|
179
204
|
import { assertEpubInputSize } from "@libraz/mejiro/epub";
|
|
@@ -202,14 +227,15 @@ async function withErrorReporting(task, report) {
|
|
|
202
227
|
import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
|
|
203
228
|
import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
|
|
204
229
|
import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
|
|
230
|
+
import { pageNumberFor as pageNumberFor2 } from "@libraz/mejiro/render";
|
|
205
231
|
import {
|
|
206
232
|
forwardRef,
|
|
207
|
-
useCallback as
|
|
208
|
-
useEffect as
|
|
233
|
+
useCallback as useCallback7,
|
|
234
|
+
useEffect as useEffect9,
|
|
209
235
|
useImperativeHandle,
|
|
210
236
|
useLayoutEffect as useLayoutEffect4,
|
|
211
|
-
useMemo as
|
|
212
|
-
useRef as
|
|
237
|
+
useMemo as useMemo5,
|
|
238
|
+
useRef as useRef12,
|
|
213
239
|
useState as useState8
|
|
214
240
|
} from "react";
|
|
215
241
|
|
|
@@ -225,7 +251,10 @@ function MejiroPageIndicator({ current, total }) {
|
|
|
225
251
|
|
|
226
252
|
// src/MejiroScrollView.tsx
|
|
227
253
|
import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
|
|
254
|
+
import { pageNumberFor } from "@libraz/mejiro/render";
|
|
228
255
|
import {
|
|
256
|
+
memo,
|
|
257
|
+
useCallback as useCallback2,
|
|
229
258
|
useEffect as useEffect2,
|
|
230
259
|
useLayoutEffect,
|
|
231
260
|
useMemo as useMemo3,
|
|
@@ -324,10 +353,7 @@ function paragraphScaleStyle(scale) {
|
|
|
324
353
|
function MejiroPage({ page, className, style }) {
|
|
325
354
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
326
355
|
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
|
-
);
|
|
356
|
+
const paraClass = paragraphClassName(paragraph.kind, paragraph.headingLevel);
|
|
331
357
|
return (
|
|
332
358
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
333
359
|
/* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
@@ -468,6 +494,8 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
468
494
|
spreadIdxRef.current = spreadIdx;
|
|
469
495
|
const onUpdateRef = useRef2(options.onUpdate);
|
|
470
496
|
onUpdateRef.current = options.onUpdate;
|
|
497
|
+
const marginRef = useRef2(margin);
|
|
498
|
+
marginRef.current = margin;
|
|
471
499
|
const activeDragCleanupsRef = useRef2(/* @__PURE__ */ new Set());
|
|
472
500
|
useEffect(
|
|
473
501
|
() => () => {
|
|
@@ -476,15 +504,12 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
476
504
|
},
|
|
477
505
|
[]
|
|
478
506
|
);
|
|
479
|
-
const syncSpreads = useCallback(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
},
|
|
486
|
-
[margin]
|
|
487
|
-
);
|
|
507
|
+
const syncSpreads = useCallback((spreads) => {
|
|
508
|
+
const lo = layoutRef.current;
|
|
509
|
+
if (!lo) return;
|
|
510
|
+
applyImages(lo, imagesRef.current, spreads, marginRef.current);
|
|
511
|
+
for (const si of spreads) onUpdateRef.current?.(lo.getSpread(si));
|
|
512
|
+
}, []);
|
|
488
513
|
const appliedRef = useRef2({
|
|
489
514
|
layout: null,
|
|
490
515
|
margin
|
|
@@ -649,6 +674,71 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
649
674
|
// src/MejiroScrollView.tsx
|
|
650
675
|
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
651
676
|
var NO_IMAGES = [];
|
|
677
|
+
var ScrollPage = memo(function ScrollPage2({
|
|
678
|
+
result,
|
|
679
|
+
index,
|
|
680
|
+
pageNumber,
|
|
681
|
+
pageWidth,
|
|
682
|
+
pageHeight,
|
|
683
|
+
contentHeight,
|
|
684
|
+
slotMode,
|
|
685
|
+
fontFamily,
|
|
686
|
+
lineSpacing,
|
|
687
|
+
contentStyle,
|
|
688
|
+
rects,
|
|
689
|
+
overlays,
|
|
690
|
+
overlaysLeft,
|
|
691
|
+
registerPage,
|
|
692
|
+
onImagePointerDown,
|
|
693
|
+
onImageResizePointerDown,
|
|
694
|
+
onImageClose
|
|
695
|
+
}) {
|
|
696
|
+
return /* @__PURE__ */ jsxs5(
|
|
697
|
+
"div",
|
|
698
|
+
{
|
|
699
|
+
ref: (el) => registerPage(index, el),
|
|
700
|
+
"data-page-idx": index,
|
|
701
|
+
className: "mejiro-reader-page",
|
|
702
|
+
style: {
|
|
703
|
+
width: pageWidth,
|
|
704
|
+
height: pageHeight,
|
|
705
|
+
flexShrink: 0,
|
|
706
|
+
...overlays.length > 0 ? { overflow: "visible" } : null
|
|
707
|
+
},
|
|
708
|
+
children: [
|
|
709
|
+
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-rule" }),
|
|
710
|
+
/* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-header", children: [
|
|
711
|
+
/* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-title" }),
|
|
712
|
+
/* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-num", children: pageNumber != null ? String(pageNumber) : "" })
|
|
713
|
+
] }),
|
|
714
|
+
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
|
|
715
|
+
/* @__PURE__ */ jsx9(
|
|
716
|
+
MejiroPageView,
|
|
717
|
+
{
|
|
718
|
+
result,
|
|
719
|
+
slotMode,
|
|
720
|
+
fontFamily,
|
|
721
|
+
lineSpacing,
|
|
722
|
+
className: "mejiro-reader-page-content",
|
|
723
|
+
style: contentStyle
|
|
724
|
+
}
|
|
725
|
+
),
|
|
726
|
+
rects && /* @__PURE__ */ jsx9(MejiroSelectionLayer, { rects, side: index % 2 === 0 ? "right" : "left" })
|
|
727
|
+
] }) }),
|
|
728
|
+
overlays.map((item) => /* @__PURE__ */ jsx9(
|
|
729
|
+
MejiroImageOverlay,
|
|
730
|
+
{
|
|
731
|
+
rect: overlaysLeft ? { ...item.rect, x: item.rect.x + pageWidth } : item.rect,
|
|
732
|
+
onOverlayPointerDown: (e) => onImagePointerDown(item.id, e),
|
|
733
|
+
onResizePointerDown: (e) => onImageResizePointerDown(item.id, e),
|
|
734
|
+
onClose: () => onImageClose(item.id)
|
|
735
|
+
},
|
|
736
|
+
item.id
|
|
737
|
+
))
|
|
738
|
+
]
|
|
739
|
+
}
|
|
740
|
+
);
|
|
741
|
+
});
|
|
652
742
|
function MejiroScrollView({
|
|
653
743
|
layout,
|
|
654
744
|
pageWidth,
|
|
@@ -664,6 +754,7 @@ function MejiroScrollView({
|
|
|
664
754
|
selectionRects,
|
|
665
755
|
spreadIdx,
|
|
666
756
|
images = NO_IMAGES,
|
|
757
|
+
pageNumbers = "both",
|
|
667
758
|
onImagePointerDown,
|
|
668
759
|
onImageResizePointerDown,
|
|
669
760
|
onImageClose
|
|
@@ -698,10 +789,30 @@ function MejiroScrollView({
|
|
|
698
789
|
}
|
|
699
790
|
return byPage;
|
|
700
791
|
}, [selectionRects]);
|
|
701
|
-
const contentStyle =
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
792
|
+
const contentStyle = useMemo3(() => {
|
|
793
|
+
const style = { height: contentHeight };
|
|
794
|
+
if (fontFamily) style.fontFamily = normalizeFontFamily2(fontFamily);
|
|
795
|
+
if (fontSize != null) style.fontSize = fontSize;
|
|
796
|
+
if (lineSpacing != null) style.lineHeight = String(lineSpacing);
|
|
797
|
+
return style;
|
|
798
|
+
}, [contentHeight, fontFamily, fontSize, lineSpacing]);
|
|
799
|
+
const registerPage = useCallback2((index, el) => {
|
|
800
|
+
pageRefs.current[index] = el;
|
|
801
|
+
}, []);
|
|
802
|
+
const imageHandlersRef = useRef3({ onImagePointerDown, onImageResizePointerDown, onImageClose });
|
|
803
|
+
imageHandlersRef.current = { onImagePointerDown, onImageResizePointerDown, onImageClose };
|
|
804
|
+
const handleImagePointerDown = useCallback2(
|
|
805
|
+
(id, e) => imageHandlersRef.current.onImagePointerDown?.(id, e),
|
|
806
|
+
[]
|
|
807
|
+
);
|
|
808
|
+
const handleImageResizePointerDown = useCallback2(
|
|
809
|
+
(id, e) => imageHandlersRef.current.onImageResizePointerDown?.(id, e),
|
|
810
|
+
[]
|
|
811
|
+
);
|
|
812
|
+
const handleImageClose = useCallback2(
|
|
813
|
+
(id) => imageHandlersRef.current.onImageClose?.(id),
|
|
814
|
+
[]
|
|
815
|
+
);
|
|
705
816
|
useEffect2(() => {
|
|
706
817
|
const container = containerRef.current;
|
|
707
818
|
if (!container) return;
|
|
@@ -759,67 +870,26 @@ function MejiroScrollView({
|
|
|
759
870
|
{
|
|
760
871
|
className: "mejiro-reader-scroll-track",
|
|
761
872
|
style: { display: "flex", flexDirection: "column", gap: pageGap },
|
|
762
|
-
children: pages.map((result, i) => /* @__PURE__ */
|
|
763
|
-
|
|
873
|
+
children: pages.map((result, i) => /* @__PURE__ */ jsx9(
|
|
874
|
+
ScrollPage,
|
|
764
875
|
{
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
|
|
783
|
-
/* @__PURE__ */ jsx9(
|
|
784
|
-
MejiroPageView,
|
|
785
|
-
{
|
|
786
|
-
result,
|
|
787
|
-
slotMode,
|
|
788
|
-
fontFamily,
|
|
789
|
-
lineSpacing,
|
|
790
|
-
className: "mejiro-reader-page-content",
|
|
791
|
-
style: contentStyle
|
|
792
|
-
}
|
|
793
|
-
),
|
|
794
|
-
rectsByPage[i] && /* @__PURE__ */ jsx9(
|
|
795
|
-
MejiroSelectionLayer,
|
|
796
|
-
{
|
|
797
|
-
rects: rectsByPage[i],
|
|
798
|
-
side: i % 2 === 0 ? "right" : "left"
|
|
799
|
-
}
|
|
800
|
-
)
|
|
801
|
-
] }) }),
|
|
802
|
-
i === rightPage && rightImages.map((item) => /* @__PURE__ */ jsx9(
|
|
803
|
-
MejiroImageOverlay,
|
|
804
|
-
{
|
|
805
|
-
rect: item.rect,
|
|
806
|
-
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
807
|
-
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
808
|
-
onClose: () => onImageClose?.(item.id)
|
|
809
|
-
},
|
|
810
|
-
item.id
|
|
811
|
-
)),
|
|
812
|
-
i === leftPage && leftImages.map((item) => /* @__PURE__ */ jsx9(
|
|
813
|
-
MejiroImageOverlay,
|
|
814
|
-
{
|
|
815
|
-
rect: { ...item.rect, x: item.rect.x + pageWidth },
|
|
816
|
-
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
817
|
-
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
818
|
-
onClose: () => onImageClose?.(item.id)
|
|
819
|
-
},
|
|
820
|
-
item.id
|
|
821
|
-
))
|
|
822
|
-
]
|
|
876
|
+
result,
|
|
877
|
+
index: i,
|
|
878
|
+
pageNumber: pageNumberFor(pageNumbers, i),
|
|
879
|
+
pageWidth,
|
|
880
|
+
pageHeight,
|
|
881
|
+
contentHeight,
|
|
882
|
+
slotMode,
|
|
883
|
+
fontFamily,
|
|
884
|
+
lineSpacing,
|
|
885
|
+
contentStyle,
|
|
886
|
+
rects: rectsByPage[i],
|
|
887
|
+
overlays: i === rightPage ? rightImages : i === leftPage ? leftImages : NO_IMAGES,
|
|
888
|
+
overlaysLeft: i === leftPage,
|
|
889
|
+
registerPage,
|
|
890
|
+
onImagePointerDown: handleImagePointerDown,
|
|
891
|
+
onImageResizePointerDown: handleImageResizePointerDown,
|
|
892
|
+
onImageClose: handleImageClose
|
|
823
893
|
},
|
|
824
894
|
i
|
|
825
895
|
))
|
|
@@ -829,7 +899,7 @@ function MejiroScrollView({
|
|
|
829
899
|
|
|
830
900
|
// src/MejiroSettingsPanel.tsx
|
|
831
901
|
import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
|
|
832
|
-
import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
|
|
902
|
+
import { useEffect as useEffect3, useId, useRef as useRef4, useState as useState3 } from "react";
|
|
833
903
|
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
834
904
|
var DEFAULT_FONTS = [
|
|
835
905
|
{ value: "serif", label: "System Serif" },
|
|
@@ -898,15 +968,24 @@ function MejiroSettingsPanel({
|
|
|
898
968
|
maxFontSize = 48
|
|
899
969
|
}) {
|
|
900
970
|
const messages = useI18n();
|
|
971
|
+
const uid = useId();
|
|
901
972
|
const patch = (next) => onChange({ ...settings, ...next });
|
|
902
973
|
const currentFont = normalizeFontFamily3(settings.fontFamily);
|
|
903
974
|
const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
|
|
904
975
|
return /* @__PURE__ */ jsx10("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-content", children: [
|
|
905
976
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
|
|
906
977
|
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
|
|
907
|
-
/* @__PURE__ */ jsx10("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx10(
|
|
978
|
+
/* @__PURE__ */ jsx10("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx10(
|
|
979
|
+
"select",
|
|
980
|
+
{
|
|
981
|
+
"aria-label": messages.settingsFont,
|
|
982
|
+
value: currentFont,
|
|
983
|
+
onChange: (e) => patch({ fontFamily: e.target.value }),
|
|
984
|
+
children: fontOptions.map((f) => /* @__PURE__ */ jsx10("option", { value: f.value, children: f.label }, f.value))
|
|
985
|
+
}
|
|
986
|
+
) }),
|
|
908
987
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
909
|
-
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor:
|
|
988
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-font-size`, children: messages.settingsSize }),
|
|
910
989
|
/* @__PURE__ */ jsx10(
|
|
911
990
|
"button",
|
|
912
991
|
{
|
|
@@ -927,7 +1006,7 @@ function MejiroSettingsPanel({
|
|
|
927
1006
|
/* @__PURE__ */ jsx10(
|
|
928
1007
|
CommittedNumberInput,
|
|
929
1008
|
{
|
|
930
|
-
id:
|
|
1009
|
+
id: `${uid}-font-size`,
|
|
931
1010
|
value: settings.fontSize,
|
|
932
1011
|
min: minFontSize,
|
|
933
1012
|
max: maxFontSize,
|
|
@@ -956,11 +1035,11 @@ function MejiroSettingsPanel({
|
|
|
956
1035
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
|
|
957
1036
|
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
|
|
958
1037
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
959
|
-
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor:
|
|
1038
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-kinsoku`, children: messages.settingsKinsoku }),
|
|
960
1039
|
/* @__PURE__ */ jsxs6(
|
|
961
1040
|
"select",
|
|
962
1041
|
{
|
|
963
|
-
id:
|
|
1042
|
+
id: `${uid}-kinsoku`,
|
|
964
1043
|
value: settings.mode ?? "strict",
|
|
965
1044
|
onChange: (e) => patch({ mode: e.target.value }),
|
|
966
1045
|
children: [
|
|
@@ -971,11 +1050,11 @@ function MejiroSettingsPanel({
|
|
|
971
1050
|
)
|
|
972
1051
|
] }),
|
|
973
1052
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
974
|
-
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor:
|
|
1053
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-hanging`, children: messages.settingsHanging }),
|
|
975
1054
|
/* @__PURE__ */ jsxs6(
|
|
976
1055
|
"select",
|
|
977
1056
|
{
|
|
978
|
-
id:
|
|
1057
|
+
id: `${uid}-hanging`,
|
|
979
1058
|
value: String(settings.enableHanging ?? true),
|
|
980
1059
|
onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
|
|
981
1060
|
children: [
|
|
@@ -986,11 +1065,11 @@ function MejiroSettingsPanel({
|
|
|
986
1065
|
)
|
|
987
1066
|
] }),
|
|
988
1067
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
989
|
-
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor:
|
|
1068
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-line-spacing`, children: messages.settingsLineSpacing }),
|
|
990
1069
|
/* @__PURE__ */ jsx10(
|
|
991
1070
|
CommittedNumberInput,
|
|
992
1071
|
{
|
|
993
|
-
id:
|
|
1072
|
+
id: `${uid}-line-spacing`,
|
|
994
1073
|
className: "mejiro-reader-control--wide",
|
|
995
1074
|
value: settings.lineSpacing ?? 1.8,
|
|
996
1075
|
min: 1,
|
|
@@ -1061,11 +1140,13 @@ function MejiroSpread({
|
|
|
1061
1140
|
const selectionRef = useRef5(null);
|
|
1062
1141
|
const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
|
|
1063
1142
|
const gestureRef = useRef5(null);
|
|
1143
|
+
const swipedRef = useRef5(false);
|
|
1064
1144
|
const gestureEnabled = onSwipe != null || onSurfaceTap != null;
|
|
1065
1145
|
const SWIPE_THRESHOLD = 40;
|
|
1066
1146
|
const TAP_MOVE_THRESHOLD = 8;
|
|
1067
1147
|
function handleGesturePointerDown(e) {
|
|
1068
1148
|
if (!gestureEnabled) return;
|
|
1149
|
+
swipedRef.current = false;
|
|
1069
1150
|
if (!isPrimaryPointerPress2(e)) return;
|
|
1070
1151
|
if (e.target?.closest(OVERLAY_SELECTOR)) return;
|
|
1071
1152
|
gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
|
|
@@ -1080,6 +1161,7 @@ function MejiroSpread({
|
|
|
1080
1161
|
const ax = Math.abs(dx);
|
|
1081
1162
|
const ay = Math.abs(dy);
|
|
1082
1163
|
if (ax >= SWIPE_THRESHOLD && ax >= ay * 1.4) {
|
|
1164
|
+
swipedRef.current = true;
|
|
1083
1165
|
onSwipe?.(dx < 0 ? "next" : "prev");
|
|
1084
1166
|
return;
|
|
1085
1167
|
}
|
|
@@ -1089,6 +1171,12 @@ function MejiroSpread({
|
|
|
1089
1171
|
onSurfaceTap?.();
|
|
1090
1172
|
}
|
|
1091
1173
|
}
|
|
1174
|
+
function suppressSwipeClick(e) {
|
|
1175
|
+
if (!swipedRef.current) return;
|
|
1176
|
+
swipedRef.current = false;
|
|
1177
|
+
e.preventDefault();
|
|
1178
|
+
e.stopPropagation();
|
|
1179
|
+
}
|
|
1092
1180
|
function resolvePointer(e) {
|
|
1093
1181
|
if (!anchorAtCoord) return null;
|
|
1094
1182
|
const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
|
|
@@ -1207,6 +1295,7 @@ function MejiroSpread({
|
|
|
1207
1295
|
onPointerUp: useCombined ? combinedPointerUp : void 0,
|
|
1208
1296
|
onPointerCancel: useCombined ? combinedPointerCancel : void 0,
|
|
1209
1297
|
onLostPointerCapture: selectionEnabled ? endSelection : void 0,
|
|
1298
|
+
onClickCapture: gestureEnabled ? suppressSwipeClick : void 0,
|
|
1210
1299
|
children: [
|
|
1211
1300
|
renderPage(singlePage ? singleSide : "right"),
|
|
1212
1301
|
!singlePage && renderPage("left"),
|
|
@@ -1366,34 +1455,64 @@ function createDraftChangeTracker(chapters, key) {
|
|
|
1366
1455
|
}
|
|
1367
1456
|
|
|
1368
1457
|
// src/useChapterLayout.ts
|
|
1458
|
+
import { useMemo as useMemo4, useRef as useRef8 } from "react";
|
|
1459
|
+
|
|
1460
|
+
// src/useSurfaceLayout.ts
|
|
1369
1461
|
import {
|
|
1370
|
-
useCallback as
|
|
1371
|
-
useEffect as
|
|
1462
|
+
useCallback as useCallback3,
|
|
1463
|
+
useEffect as useEffect5,
|
|
1372
1464
|
useLayoutEffect as useLayoutEffect2,
|
|
1373
|
-
useRef as
|
|
1465
|
+
useRef as useRef7,
|
|
1374
1466
|
useState as useState4
|
|
1375
1467
|
} from "react";
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1468
|
+
|
|
1469
|
+
// src/useRequestToken.ts
|
|
1470
|
+
import { useEffect as useEffect4, useRef as useRef6 } from "react";
|
|
1471
|
+
function useRequestToken() {
|
|
1472
|
+
const ref = useRef6(null);
|
|
1473
|
+
if (!ref.current) {
|
|
1474
|
+
let latest = 0;
|
|
1475
|
+
ref.current = {
|
|
1476
|
+
begin: () => ++latest,
|
|
1477
|
+
current: () => latest,
|
|
1478
|
+
isCurrent: (token2) => token2 === latest,
|
|
1479
|
+
invalidate: () => {
|
|
1480
|
+
latest++;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
const token = ref.current;
|
|
1485
|
+
useEffect4(() => () => token.invalidate(), [token]);
|
|
1486
|
+
return token;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/useSurfaceLayout.ts
|
|
1490
|
+
function useSurfaceLayout(source, surface, options) {
|
|
1491
|
+
const { enableResize, resizeDebounce } = options;
|
|
1379
1492
|
const [layout, setLayout] = useState4(null);
|
|
1380
1493
|
const [pageWidth, setPageWidth] = useState4(0);
|
|
1381
1494
|
const [pageHeight, setPageHeight] = useState4(0);
|
|
1382
1495
|
const [contentHeight, setContentHeight] = useState4(0);
|
|
1383
1496
|
const [elapsedMs, setElapsedMs] = useState4(0);
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1497
|
+
const [element, setElement] = useState4(surface.current);
|
|
1498
|
+
useLayoutEffect2(() => {
|
|
1499
|
+
if (surface.current !== element) setElement(surface.current);
|
|
1500
|
+
});
|
|
1501
|
+
const layoutRef = useRef7(null);
|
|
1387
1502
|
layoutRef.current = layout;
|
|
1388
|
-
const
|
|
1503
|
+
const requests = useRequestToken();
|
|
1504
|
+
const pendingRestore = useRef7(null);
|
|
1505
|
+
const measuredBoxRef = useRef7(null);
|
|
1506
|
+
const optionsRef = useRef7(options);
|
|
1389
1507
|
optionsRef.current = options;
|
|
1390
|
-
const recompute =
|
|
1508
|
+
const recompute = useCallback3(
|
|
1391
1509
|
async (opts = {}) => {
|
|
1392
1510
|
const blank = opts.blank ?? true;
|
|
1393
1511
|
const o = optionsRef.current;
|
|
1394
|
-
const requestId =
|
|
1395
|
-
const
|
|
1396
|
-
if (!(
|
|
1512
|
+
const requestId = requests.begin();
|
|
1513
|
+
const el = element;
|
|
1514
|
+
if (!(source && el)) {
|
|
1515
|
+
measuredBoxRef.current = null;
|
|
1397
1516
|
setLayout(null);
|
|
1398
1517
|
setPageWidth(0);
|
|
1399
1518
|
setPageHeight(0);
|
|
@@ -1401,24 +1520,38 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1401
1520
|
setElapsedMs(0);
|
|
1402
1521
|
return;
|
|
1403
1522
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1523
|
+
try {
|
|
1524
|
+
const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
|
|
1525
|
+
pendingRestore.current = captured;
|
|
1526
|
+
if (blank) setLayout(null);
|
|
1527
|
+
measuredBoxRef.current = { width: el.clientWidth, height: el.clientHeight };
|
|
1528
|
+
const dims = source.measure(el);
|
|
1529
|
+
setPageWidth(dims.pageWidth);
|
|
1530
|
+
setPageHeight(dims.pageHeight);
|
|
1531
|
+
setContentHeight(dims.contentHeight);
|
|
1532
|
+
const t0 = performance.now();
|
|
1533
|
+
const next = await source.layout();
|
|
1534
|
+
if (!requests.isCurrent(requestId)) return;
|
|
1535
|
+
layoutRef.current = next;
|
|
1536
|
+
setLayout(next);
|
|
1537
|
+
setElapsedMs(performance.now() - t0);
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
if (!requests.isCurrent(requestId)) return;
|
|
1540
|
+
const handler = optionsRef.current.onError;
|
|
1541
|
+
if (!handler) throw err;
|
|
1542
|
+
handler(toError(err));
|
|
1543
|
+
}
|
|
1417
1544
|
},
|
|
1418
|
-
[
|
|
1545
|
+
[source, element, requests]
|
|
1419
1546
|
);
|
|
1547
|
+
const recomputeRef = useRef7(recompute);
|
|
1548
|
+
recomputeRef.current = recompute;
|
|
1549
|
+
const trigger = useCallback3((opts) => {
|
|
1550
|
+
recomputeRef.current(opts).catch(() => {
|
|
1551
|
+
});
|
|
1552
|
+
}, []);
|
|
1420
1553
|
useLayoutEffect2(() => {
|
|
1421
|
-
|
|
1554
|
+
requests.invalidate();
|
|
1422
1555
|
pendingRestore.current = null;
|
|
1423
1556
|
setLayout(null);
|
|
1424
1557
|
setPageWidth(0);
|
|
@@ -1426,12 +1559,13 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1426
1559
|
setContentHeight(0);
|
|
1427
1560
|
setElapsedMs(0);
|
|
1428
1561
|
}, [recompute]);
|
|
1429
|
-
|
|
1430
|
-
|
|
1562
|
+
useEffect5(() => {
|
|
1563
|
+
recompute().catch(() => {
|
|
1564
|
+
});
|
|
1431
1565
|
return () => {
|
|
1432
|
-
|
|
1566
|
+
requests.invalidate();
|
|
1433
1567
|
};
|
|
1434
|
-
}, [recompute]);
|
|
1568
|
+
}, [recompute, requests]);
|
|
1435
1569
|
useLayoutEffect2(() => {
|
|
1436
1570
|
const anchor = pendingRestore.current;
|
|
1437
1571
|
const restore = optionsRef.current.restorePosition;
|
|
@@ -1439,9 +1573,9 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1439
1573
|
pendingRestore.current = null;
|
|
1440
1574
|
restore(layout, anchor);
|
|
1441
1575
|
}, [layout]);
|
|
1442
|
-
|
|
1576
|
+
useEffect5(() => {
|
|
1443
1577
|
if (!enableResize) return;
|
|
1444
|
-
const el =
|
|
1578
|
+
const el = element;
|
|
1445
1579
|
if (!el) return;
|
|
1446
1580
|
let timer = null;
|
|
1447
1581
|
let observed = false;
|
|
@@ -1454,18 +1588,23 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1454
1588
|
const scheduleReflow = (immediate) => {
|
|
1455
1589
|
clear();
|
|
1456
1590
|
if (immediate) {
|
|
1457
|
-
|
|
1591
|
+
trigger({ blank: false });
|
|
1458
1592
|
return;
|
|
1459
1593
|
}
|
|
1460
1594
|
timer = setTimeout(() => {
|
|
1461
1595
|
timer = null;
|
|
1462
|
-
|
|
1596
|
+
trigger({ blank: false });
|
|
1463
1597
|
}, resizeDebounce);
|
|
1464
1598
|
};
|
|
1465
1599
|
if (typeof ResizeObserver !== "undefined") {
|
|
1466
1600
|
const observer = new ResizeObserver(() => {
|
|
1467
1601
|
const immediate = !observed;
|
|
1468
1602
|
observed = true;
|
|
1603
|
+
const box = measuredBoxRef.current;
|
|
1604
|
+
if (box && box.width === el.clientWidth && box.height === el.clientHeight) {
|
|
1605
|
+
clear();
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1469
1608
|
scheduleReflow(immediate);
|
|
1470
1609
|
});
|
|
1471
1610
|
observer.observe(el);
|
|
@@ -1480,103 +1619,136 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1480
1619
|
window.removeEventListener("resize", onWindowResize);
|
|
1481
1620
|
clear();
|
|
1482
1621
|
};
|
|
1483
|
-
}, [
|
|
1484
|
-
return {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1622
|
+
}, [element, enableResize, resizeDebounce, trigger]);
|
|
1623
|
+
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/useChapterLayout.ts
|
|
1627
|
+
function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
1628
|
+
const optionsRef = useRef8(options);
|
|
1629
|
+
optionsRef.current = options;
|
|
1630
|
+
const source = useMemo4(() => {
|
|
1631
|
+
const chapter = epub?.chapters[chapterIndex];
|
|
1632
|
+
if (!chapter) return null;
|
|
1633
|
+
return {
|
|
1634
|
+
measure: (el) => book.computePageSize(el, optionsRef.current.pageGeometry?.()),
|
|
1635
|
+
layout: () => book.layoutChapter(chapter)
|
|
1636
|
+
};
|
|
1637
|
+
}, [book, epub, chapterIndex]);
|
|
1638
|
+
return useSurfaceLayout(source, surface, {
|
|
1639
|
+
...options,
|
|
1640
|
+
enableResize: options.enableResize ?? true,
|
|
1641
|
+
resizeDebounce: options.resizeDebounce ?? 120
|
|
1642
|
+
});
|
|
1493
1643
|
}
|
|
1494
1644
|
|
|
1495
1645
|
// src/useEpub.ts
|
|
1496
1646
|
import { parseEpub } from "@libraz/mejiro/epub";
|
|
1497
|
-
import { useCallback as
|
|
1647
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState5 } from "react";
|
|
1498
1648
|
function useEpub(options = {}) {
|
|
1499
1649
|
const [epub, setEpub] = useState5(null);
|
|
1500
1650
|
const [loading, setLoading] = useState5(false);
|
|
1501
1651
|
const [error, setError] = useState5(null);
|
|
1502
|
-
const
|
|
1503
|
-
const onLoadRef =
|
|
1652
|
+
const requests = useRequestToken();
|
|
1653
|
+
const onLoadRef = useRef9(options.onLoad);
|
|
1504
1654
|
onLoadRef.current = options.onLoad;
|
|
1505
|
-
const onErrorRef =
|
|
1655
|
+
const onErrorRef = useRef9(options.onError);
|
|
1506
1656
|
onErrorRef.current = options.onError;
|
|
1507
|
-
const fetchOptionsRef =
|
|
1657
|
+
const fetchOptionsRef = useRef9(options.fetchOptions);
|
|
1508
1658
|
fetchOptionsRef.current = options.fetchOptions;
|
|
1509
|
-
const fetchEpubRef =
|
|
1659
|
+
const fetchEpubRef = useRef9(options.fetchEpub);
|
|
1510
1660
|
fetchEpubRef.current = options.fetchEpub;
|
|
1511
|
-
const limitsRef =
|
|
1661
|
+
const limitsRef = useRef9(options.limits);
|
|
1512
1662
|
limitsRef.current = options.limits;
|
|
1513
|
-
const load =
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1663
|
+
const load = useCallback4(
|
|
1664
|
+
async (read) => {
|
|
1665
|
+
const requestId = requests.begin();
|
|
1666
|
+
setLoading(true);
|
|
1667
|
+
setError(null);
|
|
1668
|
+
try {
|
|
1669
|
+
const book = await parseEpub(await read(), { limits: limitsRef.current });
|
|
1670
|
+
if (!requests.isCurrent(requestId)) return null;
|
|
1671
|
+
setEpub(book);
|
|
1672
|
+
onLoadRef.current?.(book);
|
|
1673
|
+
return book;
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
if (requests.isCurrent(requestId)) {
|
|
1676
|
+
const nextError = toError(err);
|
|
1677
|
+
setError(nextError);
|
|
1678
|
+
onErrorRef.current?.(nextError);
|
|
1679
|
+
}
|
|
1680
|
+
return null;
|
|
1681
|
+
} finally {
|
|
1682
|
+
if (requests.isCurrent(requestId)) setLoading(false);
|
|
1528
1683
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
|
|
1535
|
-
const loadFile = useCallback3(
|
|
1684
|
+
},
|
|
1685
|
+
[requests]
|
|
1686
|
+
);
|
|
1687
|
+
const loadBuffer = useCallback4((buffer) => load(async () => buffer), [load]);
|
|
1688
|
+
const loadFile = useCallback4(
|
|
1536
1689
|
(file) => load(() => readEpubFile(file, limitsRef.current)),
|
|
1537
1690
|
[load]
|
|
1538
1691
|
);
|
|
1539
|
-
const loadUrl =
|
|
1692
|
+
const loadUrl = useCallback4(
|
|
1540
1693
|
(url) => load(
|
|
1541
1694
|
() => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
|
|
1542
1695
|
),
|
|
1543
1696
|
[load]
|
|
1544
1697
|
);
|
|
1545
1698
|
const defaultUrl = options.defaultUrl;
|
|
1546
|
-
|
|
1547
|
-
if (defaultUrl)
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1699
|
+
useEffect6(() => {
|
|
1700
|
+
if (!defaultUrl) return;
|
|
1701
|
+
void loadUrl(defaultUrl);
|
|
1702
|
+
const requestId = requests.current();
|
|
1703
|
+
return () => {
|
|
1704
|
+
if (requests.current() !== requestId) return;
|
|
1705
|
+
requests.invalidate();
|
|
1706
|
+
setLoading(false);
|
|
1707
|
+
};
|
|
1708
|
+
}, [defaultUrl, loadUrl, requests]);
|
|
1709
|
+
const replaceEpub = useCallback4(
|
|
1710
|
+
(book) => {
|
|
1711
|
+
requests.invalidate();
|
|
1712
|
+
setLoading(false);
|
|
1713
|
+
setError(null);
|
|
1714
|
+
setEpub(book);
|
|
1715
|
+
},
|
|
1716
|
+
[requests]
|
|
1717
|
+
);
|
|
1555
1718
|
return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
|
|
1556
1719
|
}
|
|
1557
1720
|
|
|
1558
1721
|
// src/useMejiroBook.ts
|
|
1559
1722
|
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1560
|
-
import { useCallback as
|
|
1723
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef10, useState as useState6 } from "react";
|
|
1561
1724
|
function useMejiroBook(initial, options = {}) {
|
|
1562
|
-
const bookRef =
|
|
1725
|
+
const bookRef = useRef10(null);
|
|
1563
1726
|
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1564
1727
|
const [snapshot, setLocal] = useState6(initial);
|
|
1565
|
-
const optionsRef =
|
|
1728
|
+
const optionsRef = useRef10(options);
|
|
1566
1729
|
optionsRef.current = options;
|
|
1567
|
-
const pendingRef =
|
|
1568
|
-
const
|
|
1730
|
+
const pendingRef = useRef10(null);
|
|
1731
|
+
const flushGenerationRef = useRef10(0);
|
|
1732
|
+
const flush = useCallback5(() => {
|
|
1569
1733
|
const pending = pendingRef.current;
|
|
1570
1734
|
if (!pending) return;
|
|
1571
1735
|
pendingRef.current = null;
|
|
1572
1736
|
if (pending.timer) clearTimeout(pending.timer);
|
|
1573
1737
|
const { waiters } = pending;
|
|
1738
|
+
const generation = ++flushGenerationRef.current;
|
|
1574
1739
|
const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
|
|
1740
|
+
const syncSnapshot = () => {
|
|
1741
|
+
const book = bookRef.current;
|
|
1742
|
+
if (!book || generation !== flushGenerationRef.current) return;
|
|
1743
|
+
setLocal((prev) => ({ ...prev, ...book.getOptions(), ...pendingRef.current?.patch }));
|
|
1744
|
+
};
|
|
1575
1745
|
void applied.then(
|
|
1576
1746
|
() => {
|
|
1747
|
+
syncSnapshot();
|
|
1577
1748
|
for (const waiter of waiters) waiter.resolve();
|
|
1578
1749
|
},
|
|
1579
1750
|
(err) => {
|
|
1751
|
+
syncSnapshot();
|
|
1580
1752
|
const error = toError(err);
|
|
1581
1753
|
const handler = optionsRef.current.onError;
|
|
1582
1754
|
if (handler) {
|
|
@@ -1588,7 +1760,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1588
1760
|
}
|
|
1589
1761
|
);
|
|
1590
1762
|
}, []);
|
|
1591
|
-
const setOptions =
|
|
1763
|
+
const setOptions = useCallback5(
|
|
1592
1764
|
(next) => {
|
|
1593
1765
|
setLocal((prev) => mergeDefined(prev, next));
|
|
1594
1766
|
const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
|
|
@@ -1612,7 +1784,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1612
1784
|
},
|
|
1613
1785
|
[flush]
|
|
1614
1786
|
);
|
|
1615
|
-
|
|
1787
|
+
useEffect7(
|
|
1616
1788
|
() => () => {
|
|
1617
1789
|
bookRef.current?.dispose();
|
|
1618
1790
|
const pending = pendingRef.current;
|
|
@@ -1627,7 +1799,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1627
1799
|
}
|
|
1628
1800
|
|
|
1629
1801
|
// src/useSpread.ts
|
|
1630
|
-
import { useCallback as
|
|
1802
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useLayoutEffect as useLayoutEffect3, useRef as useRef11, useState as useState7 } from "react";
|
|
1631
1803
|
function ignoreNavigationKey(e) {
|
|
1632
1804
|
if (e.defaultPrevented) return true;
|
|
1633
1805
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
|
|
@@ -1657,7 +1829,7 @@ function useSpread(layout, options = {}) {
|
|
|
1657
1829
|
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1658
1830
|
const turnDuration = options.turnDuration ?? 180;
|
|
1659
1831
|
const single = options.single ?? false;
|
|
1660
|
-
const onChangeRef =
|
|
1832
|
+
const onChangeRef = useRef11(options.onChange);
|
|
1661
1833
|
onChangeRef.current = options.onChange;
|
|
1662
1834
|
const [spreadIdx, setSpreadIdx] = useState7(0);
|
|
1663
1835
|
const [indexSingle, setIndexSingle] = useState7(single);
|
|
@@ -1667,21 +1839,22 @@ function useSpread(layout, options = {}) {
|
|
|
1667
1839
|
}
|
|
1668
1840
|
const [spread, setSpread] = useState7(null);
|
|
1669
1841
|
const [turning, setTurning] = useState7(false);
|
|
1670
|
-
const layoutRef =
|
|
1842
|
+
const layoutRef = useRef11(null);
|
|
1671
1843
|
layoutRef.current = layout;
|
|
1672
|
-
const spreadIdxRef =
|
|
1844
|
+
const spreadIdxRef = useRef11(0);
|
|
1673
1845
|
spreadIdxRef.current = spreadIdx;
|
|
1674
|
-
const singleRef =
|
|
1846
|
+
const singleRef = useRef11(single);
|
|
1675
1847
|
singleRef.current = single;
|
|
1676
|
-
const turnTimerRef =
|
|
1677
|
-
const
|
|
1678
|
-
const
|
|
1848
|
+
const turnTimerRef = useRef11(null);
|
|
1849
|
+
const turnTargetRef = useRef11(null);
|
|
1850
|
+
const layoutGenerationRef = useRef11(0);
|
|
1851
|
+
const lastEmittedRef = useRef11(null);
|
|
1679
1852
|
const totalPages = spread?.totalPages ?? 0;
|
|
1680
1853
|
const totalSpreads = navigationCount(totalPages, single);
|
|
1681
1854
|
const firstPage = firstPageOf(spreadIdx, single);
|
|
1682
1855
|
const layoutSpreadIdx = Math.floor(firstPage / 2);
|
|
1683
1856
|
const singleSide = single ? firstPage % 2 === 0 ? "right" : "left" : null;
|
|
1684
|
-
const refresh =
|
|
1857
|
+
const refresh = useCallback6(() => {
|
|
1685
1858
|
if (!layoutRef.current) {
|
|
1686
1859
|
setSpread(null);
|
|
1687
1860
|
return;
|
|
@@ -1695,6 +1868,7 @@ function useSpread(layout, options = {}) {
|
|
|
1695
1868
|
clearTimeout(turnTimerRef.current);
|
|
1696
1869
|
turnTimerRef.current = null;
|
|
1697
1870
|
}
|
|
1871
|
+
turnTargetRef.current = null;
|
|
1698
1872
|
setTurning(false);
|
|
1699
1873
|
setSpreadIdx(0);
|
|
1700
1874
|
if (!layout) {
|
|
@@ -1703,7 +1877,7 @@ function useSpread(layout, options = {}) {
|
|
|
1703
1877
|
}
|
|
1704
1878
|
setSpread(layout.getSpread(0));
|
|
1705
1879
|
}, [layout]);
|
|
1706
|
-
|
|
1880
|
+
useEffect8(() => {
|
|
1707
1881
|
if (!layout) return;
|
|
1708
1882
|
setSpread(layout.getSpread(layoutSpreadIdx));
|
|
1709
1883
|
const last = lastEmittedRef.current;
|
|
@@ -1716,50 +1890,67 @@ function useSpread(layout, options = {}) {
|
|
|
1716
1890
|
}
|
|
1717
1891
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
1718
1892
|
}, [layout, spreadIdx, layoutSpreadIdx]);
|
|
1719
|
-
const
|
|
1893
|
+
const navigationTarget = useCallback6(() => {
|
|
1894
|
+
const pending = turnTargetRef.current;
|
|
1895
|
+
if (!pending) return spreadIdxRef.current;
|
|
1896
|
+
return indexOfPageIn(firstPageOf(pending.index, pending.single), singleRef.current);
|
|
1897
|
+
}, []);
|
|
1898
|
+
const goTo = useCallback6(
|
|
1720
1899
|
(index) => {
|
|
1721
1900
|
if (!layoutRef.current) return;
|
|
1722
1901
|
const targetSingle = singleRef.current;
|
|
1723
1902
|
const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
|
|
1724
1903
|
const target = Math.max(0, Math.min(max, index));
|
|
1904
|
+
if (turnTimerRef.current) {
|
|
1905
|
+
if (target === navigationTarget()) return;
|
|
1906
|
+
clearTimeout(turnTimerRef.current);
|
|
1907
|
+
turnTimerRef.current = null;
|
|
1908
|
+
const landed = navigationTarget();
|
|
1909
|
+
turnTargetRef.current = null;
|
|
1910
|
+
spreadIdxRef.current = landed;
|
|
1911
|
+
setSpreadIdx(landed);
|
|
1912
|
+
setTurning(false);
|
|
1913
|
+
}
|
|
1725
1914
|
if (target === spreadIdxRef.current) return;
|
|
1726
1915
|
if (turnDuration > 0) {
|
|
1727
1916
|
const generation = layoutGenerationRef.current;
|
|
1728
|
-
|
|
1917
|
+
turnTargetRef.current = { index: target, single: targetSingle };
|
|
1729
1918
|
setTurning(true);
|
|
1730
1919
|
turnTimerRef.current = setTimeout(() => {
|
|
1731
1920
|
if (generation !== layoutGenerationRef.current) return;
|
|
1732
|
-
setSpreadIdx(
|
|
1921
|
+
setSpreadIdx(navigationTarget());
|
|
1733
1922
|
setTurning(false);
|
|
1923
|
+
turnTargetRef.current = null;
|
|
1734
1924
|
turnTimerRef.current = null;
|
|
1735
1925
|
}, turnDuration);
|
|
1736
1926
|
} else {
|
|
1737
1927
|
setSpreadIdx(target);
|
|
1738
1928
|
}
|
|
1739
1929
|
},
|
|
1740
|
-
[turnDuration]
|
|
1930
|
+
[turnDuration, navigationTarget]
|
|
1741
1931
|
);
|
|
1742
|
-
const setSpreadIndex =
|
|
1932
|
+
const setSpreadIndex = useCallback6((index) => {
|
|
1743
1933
|
if (!layoutRef.current) return;
|
|
1744
1934
|
if (turnTimerRef.current) {
|
|
1745
1935
|
clearTimeout(turnTimerRef.current);
|
|
1746
1936
|
turnTimerRef.current = null;
|
|
1747
1937
|
}
|
|
1938
|
+
turnTargetRef.current = null;
|
|
1748
1939
|
setTurning(false);
|
|
1749
1940
|
const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
|
|
1750
1941
|
setSpreadIdx(Math.max(0, Math.min(max, index)));
|
|
1751
1942
|
}, []);
|
|
1752
|
-
const indexOfPage =
|
|
1753
|
-
const anchorAt =
|
|
1943
|
+
const indexOfPage = useCallback6((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
|
|
1944
|
+
const anchorAt = useCallback6(
|
|
1754
1945
|
(index) => {
|
|
1755
1946
|
if (!layout || index < 0 || index >= navigationCount(layout.totalPages, single)) return null;
|
|
1756
1947
|
return firstTextAnchorFrom(layout, firstPageOf(index, single));
|
|
1757
1948
|
},
|
|
1758
1949
|
[layout, single]
|
|
1759
1950
|
);
|
|
1760
|
-
const next =
|
|
1761
|
-
const prev =
|
|
1762
|
-
|
|
1951
|
+
const next = useCallback6(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
|
|
1952
|
+
const prev = useCallback6(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
|
|
1953
|
+
useEffect8(() => {
|
|
1763
1954
|
if (!enableKeyboard) return;
|
|
1764
1955
|
const onKey = (e) => {
|
|
1765
1956
|
if (!layoutRef.current) return;
|
|
@@ -1770,7 +1961,7 @@ function useSpread(layout, options = {}) {
|
|
|
1770
1961
|
window.addEventListener("keydown", onKey);
|
|
1771
1962
|
return () => window.removeEventListener("keydown", onKey);
|
|
1772
1963
|
}, [enableKeyboard, next, prev]);
|
|
1773
|
-
|
|
1964
|
+
useEffect8(
|
|
1774
1965
|
() => () => {
|
|
1775
1966
|
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1776
1967
|
},
|
|
@@ -1823,10 +2014,16 @@ function scrollTargetPage(spread, userPage) {
|
|
|
1823
2014
|
return userPage != null && spread.indexOfPage(userPage) === spread.spreadIdx ? userPage : spread.firstPage;
|
|
1824
2015
|
}
|
|
1825
2016
|
function createLifecycleTracker(sink) {
|
|
2017
|
+
let lastBook = null;
|
|
1826
2018
|
let last = null;
|
|
1827
2019
|
let dwell = null;
|
|
1828
|
-
return (chapter, spreadIdx, view, controlledSpreadIdx) => {
|
|
2020
|
+
return (book, chapter, spreadIdx, view, controlledSpreadIdx) => {
|
|
1829
2021
|
if (!view) return;
|
|
2022
|
+
if (book !== lastBook) {
|
|
2023
|
+
lastBook = book;
|
|
2024
|
+
last = null;
|
|
2025
|
+
dwell = null;
|
|
2026
|
+
}
|
|
1830
2027
|
const totalSpreads = view.total;
|
|
1831
2028
|
if (controlledSpreadIdx != null) {
|
|
1832
2029
|
const target = Math.max(0, Math.min(totalSpreads - 1, controlledSpreadIdx));
|
|
@@ -1844,6 +2041,56 @@ function createLifecycleTracker(sink) {
|
|
|
1844
2041
|
if (spreadIdx === totalSpreads - 1) sink.chapterFinished(chapter);
|
|
1845
2042
|
};
|
|
1846
2043
|
}
|
|
2044
|
+
function clampChapter(index, book) {
|
|
2045
|
+
if (!book) return index;
|
|
2046
|
+
return Math.max(0, Math.min(book.chapters.length - 1, index));
|
|
2047
|
+
}
|
|
2048
|
+
function createAnchorResolver(nav) {
|
|
2049
|
+
let pending = null;
|
|
2050
|
+
const settle = () => {
|
|
2051
|
+
const settled = pending;
|
|
2052
|
+
pending = null;
|
|
2053
|
+
settled?.resolve();
|
|
2054
|
+
};
|
|
2055
|
+
const update = (target) => {
|
|
2056
|
+
const current = pending;
|
|
2057
|
+
if (!(current && target.book)) return;
|
|
2058
|
+
if (current.moved) {
|
|
2059
|
+
settle();
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const { anchor } = current;
|
|
2063
|
+
if (!(anchor.chapter >= 0 && anchor.chapter < target.book.chapters.length)) {
|
|
2064
|
+
settle();
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (anchor.chapter !== target.chapter) {
|
|
2068
|
+
if (current.chapterRequested) return;
|
|
2069
|
+
current.chapterRequested = true;
|
|
2070
|
+
nav.goToChapter(anchor.chapter);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (!target.layout) return;
|
|
2074
|
+
const loc = target.layout.locateAnchor({
|
|
2075
|
+
paragraph: anchor.paragraph,
|
|
2076
|
+
charIndex: anchor.charIndex
|
|
2077
|
+
});
|
|
2078
|
+
if (loc && nav.goToPage(loc.pageIdx)) {
|
|
2079
|
+
current.moved = true;
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
settle();
|
|
2083
|
+
};
|
|
2084
|
+
return {
|
|
2085
|
+
request: (anchor, target) => new Promise((resolve) => {
|
|
2086
|
+
settle();
|
|
2087
|
+
pending = { anchor, resolve, chapterRequested: false, moved: false };
|
|
2088
|
+
update(target);
|
|
2089
|
+
}),
|
|
2090
|
+
update,
|
|
2091
|
+
dispose: settle
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
1847
2094
|
function MejiroReaderInner(props, ref) {
|
|
1848
2095
|
const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
|
|
1849
2096
|
const dialectProp = "dialect" in props ? props.dialect : void 0;
|
|
@@ -1890,18 +2137,17 @@ function MejiroReaderInner(props, ref) {
|
|
|
1890
2137
|
onPageRead,
|
|
1891
2138
|
onChapterCompleted
|
|
1892
2139
|
} = props;
|
|
1893
|
-
const surfaceRef =
|
|
1894
|
-
const fileRef =
|
|
2140
|
+
const surfaceRef = useRef12(null);
|
|
2141
|
+
const fileRef = useRef12(null);
|
|
1895
2142
|
const [settingsOpen, setSettingsOpen] = useState8(false);
|
|
1896
2143
|
const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
|
|
1897
2144
|
const chapterIsUncontrolled = chapterProp == null;
|
|
1898
|
-
const chapter = chapterProp ?? chapterState;
|
|
1899
2145
|
const [chromeHidden, setChromeHidden] = useState8(false);
|
|
1900
2146
|
const [userScroll, setUserScroll] = useState8(
|
|
1901
2147
|
null
|
|
1902
2148
|
);
|
|
1903
2149
|
const [autoSingle, setAutoSingle] = useState8(false);
|
|
1904
|
-
|
|
2150
|
+
useEffect9(() => {
|
|
1905
2151
|
if (!settingsOpen) return;
|
|
1906
2152
|
const onKey = (e2) => {
|
|
1907
2153
|
if (e2.key === "Escape") setSettingsOpen(false);
|
|
@@ -1909,22 +2155,25 @@ function MejiroReaderInner(props, ref) {
|
|
|
1909
2155
|
window.addEventListener("keydown", onKey);
|
|
1910
2156
|
return () => window.removeEventListener("keydown", onKey);
|
|
1911
2157
|
}, [settingsOpen]);
|
|
1912
|
-
|
|
2158
|
+
useEffect9(() => {
|
|
1913
2159
|
if (spreadMode !== "auto") return;
|
|
1914
2160
|
const surface = surfaceRef.current;
|
|
1915
2161
|
if (!surface) return;
|
|
1916
2162
|
const update = () => {
|
|
1917
2163
|
const rect = surface.getBoundingClientRect();
|
|
1918
|
-
setAutoSingle(rect.width < rect.height);
|
|
2164
|
+
setAutoSingle(rect.width < (fit === "width" ? window.innerHeight : rect.height));
|
|
1919
2165
|
};
|
|
1920
2166
|
update();
|
|
1921
|
-
|
|
1922
|
-
const observer = new ResizeObserver(update);
|
|
1923
|
-
observer
|
|
1924
|
-
return () =>
|
|
1925
|
-
|
|
2167
|
+
window.addEventListener("resize", update);
|
|
2168
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
|
|
2169
|
+
observer?.observe(surface);
|
|
2170
|
+
return () => {
|
|
2171
|
+
window.removeEventListener("resize", update);
|
|
2172
|
+
observer?.disconnect();
|
|
2173
|
+
};
|
|
2174
|
+
}, [spreadMode, fit]);
|
|
1926
2175
|
const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
|
|
1927
|
-
const resolvedGeometry =
|
|
2176
|
+
const resolvedGeometry = useMemo5(() => {
|
|
1928
2177
|
const columns = effectiveSingle ? 1 : 2;
|
|
1929
2178
|
const geometry = pageGeometryProp ?? {};
|
|
1930
2179
|
if (fit !== "width") return mergeDefined({ columns }, geometry);
|
|
@@ -1940,37 +2189,46 @@ function MejiroReaderInner(props, ref) {
|
|
|
1940
2189
|
geometry
|
|
1941
2190
|
);
|
|
1942
2191
|
}, [fit, pageGeometryProp, effectiveSingle]);
|
|
1943
|
-
const surfaceAspect =
|
|
2192
|
+
const surfaceAspect = useMemo5(() => {
|
|
1944
2193
|
const columns = effectiveSingle ? 1 : 2;
|
|
1945
2194
|
const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
|
|
1946
2195
|
return `${columns} / ${aspect}`;
|
|
1947
2196
|
}, [effectiveSingle, resolvedGeometry]);
|
|
1948
|
-
const resolvedOptions =
|
|
2197
|
+
const resolvedOptions = useMemo5(
|
|
1949
2198
|
() => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
|
|
1950
2199
|
[optionsProp]
|
|
1951
2200
|
);
|
|
1952
|
-
const onErrorRef =
|
|
2201
|
+
const onErrorRef = useRef12(onError);
|
|
1953
2202
|
onErrorRef.current = onError;
|
|
1954
|
-
const reportError =
|
|
2203
|
+
const reportError = useCallback7((error) => onErrorRef.current?.(error), []);
|
|
1955
2204
|
const {
|
|
1956
2205
|
book,
|
|
1957
2206
|
options: bookOptions,
|
|
1958
|
-
setOptions
|
|
2207
|
+
setOptions: applyBookOptions
|
|
1959
2208
|
} = useMejiroBook(resolvedOptions, {
|
|
1960
2209
|
debounceMs: OPTIONS_DEBOUNCE_MS,
|
|
1961
2210
|
onError: reportError
|
|
1962
2211
|
});
|
|
2212
|
+
const lastApplyRef = useRef12(Promise.resolve());
|
|
2213
|
+
const setOptions = useCallback7(
|
|
2214
|
+
(partial) => {
|
|
2215
|
+
const applied = applyBookOptions(partial);
|
|
2216
|
+
lastApplyRef.current = applied;
|
|
2217
|
+
return applied;
|
|
2218
|
+
},
|
|
2219
|
+
[applyBookOptions]
|
|
2220
|
+
);
|
|
1963
2221
|
const optionsPropKey = JSON.stringify(resolvedOptions);
|
|
1964
|
-
const syncedOptionsPropKeyRef =
|
|
1965
|
-
const resolvedOptionsRef =
|
|
2222
|
+
const syncedOptionsPropKeyRef = useRef12(optionsPropKey);
|
|
2223
|
+
const resolvedOptionsRef = useRef12(resolvedOptions);
|
|
1966
2224
|
resolvedOptionsRef.current = resolvedOptions;
|
|
1967
|
-
|
|
2225
|
+
useEffect9(() => {
|
|
1968
2226
|
if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
|
|
1969
2227
|
syncedOptionsPropKeyRef.current = optionsPropKey;
|
|
1970
2228
|
void setOptions(resolvedOptionsRef.current);
|
|
1971
2229
|
}, [optionsPropKey, setOptions]);
|
|
1972
2230
|
const manuscriptSourceKey = manuscriptKey(manuscriptProp);
|
|
1973
|
-
const synthesizedEpub =
|
|
2231
|
+
const synthesizedEpub = useMemo5(() => {
|
|
1974
2232
|
if (manuscriptSourceKey === void 0) return null;
|
|
1975
2233
|
const chapters = JSON.parse(manuscriptSourceKey);
|
|
1976
2234
|
return manuscriptToEpubBook(chapters, { dialect: dialectProp });
|
|
@@ -1983,7 +2241,6 @@ function MejiroReaderInner(props, ref) {
|
|
|
1983
2241
|
limits,
|
|
1984
2242
|
fetchEpub: fetchEpubFn,
|
|
1985
2243
|
onLoad: (b) => {
|
|
1986
|
-
if (chapterProp == null) setChapterState(0);
|
|
1987
2244
|
clearImagesRef.current();
|
|
1988
2245
|
onLoad?.(b);
|
|
1989
2246
|
}
|
|
@@ -1991,18 +2248,43 @@ function MejiroReaderInner(props, ref) {
|
|
|
1991
2248
|
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
1992
2249
|
const isManuscriptSource = manuscriptProp !== void 0;
|
|
1993
2250
|
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
1994
|
-
|
|
2251
|
+
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : bookIdentity(epubProp);
|
|
2252
|
+
const bookKey = controlled ? sourceKey : epubCtx.epub;
|
|
2253
|
+
const [seenBookKey, setSeenBookKey] = useState8(bookKey);
|
|
2254
|
+
let chapterRaw = chapterState;
|
|
2255
|
+
if (seenBookKey !== bookKey) {
|
|
2256
|
+
setSeenBookKey(bookKey);
|
|
2257
|
+
if (chapterIsUncontrolled) {
|
|
2258
|
+
setChapterState(0);
|
|
2259
|
+
chapterRaw = 0;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
const chapter = clampChapter(chapterProp ?? chapterRaw, e);
|
|
2263
|
+
const onChapterChangeRef = useRef12(onChapterChange);
|
|
2264
|
+
onChapterChangeRef.current = onChapterChange;
|
|
2265
|
+
const shownChapterRef = useRef12({ book: seenBookKey, chapter });
|
|
2266
|
+
useEffect9(() => {
|
|
2267
|
+
const prev = shownChapterRef.current;
|
|
2268
|
+
shownChapterRef.current = { book: seenBookKey, chapter };
|
|
2269
|
+
if (chapterIsUncontrolled && prev.book !== seenBookKey && prev.chapter !== chapter) {
|
|
2270
|
+
onChapterChangeRef.current?.(chapter);
|
|
2271
|
+
}
|
|
2272
|
+
}, [seenBookKey, chapter, chapterIsUncontrolled]);
|
|
2273
|
+
const filePicker = enableDropZone && !controlled;
|
|
2274
|
+
useEffect9(() => {
|
|
1995
2275
|
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
1996
2276
|
}, [epubCtx.error]);
|
|
1997
|
-
const pageGeometryRef =
|
|
2277
|
+
const pageGeometryRef = useRef12(resolvedGeometry);
|
|
1998
2278
|
pageGeometryRef.current = resolvedGeometry;
|
|
1999
|
-
const spreadAnchorRef =
|
|
2000
|
-
const spreadIdxPropRef =
|
|
2279
|
+
const spreadAnchorRef = useRef12(() => null);
|
|
2280
|
+
const spreadIdxPropRef = useRef12(spreadIdxProp);
|
|
2001
2281
|
spreadIdxPropRef.current = spreadIdxProp;
|
|
2002
|
-
const visibleAnchorRef =
|
|
2282
|
+
const visibleAnchorRef = useRef12(
|
|
2003
2283
|
null
|
|
2004
2284
|
);
|
|
2005
|
-
const
|
|
2285
|
+
const layoutMemoRef = useRef12(null);
|
|
2286
|
+
layoutMemoRef.current = resolveLayoutEpub(layoutMemoRef.current, e, bookKey, chapter);
|
|
2287
|
+
const layoutCtx = useChapterLayout(book, layoutMemoRef.current.epub, chapter, surfaceRef, {
|
|
2006
2288
|
pageGeometry: () => pageGeometryRef.current,
|
|
2007
2289
|
// Preserve the reading position across a reflow re-layout (size / option
|
|
2008
2290
|
// changes), but only in uncontrolled mode — when `spreadIdx` is controlled
|
|
@@ -2012,10 +2294,13 @@ function MejiroReaderInner(props, ref) {
|
|
|
2012
2294
|
const visible = visibleAnchorRef.current;
|
|
2013
2295
|
if (visible?.layout === layout2) return visible.anchor;
|
|
2014
2296
|
return spreadAnchorRef.current();
|
|
2015
|
-
}
|
|
2297
|
+
},
|
|
2298
|
+
onError: reportError
|
|
2016
2299
|
});
|
|
2017
|
-
const
|
|
2300
|
+
const reportedSpreadIdxRef = useRef12(null);
|
|
2301
|
+
const spreadChangedRef = useRef12(void 0);
|
|
2018
2302
|
spreadChangedRef.current = (i) => {
|
|
2303
|
+
reportedSpreadIdxRef.current = i;
|
|
2019
2304
|
onSpreadChange?.(i);
|
|
2020
2305
|
onSpreadIdxChange?.(i);
|
|
2021
2306
|
};
|
|
@@ -2025,9 +2310,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2025
2310
|
onChange: (i) => spreadChangedRef.current?.(i)
|
|
2026
2311
|
});
|
|
2027
2312
|
spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
2028
|
-
const indexOfPageRef =
|
|
2313
|
+
const indexOfPageRef = useRef12(spreadCtx.indexOfPage);
|
|
2029
2314
|
indexOfPageRef.current = spreadCtx.indexOfPage;
|
|
2030
|
-
const anchorSingleRef =
|
|
2315
|
+
const anchorSingleRef = useRef12(effectiveSingle);
|
|
2031
2316
|
useLayoutEffect4(() => {
|
|
2032
2317
|
const l = layoutCtx.layout;
|
|
2033
2318
|
const flipped = anchorSingleRef.current !== effectiveSingle;
|
|
@@ -2035,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2035
2320
|
if (flipped && l && visibleAnchorRef.current?.layout === l) return;
|
|
2036
2321
|
visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2037
2322
|
}, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
|
|
2038
|
-
const setSpreadRef =
|
|
2323
|
+
const setSpreadRef = useRef12(spreadCtx.setSpread);
|
|
2039
2324
|
setSpreadRef.current = spreadCtx.setSpread;
|
|
2040
2325
|
useLayoutEffect4(() => {
|
|
2041
2326
|
const anchor = layoutCtx.pendingRestore.current;
|
|
@@ -2046,18 +2331,16 @@ function MejiroReaderInner(props, ref) {
|
|
|
2046
2331
|
visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
|
|
2047
2332
|
}, [layoutCtx.layout]);
|
|
2048
2333
|
const optionsKey = reflowOptionsKey(bookOptions);
|
|
2049
|
-
const optionsKeyRef =
|
|
2050
|
-
const
|
|
2051
|
-
bookOptionsRef.current = bookOptions;
|
|
2052
|
-
const recomputeRef = useRef10(layoutCtx.recompute);
|
|
2334
|
+
const optionsKeyRef = useRef12(optionsKey);
|
|
2335
|
+
const recomputeRef = useRef12(layoutCtx.recompute);
|
|
2053
2336
|
recomputeRef.current = layoutCtx.recompute;
|
|
2054
|
-
|
|
2337
|
+
useEffect9(() => {
|
|
2055
2338
|
if (optionsKeyRef.current === optionsKey) return;
|
|
2056
2339
|
optionsKeyRef.current = optionsKey;
|
|
2057
2340
|
const timer = setTimeout(() => {
|
|
2058
2341
|
void (async () => {
|
|
2059
2342
|
try {
|
|
2060
|
-
await
|
|
2343
|
+
await lastApplyRef.current;
|
|
2061
2344
|
await recomputeRef.current({ blank: false });
|
|
2062
2345
|
} catch (err) {
|
|
2063
2346
|
reportError(toError(err));
|
|
@@ -2065,10 +2348,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
2065
2348
|
})();
|
|
2066
2349
|
}, OPTIONS_DEBOUNCE_MS);
|
|
2067
2350
|
return () => clearTimeout(timer);
|
|
2068
|
-
}, [optionsKey,
|
|
2351
|
+
}, [optionsKey, reportError]);
|
|
2069
2352
|
const geometryKey = JSON.stringify(resolvedGeometry ?? null);
|
|
2070
|
-
const geometryKeyRef =
|
|
2071
|
-
|
|
2353
|
+
const geometryKeyRef = useRef12(geometryKey);
|
|
2354
|
+
useEffect9(() => {
|
|
2072
2355
|
if (geometryKeyRef.current === geometryKey) return;
|
|
2073
2356
|
geometryKeyRef.current = geometryKey;
|
|
2074
2357
|
void recomputeRef.current({ blank: false });
|
|
@@ -2077,7 +2360,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2077
2360
|
defaultX: IMAGE_DEFAULT_X,
|
|
2078
2361
|
onUpdate: () => spreadCtx.refresh()
|
|
2079
2362
|
});
|
|
2080
|
-
const annotationRects =
|
|
2363
|
+
const annotationRects = useMemo5(() => {
|
|
2081
2364
|
if (!(annotations && layoutCtx.layout)) return [];
|
|
2082
2365
|
const result = [];
|
|
2083
2366
|
for (const annotation of annotations) {
|
|
@@ -2090,30 +2373,31 @@ function MejiroReaderInner(props, ref) {
|
|
|
2090
2373
|
}
|
|
2091
2374
|
return result;
|
|
2092
2375
|
}, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
|
|
2093
|
-
const clearImagesRef =
|
|
2376
|
+
const clearImagesRef = useRef12(imageCtx.clearImages);
|
|
2094
2377
|
clearImagesRef.current = imageCtx.clearImages;
|
|
2095
|
-
const onLoadRef =
|
|
2378
|
+
const onLoadRef = useRef12(onLoad);
|
|
2096
2379
|
onLoadRef.current = onLoad;
|
|
2097
|
-
const eRef =
|
|
2380
|
+
const eRef = useRef12(e);
|
|
2098
2381
|
eRef.current = e;
|
|
2099
|
-
|
|
2100
|
-
useEffect8(() => {
|
|
2382
|
+
useEffect9(() => {
|
|
2101
2383
|
if (!controlled) return;
|
|
2102
|
-
if (chapterIsUncontrolled) setChapterState(0);
|
|
2103
2384
|
clearImagesRef.current();
|
|
2104
2385
|
book.clearCache();
|
|
2105
2386
|
if (eRef.current) onLoadRef.current?.(eRef.current);
|
|
2106
|
-
}, [controlled, sourceKey, book
|
|
2107
|
-
const imagesChapterRef =
|
|
2108
|
-
|
|
2387
|
+
}, [controlled, sourceKey, book]);
|
|
2388
|
+
const imagesChapterRef = useRef12(chapter);
|
|
2389
|
+
useEffect9(() => {
|
|
2109
2390
|
if (imagesChapterRef.current === chapter) return;
|
|
2110
2391
|
imagesChapterRef.current = chapter;
|
|
2111
2392
|
clearImagesRef.current();
|
|
2112
2393
|
}, [chapter]);
|
|
2113
|
-
const spreadGoToRef =
|
|
2394
|
+
const spreadGoToRef = useRef12(spreadCtx.goTo);
|
|
2114
2395
|
spreadGoToRef.current = spreadCtx.goTo;
|
|
2115
|
-
const appliedSpreadIdxPropRef =
|
|
2116
|
-
|
|
2396
|
+
const appliedSpreadIdxPropRef = useRef12(spreadIdxProp);
|
|
2397
|
+
const [hostTurn, setHostTurn] = useState8(0);
|
|
2398
|
+
useEffect9(() => {
|
|
2399
|
+
const reported = reportedSpreadIdxRef.current;
|
|
2400
|
+
reportedSpreadIdxRef.current = null;
|
|
2117
2401
|
if (spreadIdxProp == null) {
|
|
2118
2402
|
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
2119
2403
|
return;
|
|
@@ -2122,23 +2406,32 @@ function MejiroReaderInner(props, ref) {
|
|
|
2122
2406
|
appliedSpreadIdxPropRef.current = spreadIdxProp;
|
|
2123
2407
|
if (spreadCtx.spreadIdx === spreadIdxProp) return;
|
|
2124
2408
|
if (propChanged) spreadGoToRef.current(spreadIdxProp);
|
|
2409
|
+
else if (reported === spreadCtx.spreadIdx) setHostTurn((n) => n + 1);
|
|
2125
2410
|
else setSpreadRef.current(spreadIdxProp);
|
|
2126
|
-
}, [spreadIdxProp, spreadCtx.spreadIdx]);
|
|
2411
|
+
}, [spreadIdxProp, spreadCtx.spreadIdx, hostTurn]);
|
|
2412
|
+
const restoredForRef = useRef12(null);
|
|
2127
2413
|
useLayoutEffect4(() => {
|
|
2414
|
+
if (!layoutCtx.layout) return;
|
|
2415
|
+
const previous = restoredForRef.current;
|
|
2416
|
+
restoredForRef.current = { chapter, book: bookKey };
|
|
2417
|
+
if (previous && (previous.chapter !== chapter || previous.book !== bookKey)) return;
|
|
2128
2418
|
const next = spreadIdxPropRef.current;
|
|
2129
|
-
if (next == null
|
|
2419
|
+
if (next == null) return;
|
|
2130
2420
|
setSpreadRef.current(next);
|
|
2131
2421
|
}, [layoutCtx.layout]);
|
|
2132
|
-
const onChapter =
|
|
2133
|
-
(
|
|
2422
|
+
const onChapter = useCallback7(
|
|
2423
|
+
(index) => {
|
|
2424
|
+
const i = clampChapter(index, e);
|
|
2134
2425
|
if (i === chapter) return;
|
|
2135
2426
|
if (chapterProp == null) setChapterState(i);
|
|
2136
2427
|
onChapterChange?.(i);
|
|
2137
2428
|
},
|
|
2138
|
-
[chapter, chapterProp, onChapterChange]
|
|
2429
|
+
[chapter, chapterProp, onChapterChange, e]
|
|
2139
2430
|
);
|
|
2140
|
-
const
|
|
2141
|
-
|
|
2431
|
+
const onChapterRef = useRef12(onChapter);
|
|
2432
|
+
onChapterRef.current = onChapter;
|
|
2433
|
+
const listenersRef = useRef12(/* @__PURE__ */ new Map());
|
|
2434
|
+
const emit = useCallback7(
|
|
2142
2435
|
(event, payload) => {
|
|
2143
2436
|
const set = listenersRef.current.get(event);
|
|
2144
2437
|
if (!set) return;
|
|
@@ -2146,44 +2439,40 @@ function MejiroReaderInner(props, ref) {
|
|
|
2146
2439
|
},
|
|
2147
2440
|
[]
|
|
2148
2441
|
);
|
|
2149
|
-
const
|
|
2150
|
-
const layoutOwnerRef = useRef10({
|
|
2442
|
+
const layoutOwnerRef = useRef12({
|
|
2151
2443
|
layout: null,
|
|
2152
|
-
chapter
|
|
2444
|
+
chapter,
|
|
2445
|
+
book: bookKey
|
|
2153
2446
|
});
|
|
2154
2447
|
if (layoutOwnerRef.current.layout !== layoutCtx.layout) {
|
|
2155
|
-
layoutOwnerRef.current = { layout: layoutCtx.layout, chapter };
|
|
2448
|
+
layoutOwnerRef.current = { layout: layoutCtx.layout, chapter, book: bookKey };
|
|
2156
2449
|
}
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
},
|
|
2180
|
-
[]
|
|
2181
|
-
);
|
|
2182
|
-
const onPageReadRef = useRef10(onPageRead);
|
|
2450
|
+
const owner = layoutOwnerRef.current;
|
|
2451
|
+
const layout = owner.chapter === chapter && owner.book === bookKey ? layoutCtx.layout : null;
|
|
2452
|
+
const anchorTarget = { book: epubCtx.loading ? null : e, chapter, layout };
|
|
2453
|
+
const anchorTargetRef = useRef12(anchorTarget);
|
|
2454
|
+
anchorTargetRef.current = anchorTarget;
|
|
2455
|
+
const spreadIdxNowRef = useRef12(spreadCtx.spreadIdx);
|
|
2456
|
+
spreadIdxNowRef.current = spreadCtx.spreadIdx;
|
|
2457
|
+
const anchorResolverRef = useRef12(null);
|
|
2458
|
+
anchorResolverRef.current ??= createAnchorResolver({
|
|
2459
|
+
goToChapter: (i) => onChapterRef.current(i),
|
|
2460
|
+
goToPage: (pageIdx) => {
|
|
2461
|
+
const target = indexOfPageRef.current(pageIdx);
|
|
2462
|
+
if (target === spreadIdxNowRef.current) return false;
|
|
2463
|
+
setSpreadRef.current(target);
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
useEffect9(() => {
|
|
2468
|
+
anchorResolverRef.current?.update(anchorTargetRef.current);
|
|
2469
|
+
}, [anchorTarget.book, chapter, layout, spreadCtx.spreadIdx]);
|
|
2470
|
+
useEffect9(() => () => anchorResolverRef.current?.dispose(), []);
|
|
2471
|
+
const onPageReadRef = useRef12(onPageRead);
|
|
2183
2472
|
onPageReadRef.current = onPageRead;
|
|
2184
|
-
const onChapterCompletedRef =
|
|
2473
|
+
const onChapterCompletedRef = useRef12(onChapterCompleted);
|
|
2185
2474
|
onChapterCompletedRef.current = onChapterCompleted;
|
|
2186
|
-
const trackLifecycleRef =
|
|
2475
|
+
const trackLifecycleRef = useRef12(null);
|
|
2187
2476
|
trackLifecycleRef.current ??= createLifecycleTracker({
|
|
2188
2477
|
spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
|
|
2189
2478
|
chapterFinished: (ch) => {
|
|
@@ -2192,10 +2481,11 @@ function MejiroReaderInner(props, ref) {
|
|
|
2192
2481
|
},
|
|
2193
2482
|
pageRead: (anchor, dwellMs) => onPageReadRef.current?.(anchor, dwellMs)
|
|
2194
2483
|
});
|
|
2195
|
-
|
|
2484
|
+
useEffect9(() => {
|
|
2196
2485
|
const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2197
|
-
trackLifecycleRef.current?.(chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
|
|
2486
|
+
trackLifecycleRef.current?.(bookKey, chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
|
|
2198
2487
|
}, [
|
|
2488
|
+
bookKey,
|
|
2199
2489
|
chapter,
|
|
2200
2490
|
spreadCtx.spreadIdx,
|
|
2201
2491
|
spreadCtx.totalSpreads,
|
|
@@ -2203,8 +2493,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2203
2493
|
layout,
|
|
2204
2494
|
spreadIdxProp
|
|
2205
2495
|
]);
|
|
2206
|
-
const prevTurningRef =
|
|
2207
|
-
|
|
2496
|
+
const prevTurningRef = useRef12(false);
|
|
2497
|
+
useEffect9(() => {
|
|
2208
2498
|
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
2209
2499
|
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
2210
2500
|
} else if (!spreadCtx.turning && prevTurningRef.current) {
|
|
@@ -2225,12 +2515,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2225
2515
|
totalPages: spreadCtx.totalPages,
|
|
2226
2516
|
totalSpreads: spreadCtx.totalSpreads
|
|
2227
2517
|
}),
|
|
2228
|
-
goToAnchor: (anchor) =>
|
|
2229
|
-
pendingAnchorRef.current?.resolve();
|
|
2230
|
-
pendingAnchorRef.current = { anchor, resolve };
|
|
2231
|
-
if (anchor.chapter !== chapter) onChapter(anchor.chapter);
|
|
2232
|
-
tryApplyPendingAnchor();
|
|
2233
|
-
}),
|
|
2518
|
+
goToAnchor: (anchor) => anchorResolverRef.current?.request(anchor, anchorTargetRef.current) ?? Promise.resolve(),
|
|
2234
2519
|
getAnchor: () => {
|
|
2235
2520
|
if (!layout) return null;
|
|
2236
2521
|
const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
@@ -2269,8 +2554,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2269
2554
|
onChapter,
|
|
2270
2555
|
chapter,
|
|
2271
2556
|
layout,
|
|
2272
|
-
setOptions
|
|
2273
|
-
tryApplyPendingAnchor
|
|
2557
|
+
setOptions
|
|
2274
2558
|
]
|
|
2275
2559
|
);
|
|
2276
2560
|
const editable = {
|
|
@@ -2293,7 +2577,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2293
2577
|
const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
|
|
2294
2578
|
const themeName = typeof theme === "string" ? theme : theme.name;
|
|
2295
2579
|
const themeOverride = typeof theme === "string" ? void 0 : theme.override;
|
|
2296
|
-
const themeStyle =
|
|
2580
|
+
const themeStyle = useMemo5(() => {
|
|
2297
2581
|
const pad = pageGeometryProp?.padding;
|
|
2298
2582
|
const styleVars = {};
|
|
2299
2583
|
if (pad?.x != null) styleVars["--mejiro-page-pad-x"] = `${pad.x}px`;
|
|
@@ -2325,7 +2609,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2325
2609
|
fontLabel
|
|
2326
2610
|
}
|
|
2327
2611
|
),
|
|
2328
|
-
|
|
2612
|
+
filePicker && /* @__PURE__ */ jsx13(
|
|
2329
2613
|
"button",
|
|
2330
2614
|
{
|
|
2331
2615
|
type: "button",
|
|
@@ -2363,11 +2647,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2363
2647
|
] })
|
|
2364
2648
|
] }) : null;
|
|
2365
2649
|
const currentSpread = spreadCtx.layoutSpreadIdx;
|
|
2366
|
-
const rightPage = currentSpread * 2 + 1;
|
|
2367
2650
|
const leftPage = currentSpread * 2 + 2;
|
|
2368
2651
|
const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
|
|
2369
|
-
const
|
|
2370
|
-
const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
|
|
2652
|
+
const pageIndicator = enablePageIndicator ? /* @__PURE__ */ jsx13(MejiroPageIndicator, { current: spreadCtx.spreadIdx + 1, total: spreadCtx.totalSpreads }) : null;
|
|
2371
2653
|
return /* @__PURE__ */ jsx13(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs8(
|
|
2372
2654
|
"div",
|
|
2373
2655
|
{
|
|
@@ -2393,7 +2675,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2393
2675
|
/* @__PURE__ */ jsxs8("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
|
|
2394
2676
|
showChapterPanel && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
|
|
2395
2677
|
/* @__PURE__ */ jsxs8("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
|
|
2396
|
-
!(e || epubCtx.loading) &&
|
|
2678
|
+
!(e || epubCtx.loading) && filePicker && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
|
|
2397
2679
|
epubCtx.loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
|
|
2398
2680
|
!layoutReady && fallback && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-fallback", children: fallback }),
|
|
2399
2681
|
layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx13(
|
|
@@ -2412,18 +2694,25 @@ function MejiroReaderInner(props, ref) {
|
|
|
2412
2694
|
),
|
|
2413
2695
|
onVisiblePageChange: (pageIdx, source) => {
|
|
2414
2696
|
if (source === "programmatic") return;
|
|
2415
|
-
|
|
2697
|
+
const scrolledLayout = layoutCtx.layout;
|
|
2698
|
+
if (scrolledLayout) {
|
|
2699
|
+
setUserScroll(
|
|
2700
|
+
(prev) => prev?.layout === scrolledLayout && prev.page === pageIdx ? prev : { layout: scrolledLayout, page: pageIdx }
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2416
2703
|
const target = spreadCtx.indexOfPage(pageIdx);
|
|
2417
2704
|
if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
|
|
2418
2705
|
},
|
|
2419
2706
|
selectionRects: annotationRects.length ? annotationRects : void 0,
|
|
2420
2707
|
spreadIdx: spreadCtx.layoutSpreadIdx,
|
|
2421
2708
|
images: imageCtx.currentImages,
|
|
2709
|
+
pageNumbers,
|
|
2422
2710
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
2423
2711
|
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
2424
2712
|
onImageClose: imageCtx.removeImage
|
|
2425
2713
|
}
|
|
2426
2714
|
),
|
|
2715
|
+
layoutReady && mode === "scroll" && layoutCtx.layout && pageIndicator,
|
|
2427
2716
|
layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
|
|
2428
2717
|
MejiroSpread,
|
|
2429
2718
|
{
|
|
@@ -2441,20 +2730,14 @@ function MejiroReaderInner(props, ref) {
|
|
|
2441
2730
|
turning: spreadCtx.turning,
|
|
2442
2731
|
rightHeader: {
|
|
2443
2732
|
title: runningTitleRight,
|
|
2444
|
-
pageNumber:
|
|
2733
|
+
pageNumber: pageNumberFor2(pageNumbers, currentSpread * 2)
|
|
2445
2734
|
},
|
|
2446
2735
|
leftHeader: {
|
|
2447
2736
|
title: runningTitleLeft,
|
|
2448
|
-
pageNumber: showLeft
|
|
2737
|
+
pageNumber: showLeft ? pageNumberFor2(pageNumbers, currentSpread * 2 + 1) : null
|
|
2449
2738
|
},
|
|
2450
2739
|
images: imageCtx.currentImages,
|
|
2451
|
-
indicator:
|
|
2452
|
-
MejiroPageIndicator,
|
|
2453
|
-
{
|
|
2454
|
-
current: spreadCtx.spreadIdx + 1,
|
|
2455
|
-
total: spreadCtx.totalSpreads
|
|
2456
|
-
}
|
|
2457
|
-
) : null,
|
|
2740
|
+
indicator: pageIndicator,
|
|
2458
2741
|
onPrev: spreadCtx.prev,
|
|
2459
2742
|
onNext: spreadCtx.next,
|
|
2460
2743
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
@@ -2476,6 +2759,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2476
2759
|
hidden: true,
|
|
2477
2760
|
onChange: (ev) => {
|
|
2478
2761
|
const file = ev.target.files?.[0];
|
|
2762
|
+
ev.target.value = "";
|
|
2479
2763
|
if (file) void epubCtx.loadFile(file);
|
|
2480
2764
|
}
|
|
2481
2765
|
}
|
|
@@ -2493,6 +2777,55 @@ MejiroReader.displayName = "MejiroReader";
|
|
|
2493
2777
|
|
|
2494
2778
|
// src/MejiroEditor.tsx
|
|
2495
2779
|
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2780
|
+
function clampRubyRange(start, end, length) {
|
|
2781
|
+
if (length <= 0) return null;
|
|
2782
|
+
const from = Math.max(0, Math.min(start, length - 1));
|
|
2783
|
+
return { start: from, end: Math.max(from + 1, Math.min(end, length)) };
|
|
2784
|
+
}
|
|
2785
|
+
function commitPendingEdit(editor, chapterIndex, paragraphIndex, text, ruby) {
|
|
2786
|
+
const paragraphs = editor.book.chapters[chapterIndex]?.paragraphs;
|
|
2787
|
+
if (!paragraphs?.[paragraphIndex]) return false;
|
|
2788
|
+
const textChanged = text !== paragraphs[paragraphIndex].text;
|
|
2789
|
+
const rubyText = ruby?.rubyText.trim();
|
|
2790
|
+
const range = ruby ? clampRubyRange(ruby.start, ruby.end, [...text].length) : null;
|
|
2791
|
+
if (!(rubyText && range)) {
|
|
2792
|
+
if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
|
|
2793
|
+
return textChanged;
|
|
2794
|
+
}
|
|
2795
|
+
editor.transaction(() => {
|
|
2796
|
+
if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
|
|
2797
|
+
const committed = editor.book.chapters[chapterIndex].paragraphs[paragraphIndex];
|
|
2798
|
+
const newRuby = {
|
|
2799
|
+
kind: "ruby",
|
|
2800
|
+
startIndex: range.start,
|
|
2801
|
+
endIndex: range.end,
|
|
2802
|
+
rubyText,
|
|
2803
|
+
type: range.end - range.start === 1 ? "mono" : "group"
|
|
2804
|
+
};
|
|
2805
|
+
const inlineAnnotations = [
|
|
2806
|
+
...committed.inlineAnnotations.filter(
|
|
2807
|
+
(ann) => ann.kind !== "ruby" || ann.endIndex <= range.start || ann.startIndex >= range.end
|
|
2808
|
+
),
|
|
2809
|
+
newRuby
|
|
2810
|
+
].sort((a, b) => a.startIndex - b.startIndex);
|
|
2811
|
+
editor.updateParagraph(chapterIndex, paragraphIndex, { inlineAnnotations });
|
|
2812
|
+
});
|
|
2813
|
+
return true;
|
|
2814
|
+
}
|
|
2815
|
+
function downloadEpub(buffer, filename) {
|
|
2816
|
+
const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
|
|
2817
|
+
const a = document.createElement("a");
|
|
2818
|
+
a.href = url;
|
|
2819
|
+
a.download = filename;
|
|
2820
|
+
a.hidden = true;
|
|
2821
|
+
document.body.append(a);
|
|
2822
|
+
try {
|
|
2823
|
+
a.click();
|
|
2824
|
+
} finally {
|
|
2825
|
+
a.remove();
|
|
2826
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2496
2829
|
function utf16ToCodepoint(text, utf16Offset) {
|
|
2497
2830
|
let cp = 0;
|
|
2498
2831
|
let i = 0;
|
|
@@ -2510,6 +2843,14 @@ function watermarkedBook(book, watermark) {
|
|
|
2510
2843
|
const copy = cloneEditableEpubBook(book);
|
|
2511
2844
|
const text = `${WATERMARK_PREFIX} ${watermark.text}`;
|
|
2512
2845
|
for (const chapter of copy.chapters) {
|
|
2846
|
+
if (!chapter.isDirty && chapter.originalXhtml) {
|
|
2847
|
+
const injected = injectWatermarkXhtml(chapter.originalXhtml, text);
|
|
2848
|
+
if (injected) {
|
|
2849
|
+
chapter.originalXhtml = injected;
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
chapter.isDirty = true;
|
|
2853
|
+
}
|
|
2513
2854
|
chapter.blocks.unshift({
|
|
2514
2855
|
kind: "paragraph",
|
|
2515
2856
|
id: watermarkBlockId(chapter),
|
|
@@ -2517,10 +2858,19 @@ function watermarkedBook(book, watermark) {
|
|
|
2517
2858
|
inlineAnnotations: []
|
|
2518
2859
|
});
|
|
2519
2860
|
chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
|
|
2520
|
-
chapter.isDirty = true;
|
|
2521
2861
|
}
|
|
2522
2862
|
return copy;
|
|
2523
2863
|
}
|
|
2864
|
+
function injectWatermarkXhtml(xhtml, text) {
|
|
2865
|
+
if (!/<html\b[^>]*\sxmlns\s*=\s*["']http:\/\/www\.w3\.org\/1999\/xhtml["']/i.test(xhtml)) {
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
const body = /<body\b(?:[^>]*[^/>])?>/i.exec(xhtml);
|
|
2869
|
+
if (!body) return null;
|
|
2870
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2871
|
+
const end = body.index + body[0].length;
|
|
2872
|
+
return `${xhtml.slice(0, end)}<p>${escaped}</p>${xhtml.slice(end)}`;
|
|
2873
|
+
}
|
|
2524
2874
|
function watermarkBlockId(chapter) {
|
|
2525
2875
|
const used = new Set(chapter.blocks.map((block) => block.id));
|
|
2526
2876
|
let id = WATERMARK_BLOCK_ID;
|
|
@@ -2544,9 +2894,9 @@ function MejiroEditor({
|
|
|
2544
2894
|
onError
|
|
2545
2895
|
}) {
|
|
2546
2896
|
const messages = useI18n();
|
|
2547
|
-
const imageInputRef =
|
|
2548
|
-
const textareaRef =
|
|
2549
|
-
const readerRef =
|
|
2897
|
+
const imageInputRef = useRef13(null);
|
|
2898
|
+
const textareaRef = useRef13(null);
|
|
2899
|
+
const readerRef = useRef13(null);
|
|
2550
2900
|
const [editor, setEditor] = useState9(null);
|
|
2551
2901
|
const [loading, setLoading] = useState9(false);
|
|
2552
2902
|
const [error, setError] = useState9(null);
|
|
@@ -2556,86 +2906,89 @@ function MejiroEditor({
|
|
|
2556
2906
|
const [rubyStart, setRubyStart] = useState9(0);
|
|
2557
2907
|
const [rubyEnd, setRubyEnd] = useState9(1);
|
|
2558
2908
|
const [rubyText, setRubyText] = useState9("");
|
|
2559
|
-
const
|
|
2560
|
-
const onLoadRef =
|
|
2561
|
-
const onErrorRef =
|
|
2562
|
-
const limitsRef =
|
|
2909
|
+
const request = useRequestToken();
|
|
2910
|
+
const onLoadRef = useRef13(onLoad);
|
|
2911
|
+
const onErrorRef = useRef13(onError);
|
|
2912
|
+
const limitsRef = useRef13(limits);
|
|
2563
2913
|
limitsRef.current = limits;
|
|
2564
2914
|
const book = editor?.book ?? null;
|
|
2565
2915
|
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
2566
2916
|
const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
|
|
2567
|
-
const
|
|
2568
|
-
const
|
|
2917
|
+
const sceneBreak = chapter ? paragraphBlock(chapter, selection.paragraph)?.paragraphKind === "sceneBreak" : false;
|
|
2918
|
+
const rubyRange = clampRubyRange(rubyStart, rubyEnd, [...text].length);
|
|
2919
|
+
const previewBook = useMemo6(() => {
|
|
2569
2920
|
void revision;
|
|
2570
|
-
return book ? cloneEditableEpubBook(book) : null;
|
|
2571
|
-
}, [book, revision]);
|
|
2572
|
-
|
|
2921
|
+
return book && editor ? markBookIdentity(cloneEditableEpubBook(book), editor) : null;
|
|
2922
|
+
}, [book, editor, revision]);
|
|
2923
|
+
useEffect10(() => {
|
|
2573
2924
|
onLoadRef.current = onLoad;
|
|
2574
2925
|
}, [onLoad]);
|
|
2575
|
-
|
|
2926
|
+
useEffect10(() => {
|
|
2576
2927
|
onErrorRef.current = onError;
|
|
2577
2928
|
}, [onError]);
|
|
2578
|
-
const reportError =
|
|
2929
|
+
const reportError = useCallback8((nextError) => {
|
|
2579
2930
|
setError(nextError);
|
|
2580
2931
|
onErrorRef.current?.(nextError);
|
|
2581
2932
|
}, []);
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2933
|
+
const clearError = useCallback8(() => setError(null), []);
|
|
2934
|
+
const loadBufferForRequest = useCallback8(
|
|
2935
|
+
async (buffer, token) => {
|
|
2584
2936
|
setLoading(true);
|
|
2585
|
-
|
|
2937
|
+
clearError();
|
|
2586
2938
|
try {
|
|
2587
2939
|
const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
|
|
2588
|
-
if (
|
|
2940
|
+
if (!request.isCurrent(token)) return;
|
|
2589
2941
|
setEditor(next);
|
|
2590
2942
|
setSelection({ chapter: 0, paragraph: 0 });
|
|
2591
2943
|
setRevision((value) => value + 1);
|
|
2592
2944
|
onLoadRef.current?.(next);
|
|
2593
2945
|
} catch (err) {
|
|
2594
|
-
if (
|
|
2946
|
+
if (request.isCurrent(token)) reportError(toError(err));
|
|
2595
2947
|
} finally {
|
|
2596
|
-
if (
|
|
2948
|
+
if (request.isCurrent(token)) setLoading(false);
|
|
2597
2949
|
}
|
|
2598
2950
|
},
|
|
2599
|
-
[reportError]
|
|
2951
|
+
[clearError, reportError, request]
|
|
2600
2952
|
);
|
|
2601
2953
|
async function loadFile(file) {
|
|
2602
|
-
const
|
|
2954
|
+
const token = request.begin();
|
|
2603
2955
|
setLoading(true);
|
|
2604
|
-
|
|
2956
|
+
clearError();
|
|
2605
2957
|
try {
|
|
2606
|
-
await loadBufferForRequest(await readEpubFile(file, limitsRef.current),
|
|
2958
|
+
await loadBufferForRequest(await readEpubFile(file, limitsRef.current), token);
|
|
2607
2959
|
} catch (err) {
|
|
2608
|
-
if (
|
|
2960
|
+
if (request.isCurrent(token)) {
|
|
2609
2961
|
reportError(toError(err));
|
|
2610
2962
|
setLoading(false);
|
|
2611
2963
|
}
|
|
2612
2964
|
}
|
|
2613
2965
|
}
|
|
2614
|
-
|
|
2966
|
+
useEffect10(() => {
|
|
2615
2967
|
if (!epubUrl) return;
|
|
2616
|
-
|
|
2617
|
-
const requestId = ++loadRequestIdRef.current;
|
|
2968
|
+
const token = request.begin();
|
|
2618
2969
|
void (async () => {
|
|
2619
2970
|
setLoading(true);
|
|
2620
|
-
|
|
2971
|
+
clearError();
|
|
2621
2972
|
try {
|
|
2622
2973
|
const buffer = await fetchEpubBuffer(epubUrl);
|
|
2623
|
-
if (
|
|
2624
|
-
await loadBufferForRequest(buffer,
|
|
2974
|
+
if (!request.isCurrent(token)) return;
|
|
2975
|
+
await loadBufferForRequest(buffer, token);
|
|
2625
2976
|
} catch (err) {
|
|
2626
|
-
if (
|
|
2977
|
+
if (request.isCurrent(token)) reportError(toError(err));
|
|
2627
2978
|
} finally {
|
|
2628
|
-
if (
|
|
2979
|
+
if (request.isCurrent(token)) setLoading(false);
|
|
2629
2980
|
}
|
|
2630
2981
|
})();
|
|
2631
2982
|
return () => {
|
|
2632
|
-
|
|
2983
|
+
if (!request.isCurrent(token)) return;
|
|
2984
|
+
request.invalidate();
|
|
2985
|
+
setLoading(false);
|
|
2633
2986
|
};
|
|
2634
|
-
}, [epubUrl, loadBufferForRequest, reportError]);
|
|
2635
|
-
const syncedRef =
|
|
2987
|
+
}, [epubUrl, clearError, loadBufferForRequest, reportError, request]);
|
|
2988
|
+
const syncedRef = useRef13(
|
|
2636
2989
|
null
|
|
2637
2990
|
);
|
|
2638
|
-
|
|
2991
|
+
useEffect10(() => {
|
|
2639
2992
|
const nextText = paragraph?.text ?? "";
|
|
2640
2993
|
const target = `${selection.chapter}:${selection.paragraph}`;
|
|
2641
2994
|
const prev = syncedRef.current;
|
|
@@ -2649,7 +3002,7 @@ function MejiroEditor({
|
|
|
2649
3002
|
setRubyEnd(Math.min(1, [...nextText].length));
|
|
2650
3003
|
setRubyText("");
|
|
2651
3004
|
}, [editor, selection.chapter, selection.paragraph, paragraph]);
|
|
2652
|
-
|
|
3005
|
+
useEffect10(() => {
|
|
2653
3006
|
if (!previewBook) return;
|
|
2654
3007
|
void readerRef.current?.goToAnchor({
|
|
2655
3008
|
chapter: selection.chapter,
|
|
@@ -2659,7 +3012,7 @@ function MejiroEditor({
|
|
|
2659
3012
|
}, [previewBook, selection.chapter, selection.paragraph]);
|
|
2660
3013
|
function selectParagraph(chapterIndex, paragraphIndex) {
|
|
2661
3014
|
if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
|
|
2662
|
-
|
|
3015
|
+
commitEdit();
|
|
2663
3016
|
setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
|
|
2664
3017
|
}
|
|
2665
3018
|
function captureRubyRange() {
|
|
@@ -2672,41 +3025,22 @@ function MejiroEditor({
|
|
|
2672
3025
|
setRubyStart(start);
|
|
2673
3026
|
setRubyEnd(Math.max(start + 1, end));
|
|
2674
3027
|
}
|
|
2675
|
-
function
|
|
3028
|
+
function commitEdit(ruby) {
|
|
2676
3029
|
if (!editor) return;
|
|
2677
|
-
editor
|
|
2678
|
-
|
|
3030
|
+
if (commitPendingEdit(editor, selection.chapter, selection.paragraph, text, ruby)) {
|
|
3031
|
+
setRevision((value) => value + 1);
|
|
3032
|
+
}
|
|
2679
3033
|
}
|
|
2680
3034
|
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
|
-
});
|
|
3035
|
+
if (!(rubyRange && rubyText.trim())) return;
|
|
3036
|
+
commitEdit({ start: rubyStart, end: rubyEnd, rubyText });
|
|
2702
3037
|
setRubyText("");
|
|
2703
|
-
setRevision((value) => value + 1);
|
|
2704
3038
|
}
|
|
2705
3039
|
async function addImage(file) {
|
|
2706
3040
|
await withErrorReporting(async () => {
|
|
2707
3041
|
if (!(editor && chapter)) return;
|
|
2708
3042
|
const chapterIndex = selection.chapter;
|
|
2709
|
-
const afterBlockId =
|
|
3043
|
+
const afterBlockId = paragraphBlock(chapter, selection.paragraph)?.id;
|
|
2710
3044
|
const data = await file.arrayBuffer();
|
|
2711
3045
|
editor.addImage(chapterIndex, {
|
|
2712
3046
|
filename: file.name,
|
|
@@ -2716,11 +3050,13 @@ function MejiroEditor({
|
|
|
2716
3050
|
afterBlockId
|
|
2717
3051
|
});
|
|
2718
3052
|
setRevision((value) => value + 1);
|
|
3053
|
+
clearError();
|
|
2719
3054
|
}, reportError);
|
|
2720
3055
|
}
|
|
2721
3056
|
async function exportEpub() {
|
|
2722
3057
|
await withErrorReporting(async () => {
|
|
2723
3058
|
if (!editor) return;
|
|
3059
|
+
commitEdit();
|
|
2724
3060
|
const watermark = exportPolicy?.watermark;
|
|
2725
3061
|
const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
|
|
2726
3062
|
let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
|
|
@@ -2728,15 +3064,20 @@ function MejiroEditor({
|
|
|
2728
3064
|
const decision = await onBeforeExport?.(buffer);
|
|
2729
3065
|
onExport?.(buffer);
|
|
2730
3066
|
const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
|
|
2731
|
-
if (
|
|
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);
|
|
3067
|
+
if (allowDownload) downloadEpub(buffer, `${editor.title || "edited"}.epub`);
|
|
3068
|
+
clearError();
|
|
2738
3069
|
}, reportError);
|
|
2739
3070
|
}
|
|
3071
|
+
const paragraphText = /* @__PURE__ */ jsx14(
|
|
3072
|
+
"textarea",
|
|
3073
|
+
{
|
|
3074
|
+
ref: textareaRef,
|
|
3075
|
+
value: text,
|
|
3076
|
+
readOnly: sceneBreak || !enableProofread,
|
|
3077
|
+
onChange: (event) => setText(event.target.value),
|
|
3078
|
+
onSelect: captureRubyRange
|
|
3079
|
+
}
|
|
3080
|
+
);
|
|
2740
3081
|
return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
|
|
2741
3082
|
/* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
|
|
2742
3083
|
previewBook ? /* @__PURE__ */ jsx14(
|
|
@@ -2746,6 +3087,7 @@ function MejiroEditor({
|
|
|
2746
3087
|
epub: previewBook,
|
|
2747
3088
|
chapter: selection.chapter,
|
|
2748
3089
|
onChapterChange: (index) => selectParagraph(index, 0),
|
|
3090
|
+
onError: reportError,
|
|
2749
3091
|
fonts,
|
|
2750
3092
|
subtitle: messages.editorPreviewSubtitle,
|
|
2751
3093
|
chapterNavMode: "panel",
|
|
@@ -2783,24 +3125,26 @@ function MejiroEditor({
|
|
|
2783
3125
|
] }),
|
|
2784
3126
|
enableProofread && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
|
|
2785
3127
|
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
|
|
3128
|
+
paragraphText,
|
|
2786
3129
|
/* @__PURE__ */ jsx14(
|
|
2787
|
-
"
|
|
3130
|
+
"button",
|
|
2788
3131
|
{
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
3132
|
+
type: "button",
|
|
3133
|
+
className: "mejiro-editor-primary",
|
|
3134
|
+
disabled: sceneBreak,
|
|
3135
|
+
onClick: () => commitEdit(),
|
|
3136
|
+
children: messages.editorApplyText
|
|
2793
3137
|
}
|
|
2794
|
-
)
|
|
2795
|
-
/* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
|
|
3138
|
+
)
|
|
2796
3139
|
] }),
|
|
2797
3140
|
enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
|
|
2798
3141
|
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
|
|
2799
3142
|
/* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
|
|
3143
|
+
!enableProofread && paragraphText,
|
|
2800
3144
|
/* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
|
|
2801
|
-
start:
|
|
2802
|
-
end:
|
|
2803
|
-
count:
|
|
3145
|
+
start: rubyRange?.start ?? 0,
|
|
3146
|
+
end: rubyRange?.end ?? 0,
|
|
3147
|
+
count: rubyRange ? rubyRange.end - rubyRange.start : 0
|
|
2804
3148
|
}) }),
|
|
2805
3149
|
/* @__PURE__ */ jsx14(
|
|
2806
3150
|
"input",
|
|
@@ -2815,7 +3159,7 @@ function MejiroEditor({
|
|
|
2815
3159
|
{
|
|
2816
3160
|
type: "button",
|
|
2817
3161
|
className: "mejiro-editor-primary",
|
|
2818
|
-
disabled:
|
|
3162
|
+
disabled: !(rubyRange && rubyText.trim()),
|
|
2819
3163
|
onClick: applyRuby,
|
|
2820
3164
|
children: messages.editorApplyRuby
|
|
2821
3165
|
}
|
|
@@ -2833,6 +3177,7 @@ function MejiroEditor({
|
|
|
2833
3177
|
hidden: true,
|
|
2834
3178
|
onChange: (event) => {
|
|
2835
3179
|
const file = event.target.files?.[0];
|
|
3180
|
+
event.target.value = "";
|
|
2836
3181
|
if (file) void addImage(file);
|
|
2837
3182
|
}
|
|
2838
3183
|
}
|
|
@@ -2851,27 +3196,27 @@ function MejiroEditor({
|
|
|
2851
3196
|
] })
|
|
2852
3197
|
] });
|
|
2853
3198
|
}
|
|
2854
|
-
function
|
|
3199
|
+
function paragraphBlock(chapter, paragraphIndex) {
|
|
2855
3200
|
let current = 0;
|
|
2856
3201
|
for (const block of chapter.blocks) {
|
|
2857
3202
|
if (block.kind !== "paragraph") continue;
|
|
2858
|
-
if (current === paragraphIndex) return block
|
|
3203
|
+
if (current === paragraphIndex) return block;
|
|
2859
3204
|
current++;
|
|
2860
3205
|
}
|
|
2861
3206
|
return void 0;
|
|
2862
3207
|
}
|
|
2863
3208
|
|
|
2864
3209
|
// src/MejiroManuscriptEditor.tsx
|
|
2865
|
-
import { EpubProject } from "@libraz/mejiro/epub";
|
|
2866
|
-
import { useCallback as
|
|
3210
|
+
import { EpubProject as EpubProject2 } from "@libraz/mejiro/epub";
|
|
3211
|
+
import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef17, useState as useState13 } from "react";
|
|
2867
3212
|
|
|
2868
3213
|
// src/MejiroNotationHighlighter.tsx
|
|
2869
3214
|
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
2870
3215
|
import {
|
|
2871
3216
|
forwardRef as forwardRef2,
|
|
2872
3217
|
useImperativeHandle as useImperativeHandle2,
|
|
2873
|
-
useMemo as
|
|
2874
|
-
useRef as
|
|
3218
|
+
useMemo as useMemo7,
|
|
3219
|
+
useRef as useRef14,
|
|
2875
3220
|
useState as useState10
|
|
2876
3221
|
} from "react";
|
|
2877
3222
|
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
@@ -2888,12 +3233,13 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
2888
3233
|
onCompositionEnd,
|
|
2889
3234
|
...rest
|
|
2890
3235
|
}, ref) {
|
|
2891
|
-
const textareaRef =
|
|
2892
|
-
const overlayRef =
|
|
3236
|
+
const textareaRef = useRef14(null);
|
|
3237
|
+
const overlayRef = useRef14(null);
|
|
2893
3238
|
useImperativeHandle2(ref, () => textareaRef.current, []);
|
|
2894
3239
|
const [isComposing, setIsComposing] = useState10(false);
|
|
2895
|
-
const
|
|
2896
|
-
const
|
|
3240
|
+
const [composingValue, setComposingValue] = useState10("");
|
|
3241
|
+
const lastSegmentsRef = useRef14(null);
|
|
3242
|
+
const segments = useMemo7(() => {
|
|
2897
3243
|
if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
|
|
2898
3244
|
const next = buildSegments(value, dialect);
|
|
2899
3245
|
lastSegmentsRef.current = next;
|
|
@@ -2932,14 +3278,19 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
2932
3278
|
ref: textareaRef,
|
|
2933
3279
|
className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
|
|
2934
3280
|
style,
|
|
2935
|
-
value,
|
|
2936
|
-
onChange: (event) =>
|
|
3281
|
+
value: isComposing ? composingValue : value,
|
|
3282
|
+
onChange: (event) => {
|
|
3283
|
+
if (isComposing) setComposingValue(event.target.value);
|
|
3284
|
+
onChange(event.target.value);
|
|
3285
|
+
},
|
|
2937
3286
|
onCompositionStart: (event) => {
|
|
3287
|
+
setComposingValue(event.currentTarget.value);
|
|
2938
3288
|
setIsComposing(true);
|
|
2939
3289
|
onCompositionStart?.(event);
|
|
2940
3290
|
},
|
|
2941
3291
|
onCompositionEnd: (event) => {
|
|
2942
3292
|
setIsComposing(false);
|
|
3293
|
+
onChange(event.currentTarget.value);
|
|
2943
3294
|
onCompositionEnd?.(event);
|
|
2944
3295
|
},
|
|
2945
3296
|
onScroll: (event) => {
|
|
@@ -2980,10 +3331,276 @@ function buildSegments(text, dialect) {
|
|
|
2980
3331
|
return segments;
|
|
2981
3332
|
}
|
|
2982
3333
|
|
|
3334
|
+
// src/useEpubProject.ts
|
|
3335
|
+
import {
|
|
3336
|
+
EpubProject,
|
|
3337
|
+
parseEpub as parseEpub2
|
|
3338
|
+
} from "@libraz/mejiro/epub";
|
|
3339
|
+
import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState11 } from "react";
|
|
3340
|
+
function useEpubProject(options = {}) {
|
|
3341
|
+
const defaultTitle = options.defaultChapterTitle;
|
|
3342
|
+
const defaultBody = options.defaultChapterBody;
|
|
3343
|
+
const [metadata, setMetadataState] = useState11(
|
|
3344
|
+
() => initialMetadata(options.metadata)
|
|
3345
|
+
);
|
|
3346
|
+
const [chapters, setChaptersState] = useState11(
|
|
3347
|
+
() => options.chapters?.length ? options.chapters : [defaultChapter([], defaultTitle, defaultBody)]
|
|
3348
|
+
);
|
|
3349
|
+
const [selectedChapter, setSelectedChapterState] = useState11(0);
|
|
3350
|
+
const [cover, setCoverState] = useState11(options.cover ?? null);
|
|
3351
|
+
const [assets, setAssetsState] = useState11(options.assets ?? []);
|
|
3352
|
+
const [previewBook, setPreviewBook] = useState11(null);
|
|
3353
|
+
const [previewError, setPreviewError] = useState11(null);
|
|
3354
|
+
const [previewing, setPreviewing] = useState11(false);
|
|
3355
|
+
const previewRequest = useRequestToken();
|
|
3356
|
+
const onPreviewRef = useRef15(options.onPreview);
|
|
3357
|
+
const onExportRef = useRef15(options.onExport);
|
|
3358
|
+
onPreviewRef.current = options.onPreview;
|
|
3359
|
+
onExportRef.current = options.onExport;
|
|
3360
|
+
const assetResolverRef = useRef15(options.assetResolver);
|
|
3361
|
+
assetResolverRef.current = options.assetResolver;
|
|
3362
|
+
const metadataRef = useRef15(metadata);
|
|
3363
|
+
metadataRef.current = metadata;
|
|
3364
|
+
const chaptersRef = useRef15(chapters);
|
|
3365
|
+
chaptersRef.current = chapters;
|
|
3366
|
+
const selectedRef = useRef15(selectedChapter);
|
|
3367
|
+
selectedRef.current = selectedChapter;
|
|
3368
|
+
const coverRef = useRef15(cover);
|
|
3369
|
+
coverRef.current = cover;
|
|
3370
|
+
const assetsRef = useRef15(assets);
|
|
3371
|
+
assetsRef.current = assets;
|
|
3372
|
+
const buildProject = useCallback9(() => {
|
|
3373
|
+
const currentCover = coverRef.current;
|
|
3374
|
+
const project = EpubProject.fromManuscript({
|
|
3375
|
+
metadata: metadataRef.current,
|
|
3376
|
+
includeTitlePage: false,
|
|
3377
|
+
includeTitleInFirstChapter: true,
|
|
3378
|
+
chapters: chaptersRef.current.map((chapter) => ({
|
|
3379
|
+
id: chapter.id,
|
|
3380
|
+
title: chapter.title || "Untitled",
|
|
3381
|
+
body: chapter.body
|
|
3382
|
+
})),
|
|
3383
|
+
...currentCover ? { cover: currentCover } : {}
|
|
3384
|
+
});
|
|
3385
|
+
for (const asset of assetsRef.current) project.addAsset(asset);
|
|
3386
|
+
return project;
|
|
3387
|
+
}, []);
|
|
3388
|
+
const exportOptions = useCallback9((signal) => {
|
|
3389
|
+
const resolver = assetResolverRef.current;
|
|
3390
|
+
return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
|
|
3391
|
+
}, []);
|
|
3392
|
+
useEffect11(() => {
|
|
3393
|
+
const token = previewRequest.begin();
|
|
3394
|
+
const controller = new AbortController();
|
|
3395
|
+
setPreviewing(true);
|
|
3396
|
+
const timer = setTimeout(() => {
|
|
3397
|
+
void (async () => {
|
|
3398
|
+
try {
|
|
3399
|
+
const book = await parseEpub2(
|
|
3400
|
+
await buildProject().export(exportOptions(controller.signal))
|
|
3401
|
+
);
|
|
3402
|
+
if (!previewRequest.isCurrent(token)) return;
|
|
3403
|
+
setPreviewBook(book);
|
|
3404
|
+
setPreviewError(null);
|
|
3405
|
+
onPreviewRef.current?.(book);
|
|
3406
|
+
} catch (err) {
|
|
3407
|
+
if (previewRequest.isCurrent(token)) setPreviewError(toError(err));
|
|
3408
|
+
} finally {
|
|
3409
|
+
if (previewRequest.isCurrent(token)) setPreviewing(false);
|
|
3410
|
+
}
|
|
3411
|
+
})();
|
|
3412
|
+
}, options.debounceMs ?? 250);
|
|
3413
|
+
return () => {
|
|
3414
|
+
previewRequest.invalidate();
|
|
3415
|
+
clearTimeout(timer);
|
|
3416
|
+
controller.abort();
|
|
3417
|
+
};
|
|
3418
|
+
}, [
|
|
3419
|
+
assets,
|
|
3420
|
+
buildProject,
|
|
3421
|
+
chapters,
|
|
3422
|
+
cover,
|
|
3423
|
+
exportOptions,
|
|
3424
|
+
metadata,
|
|
3425
|
+
options.debounceMs,
|
|
3426
|
+
previewRequest
|
|
3427
|
+
]);
|
|
3428
|
+
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
3429
|
+
const setMetadata = useCallback9((patch) => {
|
|
3430
|
+
const next = mergeMetadata(metadataRef.current, patch);
|
|
3431
|
+
metadataRef.current = next;
|
|
3432
|
+
setMetadataState(next);
|
|
3433
|
+
}, []);
|
|
3434
|
+
const commit = useCallback9((nextChapters, nextSelected) => {
|
|
3435
|
+
chaptersRef.current = nextChapters;
|
|
3436
|
+
selectedRef.current = nextSelected;
|
|
3437
|
+
setChaptersState(nextChapters);
|
|
3438
|
+
setSelectedChapterState(nextSelected);
|
|
3439
|
+
}, []);
|
|
3440
|
+
const setChapters = useCallback9(
|
|
3441
|
+
(next) => {
|
|
3442
|
+
const normalized = next.length ? next : [defaultChapter([], defaultTitle, defaultBody)];
|
|
3443
|
+
const prev = selectedRef.current;
|
|
3444
|
+
const selectedId = chaptersRef.current[prev]?.id;
|
|
3445
|
+
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
3446
|
+
commit(
|
|
3447
|
+
normalized,
|
|
3448
|
+
nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
|
|
3449
|
+
);
|
|
3450
|
+
},
|
|
3451
|
+
[commit, defaultBody, defaultTitle]
|
|
3452
|
+
);
|
|
3453
|
+
const setCover = useCallback9((asset) => {
|
|
3454
|
+
coverRef.current = asset;
|
|
3455
|
+
setCoverState(asset);
|
|
3456
|
+
}, []);
|
|
3457
|
+
const setAssets = useCallback9((next) => {
|
|
3458
|
+
assetsRef.current = next;
|
|
3459
|
+
setAssetsState(next);
|
|
3460
|
+
}, []);
|
|
3461
|
+
const setSelectedChapter = useCallback9((index) => {
|
|
3462
|
+
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
3463
|
+
selectedRef.current = next;
|
|
3464
|
+
setSelectedChapterState(next);
|
|
3465
|
+
}, []);
|
|
3466
|
+
const patchChapter = useCallback9((index, patch) => {
|
|
3467
|
+
const next = chaptersRef.current.map(
|
|
3468
|
+
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3469
|
+
);
|
|
3470
|
+
chaptersRef.current = next;
|
|
3471
|
+
setChaptersState(next);
|
|
3472
|
+
}, []);
|
|
3473
|
+
const addChapter = useCallback9(
|
|
3474
|
+
(chapter = {}) => {
|
|
3475
|
+
const current = chaptersRef.current;
|
|
3476
|
+
const generated = defaultChapter(current, defaultTitle, defaultBody);
|
|
3477
|
+
const next = [
|
|
3478
|
+
...current,
|
|
3479
|
+
{
|
|
3480
|
+
id: chapter.id ?? generated.id,
|
|
3481
|
+
title: chapter.title ?? generated.title,
|
|
3482
|
+
body: chapter.body ?? generated.body
|
|
3483
|
+
}
|
|
3484
|
+
];
|
|
3485
|
+
commit(next, next.length - 1);
|
|
3486
|
+
},
|
|
3487
|
+
[commit, defaultBody, defaultTitle]
|
|
3488
|
+
);
|
|
3489
|
+
const removeChapter = useCallback9(
|
|
3490
|
+
(index = selectedRef.current) => {
|
|
3491
|
+
const current = chaptersRef.current;
|
|
3492
|
+
if (current.length <= 1) return;
|
|
3493
|
+
const next = current.filter((_, chapterIndex) => chapterIndex !== index);
|
|
3494
|
+
const prev = selectedRef.current;
|
|
3495
|
+
let nextSelected;
|
|
3496
|
+
if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
|
|
3497
|
+
else if (index < prev) nextSelected = prev - 1;
|
|
3498
|
+
else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
|
|
3499
|
+
commit(next, nextSelected);
|
|
3500
|
+
},
|
|
3501
|
+
[commit]
|
|
3502
|
+
);
|
|
3503
|
+
const reorderChapters = useCallback9(
|
|
3504
|
+
(from, to) => {
|
|
3505
|
+
const current = chaptersRef.current;
|
|
3506
|
+
if (from < 0 || from >= current.length) return;
|
|
3507
|
+
const next = [...current];
|
|
3508
|
+
const [moved] = next.splice(from, 1);
|
|
3509
|
+
const target = Math.max(0, Math.min(next.length, to));
|
|
3510
|
+
next.splice(target, 0, moved);
|
|
3511
|
+
const prev = selectedRef.current;
|
|
3512
|
+
let nextSelected = prev;
|
|
3513
|
+
if (prev === from) nextSelected = target;
|
|
3514
|
+
else if (from < prev && target >= prev) nextSelected = prev - 1;
|
|
3515
|
+
else if (from > prev && target <= prev) nextSelected = prev + 1;
|
|
3516
|
+
commit(next, nextSelected);
|
|
3517
|
+
},
|
|
3518
|
+
[commit]
|
|
3519
|
+
);
|
|
3520
|
+
const exportEpub = useCallback9(async () => {
|
|
3521
|
+
const buffer = await buildProject().export(exportOptions());
|
|
3522
|
+
onExportRef.current?.(buffer);
|
|
3523
|
+
return buffer;
|
|
3524
|
+
}, [buildProject, exportOptions]);
|
|
3525
|
+
return useMemo8(
|
|
3526
|
+
() => ({
|
|
3527
|
+
metadata,
|
|
3528
|
+
chapters,
|
|
3529
|
+
selectedChapter,
|
|
3530
|
+
currentChapter,
|
|
3531
|
+
cover,
|
|
3532
|
+
assets,
|
|
3533
|
+
previewBook,
|
|
3534
|
+
previewError,
|
|
3535
|
+
previewing,
|
|
3536
|
+
setMetadata,
|
|
3537
|
+
setChapters,
|
|
3538
|
+
setSelectedChapter,
|
|
3539
|
+
setCover,
|
|
3540
|
+
setAssets,
|
|
3541
|
+
patchChapter,
|
|
3542
|
+
addChapter,
|
|
3543
|
+
removeChapter,
|
|
3544
|
+
reorderChapters,
|
|
3545
|
+
buildProject,
|
|
3546
|
+
exportEpub
|
|
3547
|
+
}),
|
|
3548
|
+
[
|
|
3549
|
+
addChapter,
|
|
3550
|
+
assets,
|
|
3551
|
+
buildProject,
|
|
3552
|
+
chapters,
|
|
3553
|
+
cover,
|
|
3554
|
+
currentChapter,
|
|
3555
|
+
exportEpub,
|
|
3556
|
+
metadata,
|
|
3557
|
+
patchChapter,
|
|
3558
|
+
previewBook,
|
|
3559
|
+
previewError,
|
|
3560
|
+
previewing,
|
|
3561
|
+
reorderChapters,
|
|
3562
|
+
removeChapter,
|
|
3563
|
+
selectedChapter,
|
|
3564
|
+
setAssets,
|
|
3565
|
+
setChapters,
|
|
3566
|
+
setCover,
|
|
3567
|
+
setMetadata,
|
|
3568
|
+
setSelectedChapter
|
|
3569
|
+
]
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
function mergeMetadata(current, patch) {
|
|
3573
|
+
const next = { ...current, ...patch };
|
|
3574
|
+
if (!next.identifier?.trim()) next.identifier = current.identifier;
|
|
3575
|
+
return next;
|
|
3576
|
+
}
|
|
3577
|
+
function createPackageIdentifier() {
|
|
3578
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
3579
|
+
return `urn:uuid:${globalThis.crypto.randomUUID()}`;
|
|
3580
|
+
}
|
|
3581
|
+
const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
3582
|
+
const digit = Math.floor(Math.random() * 16);
|
|
3583
|
+
return (ch === "x" ? digit : digit & 3 | 8).toString(16);
|
|
3584
|
+
});
|
|
3585
|
+
return `urn:uuid:${uuid}`;
|
|
3586
|
+
}
|
|
3587
|
+
function initialMetadata(patch = {}) {
|
|
3588
|
+
const metadata = mergeDefined(
|
|
3589
|
+
{ title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
|
|
3590
|
+
patch
|
|
3591
|
+
);
|
|
3592
|
+
if (!metadata.identifier?.trim()) metadata.identifier = createPackageIdentifier();
|
|
3593
|
+
return metadata;
|
|
3594
|
+
}
|
|
3595
|
+
function defaultChapter(existing, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
|
|
3596
|
+
const index = existing.length;
|
|
3597
|
+
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
3598
|
+
}
|
|
3599
|
+
|
|
2983
3600
|
// src/useManuscriptDraft.ts
|
|
2984
|
-
import { useCallback as
|
|
3601
|
+
import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState12 } from "react";
|
|
2985
3602
|
var DEFAULT_DELAY = 800;
|
|
2986
|
-
function
|
|
3603
|
+
function defaultChapter2(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
|
|
2987
3604
|
const index = existing.length;
|
|
2988
3605
|
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
2989
3606
|
}
|
|
@@ -2996,49 +3613,66 @@ function useManuscriptDraft(options = {}) {
|
|
|
2996
3613
|
defaultChapterTitle,
|
|
2997
3614
|
defaultChapterBody
|
|
2998
3615
|
} = options;
|
|
2999
|
-
const titleForRef =
|
|
3616
|
+
const titleForRef = useRef16(defaultChapterTitle);
|
|
3000
3617
|
titleForRef.current = defaultChapterTitle;
|
|
3001
|
-
const bodyForRef =
|
|
3618
|
+
const bodyForRef = useRef16(defaultChapterBody);
|
|
3002
3619
|
bodyForRef.current = defaultChapterBody;
|
|
3003
|
-
const [chapters, setChaptersState] =
|
|
3004
|
-
() => options.initialChapters?.length ? options.initialChapters : [
|
|
3620
|
+
const [chapters, setChaptersState] = useState12(
|
|
3621
|
+
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter2([], defaultChapterTitle, defaultChapterBody)]
|
|
3005
3622
|
);
|
|
3006
|
-
const [selected, setSelectedState] =
|
|
3007
|
-
const [autosaveError, setAutosaveError] =
|
|
3008
|
-
const saveRef =
|
|
3623
|
+
const [selected, setSelectedState] = useState12(0);
|
|
3624
|
+
const [autosaveError, setAutosaveError] = useState12(null);
|
|
3625
|
+
const saveRef = useRef16(onAutosave);
|
|
3009
3626
|
saveRef.current = onAutosave;
|
|
3010
|
-
const payloadRef =
|
|
3627
|
+
const payloadRef = useRef16(autosavePayload);
|
|
3011
3628
|
payloadRef.current = autosavePayload;
|
|
3012
|
-
const chaptersRef =
|
|
3629
|
+
const chaptersRef = useRef16(chapters);
|
|
3013
3630
|
chaptersRef.current = chapters;
|
|
3014
|
-
const selectedRef =
|
|
3631
|
+
const selectedRef = useRef16(selected);
|
|
3015
3632
|
selectedRef.current = selected;
|
|
3016
|
-
const trackerRef =
|
|
3633
|
+
const trackerRef = useRef16(null);
|
|
3017
3634
|
if (!trackerRef.current)
|
|
3018
3635
|
trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
|
|
3019
|
-
const dirtyRef =
|
|
3020
|
-
const mountedRef =
|
|
3021
|
-
const revisionRef =
|
|
3022
|
-
const
|
|
3023
|
-
const
|
|
3636
|
+
const dirtyRef = useRef16(false);
|
|
3637
|
+
const mountedRef = useRef16(true);
|
|
3638
|
+
const revisionRef = useRef16(0);
|
|
3639
|
+
const inFlightRef = useRef16(false);
|
|
3640
|
+
const flushQueuedRef = useRef16(false);
|
|
3641
|
+
const flushAutosave = useCallback10(() => {
|
|
3024
3642
|
const callback = saveRef.current;
|
|
3025
|
-
if (!(callback && dirtyRef.current)
|
|
3643
|
+
if (!(callback && dirtyRef.current)) return;
|
|
3644
|
+
if (inFlightRef.current) {
|
|
3645
|
+
flushQueuedRef.current = true;
|
|
3026
3646
|
return;
|
|
3027
3647
|
}
|
|
3028
3648
|
const revision = revisionRef.current;
|
|
3029
|
-
inFlightRevisionRef.current = revision;
|
|
3030
3649
|
const plain = snapshotChapters(chaptersRef.current);
|
|
3031
3650
|
const payload = payloadRef.current ? payloadRef.current(plain) : plain;
|
|
3032
|
-
|
|
3651
|
+
const markSaved = () => {
|
|
3033
3652
|
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
3034
|
-
}
|
|
3653
|
+
};
|
|
3654
|
+
let saved;
|
|
3655
|
+
try {
|
|
3656
|
+
saved = callback(payload);
|
|
3657
|
+
} catch (err) {
|
|
3658
|
+
saved = Promise.reject(err);
|
|
3659
|
+
}
|
|
3660
|
+
if (saved === void 0) {
|
|
3661
|
+
markSaved();
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
inFlightRef.current = true;
|
|
3665
|
+
void Promise.resolve(saved).then(markSaved).catch((err) => {
|
|
3035
3666
|
if (!mountedRef.current) return;
|
|
3036
3667
|
setAutosaveError(toError(err));
|
|
3037
3668
|
}).finally(() => {
|
|
3038
|
-
|
|
3669
|
+
inFlightRef.current = false;
|
|
3670
|
+
if (!flushQueuedRef.current) return;
|
|
3671
|
+
flushQueuedRef.current = false;
|
|
3672
|
+
flushAutosave();
|
|
3039
3673
|
});
|
|
3040
3674
|
}, []);
|
|
3041
|
-
|
|
3675
|
+
useEffect12(() => {
|
|
3042
3676
|
if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
|
|
3043
3677
|
dirtyRef.current = true;
|
|
3044
3678
|
revisionRef.current += 1;
|
|
@@ -3050,7 +3684,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
3050
3684
|
}, autosaveDelay);
|
|
3051
3685
|
return () => clearTimeout(timer);
|
|
3052
3686
|
}, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
|
|
3053
|
-
|
|
3687
|
+
useEffect12(() => {
|
|
3054
3688
|
mountedRef.current = true;
|
|
3055
3689
|
const detachPageHide = flushOnPageHide(flushAutosave);
|
|
3056
3690
|
return () => {
|
|
@@ -3059,20 +3693,20 @@ function useManuscriptDraft(options = {}) {
|
|
|
3059
3693
|
detachPageHide();
|
|
3060
3694
|
};
|
|
3061
3695
|
}, [flushAutosave]);
|
|
3062
|
-
const commit =
|
|
3696
|
+
const commit = useCallback10((nextChapters, nextSelected) => {
|
|
3063
3697
|
chaptersRef.current = nextChapters;
|
|
3064
3698
|
selectedRef.current = nextSelected;
|
|
3065
3699
|
setChaptersState(nextChapters);
|
|
3066
3700
|
setSelectedState(nextSelected);
|
|
3067
3701
|
}, []);
|
|
3068
|
-
const setSelected =
|
|
3702
|
+
const setSelected = useCallback10((index) => {
|
|
3069
3703
|
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
3070
3704
|
selectedRef.current = next;
|
|
3071
3705
|
setSelectedState(next);
|
|
3072
3706
|
}, []);
|
|
3073
|
-
const setChapters =
|
|
3707
|
+
const setChapters = useCallback10(
|
|
3074
3708
|
(next) => {
|
|
3075
|
-
const normalized = next.length ? next : [
|
|
3709
|
+
const normalized = next.length ? next : [defaultChapter2([], titleForRef.current, bodyForRef.current)];
|
|
3076
3710
|
const prev = selectedRef.current;
|
|
3077
3711
|
const selectedId = chaptersRef.current[prev]?.id;
|
|
3078
3712
|
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
@@ -3083,17 +3717,17 @@ function useManuscriptDraft(options = {}) {
|
|
|
3083
3717
|
},
|
|
3084
3718
|
[commit]
|
|
3085
3719
|
);
|
|
3086
|
-
const patchChapter =
|
|
3720
|
+
const patchChapter = useCallback10((index, patch) => {
|
|
3087
3721
|
const next = chaptersRef.current.map(
|
|
3088
3722
|
(chapter, i) => i === index ? { ...chapter, ...patch } : chapter
|
|
3089
3723
|
);
|
|
3090
3724
|
chaptersRef.current = next;
|
|
3091
3725
|
setChaptersState(next);
|
|
3092
3726
|
}, []);
|
|
3093
|
-
const addChapter =
|
|
3727
|
+
const addChapter = useCallback10(
|
|
3094
3728
|
(chapter = {}) => {
|
|
3095
3729
|
const current = chaptersRef.current;
|
|
3096
|
-
const generated =
|
|
3730
|
+
const generated = defaultChapter2(current, titleForRef.current, bodyForRef.current);
|
|
3097
3731
|
const next = [
|
|
3098
3732
|
...current,
|
|
3099
3733
|
{
|
|
@@ -3106,7 +3740,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
3106
3740
|
},
|
|
3107
3741
|
[commit]
|
|
3108
3742
|
);
|
|
3109
|
-
const removeChapter =
|
|
3743
|
+
const removeChapter = useCallback10(
|
|
3110
3744
|
(index) => {
|
|
3111
3745
|
const current = chaptersRef.current;
|
|
3112
3746
|
if (current.length <= 1) return;
|
|
@@ -3120,7 +3754,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
3120
3754
|
},
|
|
3121
3755
|
[commit]
|
|
3122
3756
|
);
|
|
3123
|
-
const reorderChapters =
|
|
3757
|
+
const reorderChapters = useCallback10(
|
|
3124
3758
|
(from, to) => {
|
|
3125
3759
|
const current = chaptersRef.current;
|
|
3126
3760
|
if (from < 0 || from >= current.length) return;
|
|
@@ -3153,14 +3787,6 @@ function useManuscriptDraft(options = {}) {
|
|
|
3153
3787
|
|
|
3154
3788
|
// src/MejiroManuscriptEditor.tsx
|
|
3155
3789
|
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
3790
|
function coverExtension(mediaType) {
|
|
3165
3791
|
switch (mediaType) {
|
|
3166
3792
|
case "image/gif":
|
|
@@ -3181,7 +3807,7 @@ function coverAssetHref(file) {
|
|
|
3181
3807
|
const filename = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3182
3808
|
return `OPS/Images/${filename && !/^\.+$/u.test(filename) ? filename : `cover${coverExtension(file.type)}`}`;
|
|
3183
3809
|
}
|
|
3184
|
-
function
|
|
3810
|
+
function defaultChapter3(messages) {
|
|
3185
3811
|
return {
|
|
3186
3812
|
id: `chapter-${Date.now()}-0`,
|
|
3187
3813
|
title: format(messages.manuscriptDefaultChapterTitle, { n: 1 }),
|
|
@@ -3207,41 +3833,41 @@ function MejiroManuscriptEditor({
|
|
|
3207
3833
|
panelSide = "right"
|
|
3208
3834
|
}) {
|
|
3209
3835
|
const messages = useI18n();
|
|
3210
|
-
const coverInputRef =
|
|
3211
|
-
const bodyTextareaRef =
|
|
3836
|
+
const coverInputRef = useRef17(null);
|
|
3837
|
+
const bodyTextareaRef = useRef17(null);
|
|
3212
3838
|
const titleControlled = onTitleChange !== void 0;
|
|
3213
3839
|
const authorControlled = onAuthorChange !== void 0;
|
|
3214
3840
|
const coverControlled = onCoverChange !== void 0;
|
|
3215
|
-
const [titleState, setTitleState] =
|
|
3216
|
-
const [authorState, setAuthorState] =
|
|
3217
|
-
const [coverState, setCoverState] =
|
|
3841
|
+
const [titleState, setTitleState] = useState13(titleProp ?? messages.manuscriptDefaultTitle);
|
|
3842
|
+
const [authorState, setAuthorState] = useState13(authorProp ?? "");
|
|
3843
|
+
const [coverState, setCoverState] = useState13(coverProp ?? null);
|
|
3218
3844
|
const title = titleControlled ? titleProp ?? "" : titleState;
|
|
3219
3845
|
const author = authorControlled ? authorProp ?? "" : authorState;
|
|
3220
3846
|
const cover = coverControlled ? coverProp ?? null : coverState;
|
|
3221
|
-
|
|
3847
|
+
useEffect13(() => {
|
|
3222
3848
|
if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
|
|
3223
3849
|
}, [titleControlled, titleProp]);
|
|
3224
|
-
|
|
3850
|
+
useEffect13(() => {
|
|
3225
3851
|
if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
|
|
3226
3852
|
}, [authorControlled, authorProp]);
|
|
3227
|
-
|
|
3853
|
+
useEffect13(() => {
|
|
3228
3854
|
if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
|
|
3229
3855
|
}, [coverControlled, coverProp]);
|
|
3230
|
-
const setTitle =
|
|
3856
|
+
const setTitle = useCallback11(
|
|
3231
3857
|
(next) => {
|
|
3232
3858
|
if (!titleControlled) setTitleState(next);
|
|
3233
3859
|
onTitleChange?.(next);
|
|
3234
3860
|
},
|
|
3235
3861
|
[titleControlled, onTitleChange]
|
|
3236
3862
|
);
|
|
3237
|
-
const setAuthor =
|
|
3863
|
+
const setAuthor = useCallback11(
|
|
3238
3864
|
(next) => {
|
|
3239
3865
|
if (!authorControlled) setAuthorState(next);
|
|
3240
3866
|
onAuthorChange?.(next);
|
|
3241
3867
|
},
|
|
3242
3868
|
[authorControlled, onAuthorChange]
|
|
3243
3869
|
);
|
|
3244
|
-
const setCover =
|
|
3870
|
+
const setCover = useCallback11(
|
|
3245
3871
|
(next) => {
|
|
3246
3872
|
if (!coverControlled) setCoverState(next);
|
|
3247
3873
|
onCoverChange?.(next);
|
|
@@ -3249,7 +3875,7 @@ function MejiroManuscriptEditor({
|
|
|
3249
3875
|
[coverControlled, onCoverChange]
|
|
3250
3876
|
);
|
|
3251
3877
|
const draft = useManuscriptDraft({
|
|
3252
|
-
initialChapters: initialChapters?.length ? initialChapters : [
|
|
3878
|
+
initialChapters: initialChapters?.length ? initialChapters : [defaultChapter3(messages)],
|
|
3253
3879
|
onAutosave,
|
|
3254
3880
|
autosavePayload: (savedChapters) => ({ title, author, cover, chapters: savedChapters }),
|
|
3255
3881
|
autosaveKey: `${title}\0${author}\0${cover?.name ?? ""}\0${cover?.size ?? 0}\0${cover?.lastModified ?? 0}`,
|
|
@@ -3266,8 +3892,16 @@ function MejiroManuscriptEditor({
|
|
|
3266
3892
|
reorderChapters
|
|
3267
3893
|
} = draft;
|
|
3268
3894
|
const current = chapters[selected] ?? chapters[0];
|
|
3269
|
-
const [draggingIndex, setDraggingIndex] =
|
|
3270
|
-
const [exportError, setExportError] =
|
|
3895
|
+
const [draggingIndex, setDraggingIndex] = useState13(null);
|
|
3896
|
+
const [exportError, setExportError] = useState13(null);
|
|
3897
|
+
const reportError = useCallback11(
|
|
3898
|
+
(error) => {
|
|
3899
|
+
setExportError(error);
|
|
3900
|
+
onError?.(error);
|
|
3901
|
+
},
|
|
3902
|
+
[onError]
|
|
3903
|
+
);
|
|
3904
|
+
const clearError = useCallback11(() => setExportError(null), []);
|
|
3271
3905
|
function wrapSelection(open, close) {
|
|
3272
3906
|
const el = bodyTextareaRef.current;
|
|
3273
3907
|
if (!el) return;
|
|
@@ -3285,37 +3919,31 @@ function MejiroManuscriptEditor({
|
|
|
3285
3919
|
bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
|
|
3286
3920
|
});
|
|
3287
3921
|
}
|
|
3288
|
-
const [identifier] =
|
|
3289
|
-
const exportEpub =
|
|
3290
|
-
await withErrorReporting(
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3922
|
+
const [identifier] = useState13(createPackageIdentifier);
|
|
3923
|
+
const exportEpub = useCallback11(async () => {
|
|
3924
|
+
await withErrorReporting(async () => {
|
|
3925
|
+
const project = EpubProject2.fromManuscript({
|
|
3926
|
+
metadata: { title, author: author || void 0, identifier },
|
|
3927
|
+
dialect,
|
|
3928
|
+
chapters: chapters.map((chapter) => ({
|
|
3929
|
+
id: chapter.id,
|
|
3930
|
+
title: chapter.title || messages.untitled,
|
|
3931
|
+
body: chapter.body
|
|
3932
|
+
}))
|
|
3933
|
+
});
|
|
3934
|
+
if (cover) {
|
|
3935
|
+
project.setCover({
|
|
3936
|
+
href: coverAssetHref(cover),
|
|
3937
|
+
mediaType: cover.type || void 0,
|
|
3938
|
+
data: await cover.arrayBuffer()
|
|
3300
3939
|
});
|
|
3301
|
-
if (cover) {
|
|
3302
|
-
project.setCover({
|
|
3303
|
-
href: coverAssetHref(cover),
|
|
3304
|
-
mediaType: cover.type || void 0,
|
|
3305
|
-
data: await cover.arrayBuffer()
|
|
3306
|
-
});
|
|
3307
|
-
}
|
|
3308
|
-
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
3309
|
-
setExportError(null);
|
|
3310
|
-
onExport?.(buffer);
|
|
3311
|
-
downloadEpub(buffer, title);
|
|
3312
|
-
},
|
|
3313
|
-
(error) => {
|
|
3314
|
-
setExportError(error);
|
|
3315
|
-
onError?.(error);
|
|
3316
3940
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3941
|
+
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
3942
|
+
clearError();
|
|
3943
|
+
onExport?.(buffer);
|
|
3944
|
+
downloadEpub(buffer, `${title || "book"}.epub`);
|
|
3945
|
+
}, reportError);
|
|
3946
|
+
}, [
|
|
3319
3947
|
assetResolver,
|
|
3320
3948
|
author,
|
|
3321
3949
|
chapters,
|
|
@@ -3323,11 +3951,12 @@ function MejiroManuscriptEditor({
|
|
|
3323
3951
|
dialect,
|
|
3324
3952
|
identifier,
|
|
3325
3953
|
messages.untitled,
|
|
3326
|
-
onError,
|
|
3327
3954
|
onExport,
|
|
3955
|
+
clearError,
|
|
3956
|
+
reportError,
|
|
3328
3957
|
title
|
|
3329
3958
|
]);
|
|
3330
|
-
const previewManuscript =
|
|
3959
|
+
const previewManuscript = useMemo9(
|
|
3331
3960
|
() => chapters.map((chapter) => ({
|
|
3332
3961
|
id: chapter.id,
|
|
3333
3962
|
title: chapter.title || messages.untitled,
|
|
@@ -3352,6 +3981,7 @@ function MejiroManuscriptEditor({
|
|
|
3352
3981
|
fonts,
|
|
3353
3982
|
chapter: selected,
|
|
3354
3983
|
onChapterChange: setSelected,
|
|
3984
|
+
onError: reportError,
|
|
3355
3985
|
enableImageOverlay: false
|
|
3356
3986
|
}
|
|
3357
3987
|
) }),
|
|
@@ -3375,7 +4005,11 @@ function MejiroManuscriptEditor({
|
|
|
3375
4005
|
type: "file",
|
|
3376
4006
|
accept: "image/*",
|
|
3377
4007
|
hidden: true,
|
|
3378
|
-
onChange: (event) =>
|
|
4008
|
+
onChange: (event) => {
|
|
4009
|
+
const file = event.target.files?.[0] ?? null;
|
|
4010
|
+
event.target.value = "";
|
|
4011
|
+
setCover(file);
|
|
4012
|
+
}
|
|
3379
4013
|
}
|
|
3380
4014
|
)
|
|
3381
4015
|
] }),
|
|
@@ -3394,6 +4028,7 @@ function MejiroManuscriptEditor({
|
|
|
3394
4028
|
onDragStart: (event) => {
|
|
3395
4029
|
setDraggingIndex(index);
|
|
3396
4030
|
event.dataTransfer.effectAllowed = "move";
|
|
4031
|
+
event.dataTransfer.setData("text/plain", chapter.id);
|
|
3397
4032
|
},
|
|
3398
4033
|
onDragEnd: () => setDraggingIndex(null),
|
|
3399
4034
|
onDragOver: (event) => {
|
|
@@ -3441,7 +4076,7 @@ function MejiroManuscriptEditor({
|
|
|
3441
4076
|
onChange: (event) => patchChapter(selected, { title: event.target.value })
|
|
3442
4077
|
}
|
|
3443
4078
|
),
|
|
3444
|
-
/* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
|
|
4079
|
+
dialect === "mejiro" && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
|
|
3445
4080
|
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
|
|
3446
4081
|
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
|
|
3447
4082
|
/* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
|
|
@@ -3501,7 +4136,7 @@ function MejiroShelf({
|
|
|
3501
4136
|
}
|
|
3502
4137
|
|
|
3503
4138
|
// src/MejiroToc.tsx
|
|
3504
|
-
import { useMemo as
|
|
4139
|
+
import { useMemo as useMemo10, useState as useState14 } from "react";
|
|
3505
4140
|
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3506
4141
|
function buildEntries(epub, messages) {
|
|
3507
4142
|
return epub.chapters.map((ch, i) => {
|
|
@@ -3524,9 +4159,9 @@ function MejiroToc({
|
|
|
3524
4159
|
const messages = useI18n();
|
|
3525
4160
|
const resolvedTitle = title ?? messages.tocTitle;
|
|
3526
4161
|
const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
|
|
3527
|
-
const [query, setQuery] =
|
|
3528
|
-
const entries =
|
|
3529
|
-
const filtered =
|
|
4162
|
+
const [query, setQuery] = useState14("");
|
|
4163
|
+
const entries = useMemo10(() => buildEntries(epub, messages), [epub, messages]);
|
|
4164
|
+
const filtered = useMemo10(() => {
|
|
3530
4165
|
if (!query) return entries;
|
|
3531
4166
|
const needle = query.toLowerCase();
|
|
3532
4167
|
return entries.filter(
|
|
@@ -3576,34 +4211,41 @@ import {
|
|
|
3576
4211
|
serializeAnnotations,
|
|
3577
4212
|
sortAnnotations
|
|
3578
4213
|
} from "@libraz/mejiro";
|
|
3579
|
-
import { useCallback as
|
|
4214
|
+
import { useCallback as useCallback12, useEffect as useEffect14, useRef as useRef18, useState as useState15 } from "react";
|
|
3580
4215
|
function parseSorted(raw) {
|
|
3581
4216
|
return sortAnnotations(parseAnnotations(raw));
|
|
3582
4217
|
}
|
|
3583
4218
|
function useAnnotations(options) {
|
|
3584
4219
|
const { key, throttleMs = 250 } = options;
|
|
3585
4220
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
3586
|
-
const
|
|
3587
|
-
() => readStorage(storage, key, parseSorted, [])
|
|
3588
|
-
);
|
|
3589
|
-
const storageRef = useRef15(storage);
|
|
4221
|
+
const storageRef = useRef18(storage);
|
|
3590
4222
|
storageRef.current = storage;
|
|
3591
|
-
const
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const
|
|
4223
|
+
const [hydrated, setHydrated] = useState15(() => ({
|
|
4224
|
+
key,
|
|
4225
|
+
list: readStorage(storage, key, parseSorted, [])
|
|
4226
|
+
}));
|
|
4227
|
+
let annotations = hydrated.list;
|
|
4228
|
+
const annotationsRef = useRef18(annotations);
|
|
4229
|
+
if (hydrated.key !== key) {
|
|
4230
|
+
annotations = readStorage(storage, key, parseSorted, []);
|
|
4231
|
+
annotationsRef.current = annotations;
|
|
4232
|
+
setHydrated({ key, list: annotations });
|
|
4233
|
+
}
|
|
4234
|
+
const applyNext = useCallback12(
|
|
4235
|
+
(next) => {
|
|
4236
|
+
annotationsRef.current = next;
|
|
4237
|
+
setHydrated({ key, list: next });
|
|
4238
|
+
},
|
|
4239
|
+
[key]
|
|
4240
|
+
);
|
|
4241
|
+
const pendingRef = useRef18(null);
|
|
3597
4242
|
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
3598
4243
|
const pending = pendingRef.current;
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
return pending.flush;
|
|
3603
|
-
}, [key, applyNext, pending]);
|
|
3604
|
-
const onChangeRef = useRef15(options.onChange);
|
|
4244
|
+
useEffect14(() => flushOnPageHide(pending.flush), [pending]);
|
|
4245
|
+
useEffect14(() => pending.flush, [key, pending]);
|
|
4246
|
+
const onChangeRef = useRef18(options.onChange);
|
|
3605
4247
|
onChangeRef.current = options.onChange;
|
|
3606
|
-
const commit =
|
|
4248
|
+
const commit = useCallback12(
|
|
3607
4249
|
(next) => {
|
|
3608
4250
|
const currentStorage = storageRef.current;
|
|
3609
4251
|
if (currentStorage) {
|
|
@@ -3615,7 +4257,7 @@ function useAnnotations(options) {
|
|
|
3615
4257
|
},
|
|
3616
4258
|
[key, throttleMs, pending]
|
|
3617
4259
|
);
|
|
3618
|
-
const add =
|
|
4260
|
+
const add = useCallback12(
|
|
3619
4261
|
(input) => {
|
|
3620
4262
|
const annotation = {
|
|
3621
4263
|
...input,
|
|
@@ -3629,7 +4271,7 @@ function useAnnotations(options) {
|
|
|
3629
4271
|
},
|
|
3630
4272
|
[commit, applyNext]
|
|
3631
4273
|
);
|
|
3632
|
-
const remove =
|
|
4274
|
+
const remove = useCallback12(
|
|
3633
4275
|
(id) => {
|
|
3634
4276
|
const current = annotationsRef.current;
|
|
3635
4277
|
const next = current.filter((annotation) => annotation.id !== id);
|
|
@@ -3639,7 +4281,7 @@ function useAnnotations(options) {
|
|
|
3639
4281
|
},
|
|
3640
4282
|
[commit, applyNext]
|
|
3641
4283
|
);
|
|
3642
|
-
const update =
|
|
4284
|
+
const update = useCallback12(
|
|
3643
4285
|
(id, patch) => {
|
|
3644
4286
|
let changed = false;
|
|
3645
4287
|
const next = annotationsRef.current.map((annotation) => {
|
|
@@ -3654,7 +4296,8 @@ function useAnnotations(options) {
|
|
|
3654
4296
|
},
|
|
3655
4297
|
[commit, applyNext]
|
|
3656
4298
|
);
|
|
3657
|
-
const clear =
|
|
4299
|
+
const clear = useCallback12(() => {
|
|
4300
|
+
if (annotationsRef.current.length === 0) return;
|
|
3658
4301
|
applyNext([]);
|
|
3659
4302
|
pending.cancel();
|
|
3660
4303
|
removeStorage(storageRef.current, key);
|
|
@@ -3669,39 +4312,40 @@ import {
|
|
|
3669
4312
|
cloneEditableEpubBook as cloneEditableEpubBook2,
|
|
3670
4313
|
EditableEpub as EditableEpub2
|
|
3671
4314
|
} from "@libraz/mejiro/epub";
|
|
3672
|
-
import { useCallback as
|
|
4315
|
+
import { useCallback as useCallback13, useEffect as useEffect15, useMemo as useMemo11, useRef as useRef19, useState as useState16 } from "react";
|
|
3673
4316
|
function useEditableEpub(options = {}) {
|
|
3674
|
-
const [editor, setEditor] =
|
|
3675
|
-
const [loading, setLoading] =
|
|
3676
|
-
const [exporting, setExporting] =
|
|
3677
|
-
const [error, setError] =
|
|
3678
|
-
const [revision, setRevision] =
|
|
3679
|
-
const [selection, setSelectionState] =
|
|
4317
|
+
const [editor, setEditor] = useState16(null);
|
|
4318
|
+
const [loading, setLoading] = useState16(false);
|
|
4319
|
+
const [exporting, setExporting] = useState16(false);
|
|
4320
|
+
const [error, setError] = useState16(null);
|
|
4321
|
+
const [revision, setRevision] = useState16(0);
|
|
4322
|
+
const [selection, setSelectionState] = useState16({
|
|
3680
4323
|
chapter: 0,
|
|
3681
4324
|
paragraph: 0
|
|
3682
4325
|
});
|
|
3683
|
-
const
|
|
3684
|
-
const
|
|
3685
|
-
const
|
|
3686
|
-
const
|
|
3687
|
-
const
|
|
3688
|
-
const
|
|
4326
|
+
const request = useRequestToken();
|
|
4327
|
+
const exportsInFlightRef = useRef19(0);
|
|
4328
|
+
const onLoadRef = useRef19(options.onLoad);
|
|
4329
|
+
const onErrorRef = useRef19(options.onError);
|
|
4330
|
+
const onExportRef = useRef19(options.onExport);
|
|
4331
|
+
const limitsRef = useRef19(options.limits);
|
|
4332
|
+
const assetResolverRef = useRef19(options.assetResolver);
|
|
3689
4333
|
onLoadRef.current = options.onLoad;
|
|
3690
4334
|
onErrorRef.current = options.onError;
|
|
3691
4335
|
onExportRef.current = options.onExport;
|
|
3692
4336
|
limitsRef.current = options.limits;
|
|
3693
4337
|
assetResolverRef.current = options.assetResolver;
|
|
3694
|
-
const editorRef =
|
|
4338
|
+
const editorRef = useRef19(editor);
|
|
3695
4339
|
editorRef.current = editor;
|
|
3696
|
-
const selectionRef =
|
|
4340
|
+
const selectionRef = useRef19(selection);
|
|
3697
4341
|
selectionRef.current = selection;
|
|
3698
|
-
const loadBufferWithRequest =
|
|
3699
|
-
async (buffer,
|
|
4342
|
+
const loadBufferWithRequest = useCallback13(
|
|
4343
|
+
async (buffer, currentRequest) => {
|
|
3700
4344
|
setLoading(true);
|
|
3701
4345
|
setError(null);
|
|
3702
4346
|
try {
|
|
3703
4347
|
const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
|
|
3704
|
-
if (
|
|
4348
|
+
if (!request.isCurrent(currentRequest)) return null;
|
|
3705
4349
|
const firstParagraph = { chapter: 0, paragraph: 0 };
|
|
3706
4350
|
editorRef.current = next;
|
|
3707
4351
|
selectionRef.current = firstParagraph;
|
|
@@ -3711,34 +4355,37 @@ function useEditableEpub(options = {}) {
|
|
|
3711
4355
|
onLoadRef.current?.(next);
|
|
3712
4356
|
return next;
|
|
3713
4357
|
} catch (err) {
|
|
3714
|
-
if (
|
|
4358
|
+
if (request.isCurrent(currentRequest)) {
|
|
3715
4359
|
const nextError = toError(err);
|
|
3716
4360
|
setError(nextError);
|
|
3717
4361
|
onErrorRef.current?.(nextError);
|
|
3718
4362
|
}
|
|
3719
4363
|
return null;
|
|
3720
4364
|
} finally {
|
|
3721
|
-
if (
|
|
4365
|
+
if (request.isCurrent(currentRequest)) setLoading(false);
|
|
3722
4366
|
}
|
|
3723
4367
|
},
|
|
3724
|
-
[]
|
|
4368
|
+
[request]
|
|
3725
4369
|
);
|
|
3726
|
-
const loadBuffer =
|
|
4370
|
+
const loadBuffer = useCallback13(
|
|
3727
4371
|
async (buffer) => {
|
|
3728
|
-
const
|
|
3729
|
-
return loadBufferWithRequest(buffer,
|
|
4372
|
+
const currentRequest = request.begin();
|
|
4373
|
+
return loadBufferWithRequest(buffer, currentRequest);
|
|
3730
4374
|
},
|
|
3731
|
-
[loadBufferWithRequest]
|
|
4375
|
+
[loadBufferWithRequest, request]
|
|
3732
4376
|
);
|
|
3733
|
-
const loadFile =
|
|
4377
|
+
const loadFile = useCallback13(
|
|
3734
4378
|
async (file) => {
|
|
3735
|
-
const
|
|
4379
|
+
const currentRequest = request.begin();
|
|
3736
4380
|
setLoading(true);
|
|
3737
4381
|
setError(null);
|
|
3738
4382
|
try {
|
|
3739
|
-
return await loadBufferWithRequest(
|
|
4383
|
+
return await loadBufferWithRequest(
|
|
4384
|
+
await readEpubFile(file, limitsRef.current),
|
|
4385
|
+
currentRequest
|
|
4386
|
+
);
|
|
3740
4387
|
} catch (err) {
|
|
3741
|
-
if (
|
|
4388
|
+
if (request.isCurrent(currentRequest)) {
|
|
3742
4389
|
const nextError = toError(err);
|
|
3743
4390
|
setError(nextError);
|
|
3744
4391
|
onErrorRef.current?.(nextError);
|
|
@@ -3747,17 +4394,17 @@ function useEditableEpub(options = {}) {
|
|
|
3747
4394
|
return null;
|
|
3748
4395
|
}
|
|
3749
4396
|
},
|
|
3750
|
-
[loadBufferWithRequest]
|
|
4397
|
+
[loadBufferWithRequest, request]
|
|
3751
4398
|
);
|
|
3752
|
-
const loadUrl =
|
|
4399
|
+
const loadUrl = useCallback13(
|
|
3753
4400
|
async (url) => {
|
|
3754
|
-
const
|
|
4401
|
+
const currentRequest = request.begin();
|
|
3755
4402
|
setLoading(true);
|
|
3756
4403
|
setError(null);
|
|
3757
4404
|
try {
|
|
3758
|
-
return await loadBufferWithRequest(await fetchEpubBuffer(url),
|
|
4405
|
+
return await loadBufferWithRequest(await fetchEpubBuffer(url), currentRequest);
|
|
3759
4406
|
} catch (err) {
|
|
3760
|
-
if (
|
|
4407
|
+
if (request.isCurrent(currentRequest)) {
|
|
3761
4408
|
const nextError = toError(err);
|
|
3762
4409
|
setError(nextError);
|
|
3763
4410
|
onErrorRef.current?.(nextError);
|
|
@@ -3766,24 +4413,32 @@ function useEditableEpub(options = {}) {
|
|
|
3766
4413
|
return null;
|
|
3767
4414
|
}
|
|
3768
4415
|
},
|
|
3769
|
-
[loadBufferWithRequest]
|
|
4416
|
+
[loadBufferWithRequest, request]
|
|
3770
4417
|
);
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
4418
|
+
const defaultUrl = options.defaultUrl;
|
|
4419
|
+
useEffect15(() => {
|
|
4420
|
+
if (!defaultUrl) return;
|
|
4421
|
+
void loadUrl(defaultUrl);
|
|
4422
|
+
const urlRequest = request.current();
|
|
4423
|
+
return () => {
|
|
4424
|
+
if (!request.isCurrent(urlRequest)) return;
|
|
4425
|
+
request.invalidate();
|
|
4426
|
+
setLoading(false);
|
|
4427
|
+
};
|
|
4428
|
+
}, [defaultUrl, loadUrl, request]);
|
|
3774
4429
|
const book = editor?.book ?? null;
|
|
3775
4430
|
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3776
|
-
const previewBook =
|
|
4431
|
+
const previewBook = useMemo11(() => {
|
|
3777
4432
|
void revision;
|
|
3778
|
-
return book ? cloneEditableEpubBook2(book) : null;
|
|
3779
|
-
}, [book, revision]);
|
|
4433
|
+
return book && editor ? markBookIdentity(cloneEditableEpubBook2(book), editor) : null;
|
|
4434
|
+
}, [book, editor, revision]);
|
|
3780
4435
|
const history = editor?.history ?? null;
|
|
3781
|
-
const setSelection =
|
|
4436
|
+
const setSelection = useCallback13((nextSelection) => {
|
|
3782
4437
|
const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
|
|
3783
4438
|
selectionRef.current = next;
|
|
3784
4439
|
setSelectionState(next);
|
|
3785
4440
|
}, []);
|
|
3786
|
-
const updateParagraph =
|
|
4441
|
+
const updateParagraph = useCallback13(
|
|
3787
4442
|
(text, inlineAnnotations) => {
|
|
3788
4443
|
const current = editorRef.current;
|
|
3789
4444
|
if (!current) return;
|
|
@@ -3793,37 +4448,38 @@ function useEditableEpub(options = {}) {
|
|
|
3793
4448
|
},
|
|
3794
4449
|
[]
|
|
3795
4450
|
);
|
|
3796
|
-
const setInlineAnnotations =
|
|
4451
|
+
const setInlineAnnotations = useCallback13((inlineAnnotations) => {
|
|
3797
4452
|
const current = editorRef.current;
|
|
3798
4453
|
if (!current) return;
|
|
3799
4454
|
const { chapter, paragraph } = selectionRef.current;
|
|
3800
4455
|
current.setInlineAnnotations(chapter, paragraph, inlineAnnotations);
|
|
3801
4456
|
setRevision((value) => value + 1);
|
|
3802
4457
|
}, []);
|
|
3803
|
-
const addImage =
|
|
4458
|
+
const addImage = useCallback13((image) => {
|
|
3804
4459
|
const current = editorRef.current;
|
|
3805
4460
|
if (!current) return;
|
|
3806
4461
|
current.addImage(selectionRef.current.chapter, image);
|
|
3807
4462
|
setRevision((value) => value + 1);
|
|
3808
4463
|
}, []);
|
|
3809
|
-
const undo =
|
|
4464
|
+
const undo = useCallback13(() => {
|
|
3810
4465
|
const current = editorRef.current;
|
|
3811
4466
|
if (!current) return false;
|
|
3812
4467
|
const changed = current.undo();
|
|
3813
4468
|
if (changed) setRevision((value) => value + 1);
|
|
3814
4469
|
return changed;
|
|
3815
4470
|
}, []);
|
|
3816
|
-
const redo =
|
|
4471
|
+
const redo = useCallback13(() => {
|
|
3817
4472
|
const current = editorRef.current;
|
|
3818
4473
|
if (!current) return false;
|
|
3819
4474
|
const changed = current.redo();
|
|
3820
4475
|
if (changed) setRevision((value) => value + 1);
|
|
3821
4476
|
return changed;
|
|
3822
4477
|
}, []);
|
|
3823
|
-
const exportEpub =
|
|
4478
|
+
const exportEpub = useCallback13(
|
|
3824
4479
|
async (exportOptions) => {
|
|
3825
4480
|
const current = editorRef.current;
|
|
3826
4481
|
if (!current) return null;
|
|
4482
|
+
exportsInFlightRef.current++;
|
|
3827
4483
|
setExporting(true);
|
|
3828
4484
|
try {
|
|
3829
4485
|
const buffer = await current.export(
|
|
@@ -3832,7 +4488,7 @@ function useEditableEpub(options = {}) {
|
|
|
3832
4488
|
onExportRef.current?.(buffer);
|
|
3833
4489
|
return buffer;
|
|
3834
4490
|
} finally {
|
|
3835
|
-
setExporting(
|
|
4491
|
+
setExporting(--exportsInFlightRef.current > 0);
|
|
3836
4492
|
}
|
|
3837
4493
|
},
|
|
3838
4494
|
[]
|
|
@@ -3865,258 +4521,12 @@ function withAssetResolver(options, resolver) {
|
|
|
3865
4521
|
return { ...options, assetResolver: resolver };
|
|
3866
4522
|
}
|
|
3867
4523
|
|
|
3868
|
-
// src/useEpubProject.ts
|
|
3869
|
-
import {
|
|
3870
|
-
EpubProject as EpubProject2,
|
|
3871
|
-
parseEpub as parseEpub2
|
|
3872
|
-
} from "@libraz/mejiro/epub";
|
|
3873
|
-
import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo10, useRef as useRef17, useState as useState16 } from "react";
|
|
3874
|
-
function useEpubProject(options = {}) {
|
|
3875
|
-
const defaultTitle = options.defaultChapterTitle;
|
|
3876
|
-
const defaultBody = options.defaultChapterBody;
|
|
3877
|
-
const [metadata, setMetadataState] = useState16(
|
|
3878
|
-
() => initialMetadata(options.metadata)
|
|
3879
|
-
);
|
|
3880
|
-
const [chapters, setChaptersState] = useState16(
|
|
3881
|
-
() => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
|
|
3882
|
-
);
|
|
3883
|
-
const [selectedChapter, setSelectedChapterState] = useState16(0);
|
|
3884
|
-
const [cover, setCoverState] = useState16(options.cover ?? null);
|
|
3885
|
-
const [assets, setAssetsState] = useState16(options.assets ?? []);
|
|
3886
|
-
const [previewBook, setPreviewBook] = useState16(null);
|
|
3887
|
-
const [previewError, setPreviewError] = useState16(null);
|
|
3888
|
-
const [previewing, setPreviewing] = useState16(false);
|
|
3889
|
-
const previewRequestIdRef = useRef17(0);
|
|
3890
|
-
const onPreviewRef = useRef17(options.onPreview);
|
|
3891
|
-
const onExportRef = useRef17(options.onExport);
|
|
3892
|
-
onPreviewRef.current = options.onPreview;
|
|
3893
|
-
onExportRef.current = options.onExport;
|
|
3894
|
-
const assetResolverRef = useRef17(options.assetResolver);
|
|
3895
|
-
assetResolverRef.current = options.assetResolver;
|
|
3896
|
-
const metadataRef = useRef17(metadata);
|
|
3897
|
-
metadataRef.current = metadata;
|
|
3898
|
-
const chaptersRef = useRef17(chapters);
|
|
3899
|
-
chaptersRef.current = chapters;
|
|
3900
|
-
const selectedRef = useRef17(selectedChapter);
|
|
3901
|
-
selectedRef.current = selectedChapter;
|
|
3902
|
-
const coverRef = useRef17(cover);
|
|
3903
|
-
coverRef.current = cover;
|
|
3904
|
-
const assetsRef = useRef17(assets);
|
|
3905
|
-
assetsRef.current = assets;
|
|
3906
|
-
const buildProject = useCallback12(() => {
|
|
3907
|
-
const currentCover = coverRef.current;
|
|
3908
|
-
const project = EpubProject2.fromManuscript({
|
|
3909
|
-
metadata: metadataRef.current,
|
|
3910
|
-
includeTitlePage: false,
|
|
3911
|
-
includeTitleInFirstChapter: true,
|
|
3912
|
-
chapters: chaptersRef.current.map((chapter) => ({
|
|
3913
|
-
id: chapter.id,
|
|
3914
|
-
title: chapter.title || "Untitled",
|
|
3915
|
-
body: chapter.body
|
|
3916
|
-
})),
|
|
3917
|
-
...currentCover ? { cover: currentCover } : {}
|
|
3918
|
-
});
|
|
3919
|
-
for (const asset of assetsRef.current) project.addAsset(asset);
|
|
3920
|
-
return project;
|
|
3921
|
-
}, []);
|
|
3922
|
-
const exportOptions = useCallback12((signal) => {
|
|
3923
|
-
const resolver = assetResolverRef.current;
|
|
3924
|
-
return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
|
|
3925
|
-
}, []);
|
|
3926
|
-
useEffect14(() => {
|
|
3927
|
-
const requestId = ++previewRequestIdRef.current;
|
|
3928
|
-
const controller = new AbortController();
|
|
3929
|
-
let cancelled = false;
|
|
3930
|
-
setPreviewing(true);
|
|
3931
|
-
const timer = setTimeout(() => {
|
|
3932
|
-
void (async () => {
|
|
3933
|
-
try {
|
|
3934
|
-
const book = await parseEpub2(
|
|
3935
|
-
await buildProject().export(exportOptions(controller.signal))
|
|
3936
|
-
);
|
|
3937
|
-
if (cancelled || requestId !== previewRequestIdRef.current) return;
|
|
3938
|
-
setPreviewBook(book);
|
|
3939
|
-
setPreviewError(null);
|
|
3940
|
-
onPreviewRef.current?.(book);
|
|
3941
|
-
} catch (err) {
|
|
3942
|
-
if (!cancelled && requestId === previewRequestIdRef.current) {
|
|
3943
|
-
setPreviewError(toError(err));
|
|
3944
|
-
}
|
|
3945
|
-
} finally {
|
|
3946
|
-
if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
|
|
3947
|
-
}
|
|
3948
|
-
})();
|
|
3949
|
-
}, options.debounceMs ?? 250);
|
|
3950
|
-
return () => {
|
|
3951
|
-
cancelled = true;
|
|
3952
|
-
previewRequestIdRef.current++;
|
|
3953
|
-
clearTimeout(timer);
|
|
3954
|
-
controller.abort();
|
|
3955
|
-
};
|
|
3956
|
-
}, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
|
|
3957
|
-
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
3958
|
-
const setMetadata = useCallback12((patch) => {
|
|
3959
|
-
const next = { ...metadataRef.current, ...patch };
|
|
3960
|
-
metadataRef.current = next;
|
|
3961
|
-
setMetadataState(next);
|
|
3962
|
-
}, []);
|
|
3963
|
-
const commit = useCallback12((nextChapters, nextSelected) => {
|
|
3964
|
-
chaptersRef.current = nextChapters;
|
|
3965
|
-
selectedRef.current = nextSelected;
|
|
3966
|
-
setChaptersState(nextChapters);
|
|
3967
|
-
setSelectedChapterState(nextSelected);
|
|
3968
|
-
}, []);
|
|
3969
|
-
const setChapters = useCallback12(
|
|
3970
|
-
(next) => {
|
|
3971
|
-
const normalized = next.length ? next : [defaultChapter3([], defaultTitle, defaultBody)];
|
|
3972
|
-
const prev = selectedRef.current;
|
|
3973
|
-
const selectedId = chaptersRef.current[prev]?.id;
|
|
3974
|
-
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
3975
|
-
commit(
|
|
3976
|
-
normalized,
|
|
3977
|
-
nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
|
|
3978
|
-
);
|
|
3979
|
-
},
|
|
3980
|
-
[commit, defaultBody, defaultTitle]
|
|
3981
|
-
);
|
|
3982
|
-
const setCover = useCallback12((asset) => {
|
|
3983
|
-
coverRef.current = asset;
|
|
3984
|
-
setCoverState(asset);
|
|
3985
|
-
}, []);
|
|
3986
|
-
const setAssets = useCallback12((next) => {
|
|
3987
|
-
assetsRef.current = next;
|
|
3988
|
-
setAssetsState(next);
|
|
3989
|
-
}, []);
|
|
3990
|
-
const setSelectedChapter = useCallback12((index) => {
|
|
3991
|
-
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
3992
|
-
selectedRef.current = next;
|
|
3993
|
-
setSelectedChapterState(next);
|
|
3994
|
-
}, []);
|
|
3995
|
-
const patchChapter = useCallback12((index, patch) => {
|
|
3996
|
-
const next = chaptersRef.current.map(
|
|
3997
|
-
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3998
|
-
);
|
|
3999
|
-
chaptersRef.current = next;
|
|
4000
|
-
setChaptersState(next);
|
|
4001
|
-
}, []);
|
|
4002
|
-
const addChapter = useCallback12(
|
|
4003
|
-
(chapter = {}) => {
|
|
4004
|
-
const current = chaptersRef.current;
|
|
4005
|
-
const generated = defaultChapter3(current, defaultTitle, defaultBody);
|
|
4006
|
-
const next = [
|
|
4007
|
-
...current,
|
|
4008
|
-
{
|
|
4009
|
-
id: chapter.id ?? generated.id,
|
|
4010
|
-
title: chapter.title ?? generated.title,
|
|
4011
|
-
body: chapter.body ?? generated.body
|
|
4012
|
-
}
|
|
4013
|
-
];
|
|
4014
|
-
commit(next, next.length - 1);
|
|
4015
|
-
},
|
|
4016
|
-
[commit, defaultBody, defaultTitle]
|
|
4017
|
-
);
|
|
4018
|
-
const removeChapter = useCallback12(
|
|
4019
|
-
(index = selectedRef.current) => {
|
|
4020
|
-
const current = chaptersRef.current;
|
|
4021
|
-
if (current.length <= 1) return;
|
|
4022
|
-
const next = current.filter((_, chapterIndex) => chapterIndex !== index);
|
|
4023
|
-
const prev = selectedRef.current;
|
|
4024
|
-
let nextSelected;
|
|
4025
|
-
if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
|
|
4026
|
-
else if (index < prev) nextSelected = prev - 1;
|
|
4027
|
-
else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
|
|
4028
|
-
commit(next, nextSelected);
|
|
4029
|
-
},
|
|
4030
|
-
[commit]
|
|
4031
|
-
);
|
|
4032
|
-
const reorderChapters = useCallback12(
|
|
4033
|
-
(from, to) => {
|
|
4034
|
-
const current = chaptersRef.current;
|
|
4035
|
-
if (from < 0 || from >= current.length) return;
|
|
4036
|
-
const next = [...current];
|
|
4037
|
-
const [moved] = next.splice(from, 1);
|
|
4038
|
-
const target = Math.max(0, Math.min(next.length, to));
|
|
4039
|
-
next.splice(target, 0, moved);
|
|
4040
|
-
const prev = selectedRef.current;
|
|
4041
|
-
let nextSelected = prev;
|
|
4042
|
-
if (prev === from) nextSelected = target;
|
|
4043
|
-
else if (from < prev && target >= prev) nextSelected = prev - 1;
|
|
4044
|
-
else if (from > prev && target <= prev) nextSelected = prev + 1;
|
|
4045
|
-
commit(next, nextSelected);
|
|
4046
|
-
},
|
|
4047
|
-
[commit]
|
|
4048
|
-
);
|
|
4049
|
-
const exportEpub = useCallback12(async () => {
|
|
4050
|
-
const buffer = await buildProject().export(exportOptions());
|
|
4051
|
-
onExportRef.current?.(buffer);
|
|
4052
|
-
return buffer;
|
|
4053
|
-
}, [buildProject, exportOptions]);
|
|
4054
|
-
return useMemo10(
|
|
4055
|
-
() => ({
|
|
4056
|
-
metadata,
|
|
4057
|
-
chapters,
|
|
4058
|
-
selectedChapter,
|
|
4059
|
-
currentChapter,
|
|
4060
|
-
cover,
|
|
4061
|
-
assets,
|
|
4062
|
-
previewBook,
|
|
4063
|
-
previewError,
|
|
4064
|
-
previewing,
|
|
4065
|
-
setMetadata,
|
|
4066
|
-
setChapters,
|
|
4067
|
-
setSelectedChapter,
|
|
4068
|
-
setCover,
|
|
4069
|
-
setAssets,
|
|
4070
|
-
patchChapter,
|
|
4071
|
-
addChapter,
|
|
4072
|
-
removeChapter,
|
|
4073
|
-
reorderChapters,
|
|
4074
|
-
buildProject,
|
|
4075
|
-
exportEpub
|
|
4076
|
-
}),
|
|
4077
|
-
[
|
|
4078
|
-
addChapter,
|
|
4079
|
-
assets,
|
|
4080
|
-
buildProject,
|
|
4081
|
-
chapters,
|
|
4082
|
-
cover,
|
|
4083
|
-
currentChapter,
|
|
4084
|
-
exportEpub,
|
|
4085
|
-
metadata,
|
|
4086
|
-
patchChapter,
|
|
4087
|
-
previewBook,
|
|
4088
|
-
previewError,
|
|
4089
|
-
previewing,
|
|
4090
|
-
reorderChapters,
|
|
4091
|
-
removeChapter,
|
|
4092
|
-
selectedChapter,
|
|
4093
|
-
setAssets,
|
|
4094
|
-
setChapters,
|
|
4095
|
-
setCover,
|
|
4096
|
-
setMetadata,
|
|
4097
|
-
setSelectedChapter
|
|
4098
|
-
]
|
|
4099
|
-
);
|
|
4100
|
-
}
|
|
4101
|
-
function initialMetadata(patch = {}) {
|
|
4102
|
-
const metadata = mergeDefined(
|
|
4103
|
-
{ title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
|
|
4104
|
-
patch
|
|
4105
|
-
);
|
|
4106
|
-
if (!metadata.identifier?.trim()) metadata.identifier = `urn:uuid:${crypto.randomUUID()}`;
|
|
4107
|
-
return metadata;
|
|
4108
|
-
}
|
|
4109
|
-
function defaultChapter3(existing, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
|
|
4110
|
-
const index = existing.length;
|
|
4111
|
-
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
4524
|
// src/useImageOverlay.ts
|
|
4115
4525
|
import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
|
|
4116
4526
|
import {
|
|
4117
|
-
useCallback as
|
|
4118
|
-
useEffect as
|
|
4119
|
-
useRef as
|
|
4527
|
+
useCallback as useCallback14,
|
|
4528
|
+
useEffect as useEffect16,
|
|
4529
|
+
useRef as useRef20,
|
|
4120
4530
|
useState as useState17
|
|
4121
4531
|
} from "react";
|
|
4122
4532
|
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
@@ -4126,34 +4536,33 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4126
4536
|
const defY = options?.defaultY ?? 100;
|
|
4127
4537
|
const margin = options?.margin;
|
|
4128
4538
|
const [imageRect, setImageRect] = useState17(null);
|
|
4129
|
-
const rectRef =
|
|
4539
|
+
const rectRef = useRef20(imageRect);
|
|
4130
4540
|
rectRef.current = imageRect;
|
|
4131
|
-
const layoutRef =
|
|
4541
|
+
const layoutRef = useRef20(layout);
|
|
4132
4542
|
layoutRef.current = layout;
|
|
4133
|
-
const spreadRef =
|
|
4543
|
+
const spreadRef = useRef20(spreadIdx);
|
|
4134
4544
|
spreadRef.current = spreadIdx;
|
|
4135
|
-
const onUpdateRef =
|
|
4545
|
+
const onUpdateRef = useRef20(onUpdate);
|
|
4136
4546
|
onUpdateRef.current = onUpdate;
|
|
4137
|
-
const
|
|
4138
|
-
|
|
4547
|
+
const marginRef = useRef20(margin);
|
|
4548
|
+
marginRef.current = margin;
|
|
4549
|
+
const activeDragCleanupsRef = useRef20(/* @__PURE__ */ new Set());
|
|
4550
|
+
useEffect16(
|
|
4139
4551
|
() => () => {
|
|
4140
4552
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
4141
4553
|
activeDragCleanupsRef.current.clear();
|
|
4142
4554
|
},
|
|
4143
4555
|
[]
|
|
4144
4556
|
);
|
|
4145
|
-
const syncToLayout =
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
);
|
|
4155
|
-
const syncedRef = useRef18({ layout, spreadIdx, margin });
|
|
4156
|
-
useEffect15(() => {
|
|
4557
|
+
const syncToLayout = useCallback14((rect) => {
|
|
4558
|
+
const lo = layoutRef.current;
|
|
4559
|
+
if (!lo) return;
|
|
4560
|
+
const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: marginRef.current }] : void 0;
|
|
4561
|
+
const spread = lo.syncImages(spreadRef.current, images);
|
|
4562
|
+
onUpdateRef.current(spread);
|
|
4563
|
+
}, []);
|
|
4564
|
+
const syncedRef = useRef20({ layout, spreadIdx, margin });
|
|
4565
|
+
useEffect16(() => {
|
|
4157
4566
|
const prev = syncedRef.current;
|
|
4158
4567
|
syncedRef.current = { layout, spreadIdx, margin };
|
|
4159
4568
|
if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
|
|
@@ -4163,24 +4572,29 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4163
4572
|
}
|
|
4164
4573
|
syncToLayout(rectRef.current);
|
|
4165
4574
|
}, [layout, spreadIdx, margin, syncToLayout]);
|
|
4166
|
-
const toggleImage =
|
|
4575
|
+
const toggleImage = useCallback14(() => {
|
|
4167
4576
|
if (rectRef.current) {
|
|
4577
|
+
for (const cancel of [...activeDragCleanupsRef.current]) cancel();
|
|
4578
|
+
rectRef.current = null;
|
|
4168
4579
|
setImageRect(null);
|
|
4169
4580
|
syncToLayout(null);
|
|
4170
4581
|
} else {
|
|
4171
4582
|
const r = { x: defX, y: defY, w: defW, h: defH };
|
|
4583
|
+
rectRef.current = r;
|
|
4172
4584
|
setImageRect(r);
|
|
4173
4585
|
syncToLayout(r);
|
|
4174
4586
|
}
|
|
4175
4587
|
}, [defX, defY, defW, defH, syncToLayout]);
|
|
4176
|
-
const applyRect =
|
|
4588
|
+
const applyRect = useCallback14(
|
|
4177
4589
|
(rect) => {
|
|
4590
|
+
if (rectRef.current === null) return;
|
|
4591
|
+
rectRef.current = rect;
|
|
4178
4592
|
setImageRect(rect);
|
|
4179
4593
|
syncToLayout(rect);
|
|
4180
4594
|
},
|
|
4181
4595
|
[syncToLayout]
|
|
4182
4596
|
);
|
|
4183
|
-
const onOverlayPointerDown =
|
|
4597
|
+
const onOverlayPointerDown = useCallback14(
|
|
4184
4598
|
(e) => {
|
|
4185
4599
|
if (!isPrimaryPointerPress3(e)) return;
|
|
4186
4600
|
e.preventDefault();
|
|
@@ -4202,7 +4616,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4202
4616
|
},
|
|
4203
4617
|
[applyRect]
|
|
4204
4618
|
);
|
|
4205
|
-
const onResizePointerDown =
|
|
4619
|
+
const onResizePointerDown = useCallback14(
|
|
4206
4620
|
(e) => {
|
|
4207
4621
|
if (!isPrimaryPointerPress3(e)) return;
|
|
4208
4622
|
e.preventDefault();
|
|
@@ -4235,7 +4649,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4235
4649
|
}
|
|
4236
4650
|
|
|
4237
4651
|
// src/useLibrary.ts
|
|
4238
|
-
import { useCallback as
|
|
4652
|
+
import { useCallback as useCallback15, useEffect as useEffect17, useMemo as useMemo12, useState as useState18 } from "react";
|
|
4239
4653
|
function resolveCurrentId(volumes, currentId, preferredId) {
|
|
4240
4654
|
if (volumes.length === 0) return null;
|
|
4241
4655
|
if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
|
|
@@ -4249,21 +4663,21 @@ function useLibrary(options) {
|
|
|
4249
4663
|
);
|
|
4250
4664
|
const [navigated, setNavigated] = useState18(false);
|
|
4251
4665
|
const resolvedId = resolveCurrentId(volumes, currentId, navigated ? void 0 : initialVolumeId);
|
|
4252
|
-
|
|
4666
|
+
useEffect17(() => {
|
|
4253
4667
|
if (resolvedId === currentId) return;
|
|
4254
4668
|
setCurrentId(resolvedId);
|
|
4255
4669
|
const volume = volumes.find((v) => v.id === resolvedId);
|
|
4256
4670
|
if (volume) onChange?.(volume);
|
|
4257
4671
|
}, [currentId, resolvedId, volumes, onChange]);
|
|
4258
|
-
const index =
|
|
4672
|
+
const index = useMemo12(
|
|
4259
4673
|
() => resolvedId == null ? -1 : volumes.findIndex((v) => v.id === resolvedId),
|
|
4260
4674
|
[resolvedId, volumes]
|
|
4261
4675
|
);
|
|
4262
|
-
const current =
|
|
4676
|
+
const current = useMemo12(
|
|
4263
4677
|
() => index >= 0 && index < volumes.length ? volumes[index] : null,
|
|
4264
4678
|
[index, volumes]
|
|
4265
4679
|
);
|
|
4266
|
-
const activate =
|
|
4680
|
+
const activate = useCallback15(
|
|
4267
4681
|
(i) => {
|
|
4268
4682
|
const v = volumes[i];
|
|
4269
4683
|
if (!v) return;
|
|
@@ -4273,17 +4687,17 @@ function useLibrary(options) {
|
|
|
4273
4687
|
},
|
|
4274
4688
|
[volumes, onChange]
|
|
4275
4689
|
);
|
|
4276
|
-
const next =
|
|
4690
|
+
const next = useCallback15(() => {
|
|
4277
4691
|
if (index < 0) return;
|
|
4278
4692
|
const ni = Math.min(volumes.length - 1, index + 1);
|
|
4279
4693
|
if (ni !== index) activate(ni);
|
|
4280
4694
|
}, [index, activate, volumes.length]);
|
|
4281
|
-
const prev =
|
|
4695
|
+
const prev = useCallback15(() => {
|
|
4282
4696
|
if (index < 0) return;
|
|
4283
4697
|
const ni = Math.max(0, index - 1);
|
|
4284
4698
|
if (ni !== index) activate(ni);
|
|
4285
4699
|
}, [index, activate]);
|
|
4286
|
-
const goTo =
|
|
4700
|
+
const goTo = useCallback15(
|
|
4287
4701
|
(id) => {
|
|
4288
4702
|
const ni = volumes.findIndex((v) => v.id === id);
|
|
4289
4703
|
if (ni < 0) return;
|
|
@@ -4295,122 +4709,32 @@ function useLibrary(options) {
|
|
|
4295
4709
|
}
|
|
4296
4710
|
|
|
4297
4711
|
// src/useManuscriptLayout.ts
|
|
4298
|
-
import {
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
useRef as useRef19,
|
|
4303
|
-
useState as useState19
|
|
4304
|
-
} from "react";
|
|
4712
|
+
import { useMemo as useMemo13, useRef as useRef21 } from "react";
|
|
4713
|
+
function chapterSignature(chapter) {
|
|
4714
|
+
return chapter ? JSON.stringify([chapter.id, chapter.title, chapter.body]) : null;
|
|
4715
|
+
}
|
|
4305
4716
|
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
4306
4717
|
const dialect = options.dialect ?? "mejiro";
|
|
4307
|
-
const
|
|
4308
|
-
const
|
|
4309
|
-
|
|
4310
|
-
const
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
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;
|
|
4718
|
+
const signature = chapterSignature(chapter);
|
|
4719
|
+
const chapterRef = useRef21(chapter);
|
|
4720
|
+
chapterRef.current = chapter;
|
|
4721
|
+
const source = useMemo13(() => {
|
|
4722
|
+
if (signature === null) return null;
|
|
4723
|
+
return {
|
|
4724
|
+
measure: (el) => book.computePageSize(el),
|
|
4725
|
+
layout: async () => {
|
|
4726
|
+
const current = chapterRef.current;
|
|
4727
|
+
if (!current) return null;
|
|
4728
|
+
const layouts = await book.layoutManuscript({ chapters: [current], dialect });
|
|
4729
|
+
return layouts.values().next().value ?? null;
|
|
4381
4730
|
}
|
|
4382
4731
|
};
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
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
|
-
};
|
|
4412
|
-
}, [surface, enableResize, resizeDebounce, recompute]);
|
|
4413
|
-
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
|
|
4732
|
+
}, [book, signature, dialect]);
|
|
4733
|
+
return useSurfaceLayout(source, surface, {
|
|
4734
|
+
...options,
|
|
4735
|
+
enableResize: options.enableResize ?? true,
|
|
4736
|
+
resizeDebounce: options.resizeDebounce ?? 120
|
|
4737
|
+
});
|
|
4414
4738
|
}
|
|
4415
4739
|
|
|
4416
4740
|
// src/useReadingPosition.ts
|
|
@@ -4418,28 +4742,30 @@ import {
|
|
|
4418
4742
|
parseReadingPosition,
|
|
4419
4743
|
serializeReadingPosition
|
|
4420
4744
|
} from "@libraz/mejiro";
|
|
4421
|
-
import { useCallback as useCallback16, useEffect as useEffect18, useRef as
|
|
4745
|
+
import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef22, useState as useState19 } from "react";
|
|
4422
4746
|
function useReadingPosition(options) {
|
|
4423
4747
|
const { key, throttleMs = 250 } = options;
|
|
4424
4748
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
4425
|
-
const
|
|
4426
|
-
() => readStorage(storage, key, parseReadingPosition, null)
|
|
4427
|
-
);
|
|
4428
|
-
const storageRef = useRef20(storage);
|
|
4749
|
+
const storageRef = useRef22(storage);
|
|
4429
4750
|
storageRef.current = storage;
|
|
4430
|
-
const
|
|
4751
|
+
const [hydrated, setHydrated] = useState19(
|
|
4752
|
+
() => ({ key, value: readStorage(storage, key, parseReadingPosition, null) })
|
|
4753
|
+
);
|
|
4754
|
+
let position = hydrated.value;
|
|
4755
|
+
if (hydrated.key !== key) {
|
|
4756
|
+
position = readStorage(storage, key, parseReadingPosition, null);
|
|
4757
|
+
setHydrated({ key, value: position });
|
|
4758
|
+
}
|
|
4759
|
+
const pendingRef = useRef22(null);
|
|
4431
4760
|
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
4432
4761
|
const pending = pendingRef.current;
|
|
4433
4762
|
useEffect18(() => flushOnPageHide(pending.flush), [pending]);
|
|
4434
|
-
useEffect18(() =>
|
|
4435
|
-
|
|
4436
|
-
return pending.flush;
|
|
4437
|
-
}, [key, pending]);
|
|
4438
|
-
const onChangeRef = useRef20(options.onChange);
|
|
4763
|
+
useEffect18(() => pending.flush, [key, pending]);
|
|
4764
|
+
const onChangeRef = useRef22(options.onChange);
|
|
4439
4765
|
onChangeRef.current = options.onChange;
|
|
4440
4766
|
const save = useCallback16(
|
|
4441
4767
|
(next) => {
|
|
4442
|
-
|
|
4768
|
+
setHydrated({ key, value: next });
|
|
4443
4769
|
const currentStorage = storageRef.current;
|
|
4444
4770
|
if (currentStorage) {
|
|
4445
4771
|
pending.schedule(() => {
|
|
@@ -4451,7 +4777,7 @@ function useReadingPosition(options) {
|
|
|
4451
4777
|
[key, throttleMs, pending]
|
|
4452
4778
|
);
|
|
4453
4779
|
const clear = useCallback16(() => {
|
|
4454
|
-
|
|
4780
|
+
setHydrated({ key, value: null });
|
|
4455
4781
|
pending.cancel();
|
|
4456
4782
|
removeStorage(storageRef.current, key);
|
|
4457
4783
|
onChangeRef.current?.(null);
|