@libraz/mejiro-react 0.9.2 → 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/dist/index.d.ts +39 -22
- package/dist/index.js +1032 -808
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -176,7 +176,29 @@ import {
|
|
|
176
176
|
EditableEpub,
|
|
177
177
|
exportEditableEpub
|
|
178
178
|
} from "@libraz/mejiro/epub";
|
|
179
|
-
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
|
+
}
|
|
180
202
|
|
|
181
203
|
// src/errors.ts
|
|
182
204
|
import { assertEpubInputSize } from "@libraz/mejiro/epub";
|
|
@@ -205,14 +227,15 @@ async function withErrorReporting(task, report) {
|
|
|
205
227
|
import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
|
|
206
228
|
import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
|
|
207
229
|
import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
|
|
230
|
+
import { pageNumberFor as pageNumberFor2 } from "@libraz/mejiro/render";
|
|
208
231
|
import {
|
|
209
232
|
forwardRef,
|
|
210
|
-
useCallback as
|
|
211
|
-
useEffect as
|
|
233
|
+
useCallback as useCallback7,
|
|
234
|
+
useEffect as useEffect9,
|
|
212
235
|
useImperativeHandle,
|
|
213
236
|
useLayoutEffect as useLayoutEffect4,
|
|
214
237
|
useMemo as useMemo5,
|
|
215
|
-
useRef as
|
|
238
|
+
useRef as useRef12,
|
|
216
239
|
useState as useState8
|
|
217
240
|
} from "react";
|
|
218
241
|
|
|
@@ -228,7 +251,10 @@ function MejiroPageIndicator({ current, total }) {
|
|
|
228
251
|
|
|
229
252
|
// src/MejiroScrollView.tsx
|
|
230
253
|
import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
|
|
254
|
+
import { pageNumberFor } from "@libraz/mejiro/render";
|
|
231
255
|
import {
|
|
256
|
+
memo,
|
|
257
|
+
useCallback as useCallback2,
|
|
232
258
|
useEffect as useEffect2,
|
|
233
259
|
useLayoutEffect,
|
|
234
260
|
useMemo as useMemo3,
|
|
@@ -648,6 +674,71 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
|
648
674
|
// src/MejiroScrollView.tsx
|
|
649
675
|
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
650
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
|
+
});
|
|
651
742
|
function MejiroScrollView({
|
|
652
743
|
layout,
|
|
653
744
|
pageWidth,
|
|
@@ -663,6 +754,7 @@ function MejiroScrollView({
|
|
|
663
754
|
selectionRects,
|
|
664
755
|
spreadIdx,
|
|
665
756
|
images = NO_IMAGES,
|
|
757
|
+
pageNumbers = "both",
|
|
666
758
|
onImagePointerDown,
|
|
667
759
|
onImageResizePointerDown,
|
|
668
760
|
onImageClose
|
|
@@ -697,10 +789,30 @@ function MejiroScrollView({
|
|
|
697
789
|
}
|
|
698
790
|
return byPage;
|
|
699
791
|
}, [selectionRects]);
|
|
700
|
-
const contentStyle =
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
+
);
|
|
704
816
|
useEffect2(() => {
|
|
705
817
|
const container = containerRef.current;
|
|
706
818
|
if (!container) return;
|
|
@@ -758,67 +870,26 @@ function MejiroScrollView({
|
|
|
758
870
|
{
|
|
759
871
|
className: "mejiro-reader-scroll-track",
|
|
760
872
|
style: { display: "flex", flexDirection: "column", gap: pageGap },
|
|
761
|
-
children: pages.map((result, i) => /* @__PURE__ */
|
|
762
|
-
|
|
873
|
+
children: pages.map((result, i) => /* @__PURE__ */ jsx9(
|
|
874
|
+
ScrollPage,
|
|
763
875
|
{
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
/* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
|
|
782
|
-
/* @__PURE__ */ jsx9(
|
|
783
|
-
MejiroPageView,
|
|
784
|
-
{
|
|
785
|
-
result,
|
|
786
|
-
slotMode,
|
|
787
|
-
fontFamily,
|
|
788
|
-
lineSpacing,
|
|
789
|
-
className: "mejiro-reader-page-content",
|
|
790
|
-
style: contentStyle
|
|
791
|
-
}
|
|
792
|
-
),
|
|
793
|
-
rectsByPage[i] && /* @__PURE__ */ jsx9(
|
|
794
|
-
MejiroSelectionLayer,
|
|
795
|
-
{
|
|
796
|
-
rects: rectsByPage[i],
|
|
797
|
-
side: i % 2 === 0 ? "right" : "left"
|
|
798
|
-
}
|
|
799
|
-
)
|
|
800
|
-
] }) }),
|
|
801
|
-
i === rightPage && rightImages.map((item) => /* @__PURE__ */ jsx9(
|
|
802
|
-
MejiroImageOverlay,
|
|
803
|
-
{
|
|
804
|
-
rect: item.rect,
|
|
805
|
-
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
806
|
-
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
807
|
-
onClose: () => onImageClose?.(item.id)
|
|
808
|
-
},
|
|
809
|
-
item.id
|
|
810
|
-
)),
|
|
811
|
-
i === leftPage && leftImages.map((item) => /* @__PURE__ */ jsx9(
|
|
812
|
-
MejiroImageOverlay,
|
|
813
|
-
{
|
|
814
|
-
rect: { ...item.rect, x: item.rect.x + pageWidth },
|
|
815
|
-
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
816
|
-
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
817
|
-
onClose: () => onImageClose?.(item.id)
|
|
818
|
-
},
|
|
819
|
-
item.id
|
|
820
|
-
))
|
|
821
|
-
]
|
|
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
|
|
822
893
|
},
|
|
823
894
|
i
|
|
824
895
|
))
|
|
@@ -828,7 +899,7 @@ function MejiroScrollView({
|
|
|
828
899
|
|
|
829
900
|
// src/MejiroSettingsPanel.tsx
|
|
830
901
|
import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
|
|
831
|
-
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";
|
|
832
903
|
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
833
904
|
var DEFAULT_FONTS = [
|
|
834
905
|
{ value: "serif", label: "System Serif" },
|
|
@@ -897,15 +968,24 @@ function MejiroSettingsPanel({
|
|
|
897
968
|
maxFontSize = 48
|
|
898
969
|
}) {
|
|
899
970
|
const messages = useI18n();
|
|
971
|
+
const uid = useId();
|
|
900
972
|
const patch = (next) => onChange({ ...settings, ...next });
|
|
901
973
|
const currentFont = normalizeFontFamily3(settings.fontFamily);
|
|
902
974
|
const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
|
|
903
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: [
|
|
904
976
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
|
|
905
977
|
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
|
|
906
|
-
/* @__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
|
+
) }),
|
|
907
987
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
908
|
-
/* @__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 }),
|
|
909
989
|
/* @__PURE__ */ jsx10(
|
|
910
990
|
"button",
|
|
911
991
|
{
|
|
@@ -926,7 +1006,7 @@ function MejiroSettingsPanel({
|
|
|
926
1006
|
/* @__PURE__ */ jsx10(
|
|
927
1007
|
CommittedNumberInput,
|
|
928
1008
|
{
|
|
929
|
-
id:
|
|
1009
|
+
id: `${uid}-font-size`,
|
|
930
1010
|
value: settings.fontSize,
|
|
931
1011
|
min: minFontSize,
|
|
932
1012
|
max: maxFontSize,
|
|
@@ -955,11 +1035,11 @@ function MejiroSettingsPanel({
|
|
|
955
1035
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
|
|
956
1036
|
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
|
|
957
1037
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
958
|
-
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor:
|
|
1038
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-kinsoku`, children: messages.settingsKinsoku }),
|
|
959
1039
|
/* @__PURE__ */ jsxs6(
|
|
960
1040
|
"select",
|
|
961
1041
|
{
|
|
962
|
-
id:
|
|
1042
|
+
id: `${uid}-kinsoku`,
|
|
963
1043
|
value: settings.mode ?? "strict",
|
|
964
1044
|
onChange: (e) => patch({ mode: e.target.value }),
|
|
965
1045
|
children: [
|
|
@@ -970,11 +1050,11 @@ function MejiroSettingsPanel({
|
|
|
970
1050
|
)
|
|
971
1051
|
] }),
|
|
972
1052
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
973
|
-
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor:
|
|
1053
|
+
/* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-hanging`, children: messages.settingsHanging }),
|
|
974
1054
|
/* @__PURE__ */ jsxs6(
|
|
975
1055
|
"select",
|
|
976
1056
|
{
|
|
977
|
-
id:
|
|
1057
|
+
id: `${uid}-hanging`,
|
|
978
1058
|
value: String(settings.enableHanging ?? true),
|
|
979
1059
|
onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
|
|
980
1060
|
children: [
|
|
@@ -985,11 +1065,11 @@ function MejiroSettingsPanel({
|
|
|
985
1065
|
)
|
|
986
1066
|
] }),
|
|
987
1067
|
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
|
|
988
|
-
/* @__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 }),
|
|
989
1069
|
/* @__PURE__ */ jsx10(
|
|
990
1070
|
CommittedNumberInput,
|
|
991
1071
|
{
|
|
992
|
-
id:
|
|
1072
|
+
id: `${uid}-line-spacing`,
|
|
993
1073
|
className: "mejiro-reader-control--wide",
|
|
994
1074
|
value: settings.lineSpacing ?? 1.8,
|
|
995
1075
|
min: 1,
|
|
@@ -1375,16 +1455,38 @@ function createDraftChangeTracker(chapters, key) {
|
|
|
1375
1455
|
}
|
|
1376
1456
|
|
|
1377
1457
|
// src/useChapterLayout.ts
|
|
1378
|
-
import { useMemo as useMemo4, useRef as
|
|
1458
|
+
import { useMemo as useMemo4, useRef as useRef8 } from "react";
|
|
1379
1459
|
|
|
1380
1460
|
// src/useSurfaceLayout.ts
|
|
1381
1461
|
import {
|
|
1382
|
-
useCallback as
|
|
1383
|
-
useEffect as
|
|
1462
|
+
useCallback as useCallback3,
|
|
1463
|
+
useEffect as useEffect5,
|
|
1384
1464
|
useLayoutEffect as useLayoutEffect2,
|
|
1385
|
-
useRef as
|
|
1465
|
+
useRef as useRef7,
|
|
1386
1466
|
useState as useState4
|
|
1387
1467
|
} from "react";
|
|
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
|
|
1388
1490
|
function useSurfaceLayout(source, surface, options) {
|
|
1389
1491
|
const { enableResize, resizeDebounce } = options;
|
|
1390
1492
|
const [layout, setLayout] = useState4(null);
|
|
@@ -1392,19 +1494,23 @@ function useSurfaceLayout(source, surface, options) {
|
|
|
1392
1494
|
const [pageHeight, setPageHeight] = useState4(0);
|
|
1393
1495
|
const [contentHeight, setContentHeight] = useState4(0);
|
|
1394
1496
|
const [elapsedMs, setElapsedMs] = useState4(0);
|
|
1395
|
-
const
|
|
1497
|
+
const [element, setElement] = useState4(surface.current);
|
|
1498
|
+
useLayoutEffect2(() => {
|
|
1499
|
+
if (surface.current !== element) setElement(surface.current);
|
|
1500
|
+
});
|
|
1501
|
+
const layoutRef = useRef7(null);
|
|
1396
1502
|
layoutRef.current = layout;
|
|
1397
|
-
const
|
|
1398
|
-
const pendingRestore =
|
|
1399
|
-
const measuredBoxRef =
|
|
1400
|
-
const optionsRef =
|
|
1503
|
+
const requests = useRequestToken();
|
|
1504
|
+
const pendingRestore = useRef7(null);
|
|
1505
|
+
const measuredBoxRef = useRef7(null);
|
|
1506
|
+
const optionsRef = useRef7(options);
|
|
1401
1507
|
optionsRef.current = options;
|
|
1402
|
-
const recompute =
|
|
1508
|
+
const recompute = useCallback3(
|
|
1403
1509
|
async (opts = {}) => {
|
|
1404
1510
|
const blank = opts.blank ?? true;
|
|
1405
1511
|
const o = optionsRef.current;
|
|
1406
|
-
const requestId =
|
|
1407
|
-
const el =
|
|
1512
|
+
const requestId = requests.begin();
|
|
1513
|
+
const el = element;
|
|
1408
1514
|
if (!(source && el)) {
|
|
1409
1515
|
measuredBoxRef.current = null;
|
|
1410
1516
|
setLayout(null);
|
|
@@ -1425,27 +1531,27 @@ function useSurfaceLayout(source, surface, options) {
|
|
|
1425
1531
|
setContentHeight(dims.contentHeight);
|
|
1426
1532
|
const t0 = performance.now();
|
|
1427
1533
|
const next = await source.layout();
|
|
1428
|
-
if (requestId
|
|
1534
|
+
if (!requests.isCurrent(requestId)) return;
|
|
1429
1535
|
layoutRef.current = next;
|
|
1430
1536
|
setLayout(next);
|
|
1431
1537
|
setElapsedMs(performance.now() - t0);
|
|
1432
1538
|
} catch (err) {
|
|
1433
|
-
if (requestId
|
|
1539
|
+
if (!requests.isCurrent(requestId)) return;
|
|
1434
1540
|
const handler = optionsRef.current.onError;
|
|
1435
1541
|
if (!handler) throw err;
|
|
1436
1542
|
handler(toError(err));
|
|
1437
1543
|
}
|
|
1438
1544
|
},
|
|
1439
|
-
[source,
|
|
1545
|
+
[source, element, requests]
|
|
1440
1546
|
);
|
|
1441
|
-
const recomputeRef =
|
|
1547
|
+
const recomputeRef = useRef7(recompute);
|
|
1442
1548
|
recomputeRef.current = recompute;
|
|
1443
|
-
const trigger =
|
|
1549
|
+
const trigger = useCallback3((opts) => {
|
|
1444
1550
|
recomputeRef.current(opts).catch(() => {
|
|
1445
1551
|
});
|
|
1446
1552
|
}, []);
|
|
1447
1553
|
useLayoutEffect2(() => {
|
|
1448
|
-
|
|
1554
|
+
requests.invalidate();
|
|
1449
1555
|
pendingRestore.current = null;
|
|
1450
1556
|
setLayout(null);
|
|
1451
1557
|
setPageWidth(0);
|
|
@@ -1453,13 +1559,13 @@ function useSurfaceLayout(source, surface, options) {
|
|
|
1453
1559
|
setContentHeight(0);
|
|
1454
1560
|
setElapsedMs(0);
|
|
1455
1561
|
}, [recompute]);
|
|
1456
|
-
|
|
1562
|
+
useEffect5(() => {
|
|
1457
1563
|
recompute().catch(() => {
|
|
1458
1564
|
});
|
|
1459
1565
|
return () => {
|
|
1460
|
-
|
|
1566
|
+
requests.invalidate();
|
|
1461
1567
|
};
|
|
1462
|
-
}, [recompute]);
|
|
1568
|
+
}, [recompute, requests]);
|
|
1463
1569
|
useLayoutEffect2(() => {
|
|
1464
1570
|
const anchor = pendingRestore.current;
|
|
1465
1571
|
const restore = optionsRef.current.restorePosition;
|
|
@@ -1467,9 +1573,9 @@ function useSurfaceLayout(source, surface, options) {
|
|
|
1467
1573
|
pendingRestore.current = null;
|
|
1468
1574
|
restore(layout, anchor);
|
|
1469
1575
|
}, [layout]);
|
|
1470
|
-
|
|
1576
|
+
useEffect5(() => {
|
|
1471
1577
|
if (!enableResize) return;
|
|
1472
|
-
const el =
|
|
1578
|
+
const el = element;
|
|
1473
1579
|
if (!el) return;
|
|
1474
1580
|
let timer = null;
|
|
1475
1581
|
let observed = false;
|
|
@@ -1513,13 +1619,13 @@ function useSurfaceLayout(source, surface, options) {
|
|
|
1513
1619
|
window.removeEventListener("resize", onWindowResize);
|
|
1514
1620
|
clear();
|
|
1515
1621
|
};
|
|
1516
|
-
}, [
|
|
1622
|
+
}, [element, enableResize, resizeDebounce, trigger]);
|
|
1517
1623
|
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
|
|
1518
1624
|
}
|
|
1519
1625
|
|
|
1520
1626
|
// src/useChapterLayout.ts
|
|
1521
1627
|
function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
1522
|
-
const optionsRef =
|
|
1628
|
+
const optionsRef = useRef8(options);
|
|
1523
1629
|
optionsRef.current = options;
|
|
1524
1630
|
const source = useMemo4(() => {
|
|
1525
1631
|
const chapter = epub?.chapters[chapterIndex];
|
|
@@ -1538,92 +1644,92 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
|
1538
1644
|
|
|
1539
1645
|
// src/useEpub.ts
|
|
1540
1646
|
import { parseEpub } from "@libraz/mejiro/epub";
|
|
1541
|
-
import { useCallback as
|
|
1647
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState5 } from "react";
|
|
1542
1648
|
function useEpub(options = {}) {
|
|
1543
1649
|
const [epub, setEpub] = useState5(null);
|
|
1544
1650
|
const [loading, setLoading] = useState5(false);
|
|
1545
1651
|
const [error, setError] = useState5(null);
|
|
1546
|
-
const
|
|
1547
|
-
const onLoadRef =
|
|
1652
|
+
const requests = useRequestToken();
|
|
1653
|
+
const onLoadRef = useRef9(options.onLoad);
|
|
1548
1654
|
onLoadRef.current = options.onLoad;
|
|
1549
|
-
const onErrorRef =
|
|
1655
|
+
const onErrorRef = useRef9(options.onError);
|
|
1550
1656
|
onErrorRef.current = options.onError;
|
|
1551
|
-
const fetchOptionsRef =
|
|
1657
|
+
const fetchOptionsRef = useRef9(options.fetchOptions);
|
|
1552
1658
|
fetchOptionsRef.current = options.fetchOptions;
|
|
1553
|
-
const fetchEpubRef =
|
|
1659
|
+
const fetchEpubRef = useRef9(options.fetchEpub);
|
|
1554
1660
|
fetchEpubRef.current = options.fetchEpub;
|
|
1555
|
-
const limitsRef =
|
|
1661
|
+
const limitsRef = useRef9(options.limits);
|
|
1556
1662
|
limitsRef.current = options.limits;
|
|
1557
|
-
const load =
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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);
|
|
1572
1683
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
|
|
1579
|
-
const loadFile = useCallback3(
|
|
1684
|
+
},
|
|
1685
|
+
[requests]
|
|
1686
|
+
);
|
|
1687
|
+
const loadBuffer = useCallback4((buffer) => load(async () => buffer), [load]);
|
|
1688
|
+
const loadFile = useCallback4(
|
|
1580
1689
|
(file) => load(() => readEpubFile(file, limitsRef.current)),
|
|
1581
1690
|
[load]
|
|
1582
1691
|
);
|
|
1583
|
-
const loadUrl =
|
|
1692
|
+
const loadUrl = useCallback4(
|
|
1584
1693
|
(url) => load(
|
|
1585
1694
|
() => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
|
|
1586
1695
|
),
|
|
1587
1696
|
[load]
|
|
1588
1697
|
);
|
|
1589
1698
|
const defaultUrl = options.defaultUrl;
|
|
1590
|
-
|
|
1699
|
+
useEffect6(() => {
|
|
1591
1700
|
if (!defaultUrl) return;
|
|
1592
1701
|
void loadUrl(defaultUrl);
|
|
1593
|
-
const requestId =
|
|
1702
|
+
const requestId = requests.current();
|
|
1594
1703
|
return () => {
|
|
1595
|
-
if (
|
|
1596
|
-
|
|
1704
|
+
if (requests.current() !== requestId) return;
|
|
1705
|
+
requests.invalidate();
|
|
1597
1706
|
setLoading(false);
|
|
1598
1707
|
};
|
|
1599
|
-
}, [defaultUrl, loadUrl]);
|
|
1600
|
-
|
|
1601
|
-
() =>
|
|
1602
|
-
|
|
1708
|
+
}, [defaultUrl, loadUrl, requests]);
|
|
1709
|
+
const replaceEpub = useCallback4(
|
|
1710
|
+
(book) => {
|
|
1711
|
+
requests.invalidate();
|
|
1712
|
+
setLoading(false);
|
|
1713
|
+
setError(null);
|
|
1714
|
+
setEpub(book);
|
|
1603
1715
|
},
|
|
1604
|
-
[]
|
|
1716
|
+
[requests]
|
|
1605
1717
|
);
|
|
1606
|
-
const replaceEpub = useCallback3((book) => {
|
|
1607
|
-
requestIdRef.current++;
|
|
1608
|
-
setLoading(false);
|
|
1609
|
-
setError(null);
|
|
1610
|
-
setEpub(book);
|
|
1611
|
-
}, []);
|
|
1612
1718
|
return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
|
|
1613
1719
|
}
|
|
1614
1720
|
|
|
1615
1721
|
// src/useMejiroBook.ts
|
|
1616
1722
|
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1617
|
-
import { useCallback as
|
|
1723
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef10, useState as useState6 } from "react";
|
|
1618
1724
|
function useMejiroBook(initial, options = {}) {
|
|
1619
|
-
const bookRef =
|
|
1725
|
+
const bookRef = useRef10(null);
|
|
1620
1726
|
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1621
1727
|
const [snapshot, setLocal] = useState6(initial);
|
|
1622
|
-
const optionsRef =
|
|
1728
|
+
const optionsRef = useRef10(options);
|
|
1623
1729
|
optionsRef.current = options;
|
|
1624
|
-
const pendingRef =
|
|
1625
|
-
const flushGenerationRef =
|
|
1626
|
-
const flush =
|
|
1730
|
+
const pendingRef = useRef10(null);
|
|
1731
|
+
const flushGenerationRef = useRef10(0);
|
|
1732
|
+
const flush = useCallback5(() => {
|
|
1627
1733
|
const pending = pendingRef.current;
|
|
1628
1734
|
if (!pending) return;
|
|
1629
1735
|
pendingRef.current = null;
|
|
@@ -1654,7 +1760,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1654
1760
|
}
|
|
1655
1761
|
);
|
|
1656
1762
|
}, []);
|
|
1657
|
-
const setOptions =
|
|
1763
|
+
const setOptions = useCallback5(
|
|
1658
1764
|
(next) => {
|
|
1659
1765
|
setLocal((prev) => mergeDefined(prev, next));
|
|
1660
1766
|
const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
|
|
@@ -1678,7 +1784,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1678
1784
|
},
|
|
1679
1785
|
[flush]
|
|
1680
1786
|
);
|
|
1681
|
-
|
|
1787
|
+
useEffect7(
|
|
1682
1788
|
() => () => {
|
|
1683
1789
|
bookRef.current?.dispose();
|
|
1684
1790
|
const pending = pendingRef.current;
|
|
@@ -1693,7 +1799,7 @@ function useMejiroBook(initial, options = {}) {
|
|
|
1693
1799
|
}
|
|
1694
1800
|
|
|
1695
1801
|
// src/useSpread.ts
|
|
1696
|
-
import { useCallback as
|
|
1802
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useLayoutEffect as useLayoutEffect3, useRef as useRef11, useState as useState7 } from "react";
|
|
1697
1803
|
function ignoreNavigationKey(e) {
|
|
1698
1804
|
if (e.defaultPrevented) return true;
|
|
1699
1805
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
|
|
@@ -1723,7 +1829,7 @@ function useSpread(layout, options = {}) {
|
|
|
1723
1829
|
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1724
1830
|
const turnDuration = options.turnDuration ?? 180;
|
|
1725
1831
|
const single = options.single ?? false;
|
|
1726
|
-
const onChangeRef =
|
|
1832
|
+
const onChangeRef = useRef11(options.onChange);
|
|
1727
1833
|
onChangeRef.current = options.onChange;
|
|
1728
1834
|
const [spreadIdx, setSpreadIdx] = useState7(0);
|
|
1729
1835
|
const [indexSingle, setIndexSingle] = useState7(single);
|
|
@@ -1733,22 +1839,22 @@ function useSpread(layout, options = {}) {
|
|
|
1733
1839
|
}
|
|
1734
1840
|
const [spread, setSpread] = useState7(null);
|
|
1735
1841
|
const [turning, setTurning] = useState7(false);
|
|
1736
|
-
const layoutRef =
|
|
1842
|
+
const layoutRef = useRef11(null);
|
|
1737
1843
|
layoutRef.current = layout;
|
|
1738
|
-
const spreadIdxRef =
|
|
1844
|
+
const spreadIdxRef = useRef11(0);
|
|
1739
1845
|
spreadIdxRef.current = spreadIdx;
|
|
1740
|
-
const singleRef =
|
|
1846
|
+
const singleRef = useRef11(single);
|
|
1741
1847
|
singleRef.current = single;
|
|
1742
|
-
const turnTimerRef =
|
|
1743
|
-
const turnTargetRef =
|
|
1744
|
-
const layoutGenerationRef =
|
|
1745
|
-
const lastEmittedRef =
|
|
1848
|
+
const turnTimerRef = useRef11(null);
|
|
1849
|
+
const turnTargetRef = useRef11(null);
|
|
1850
|
+
const layoutGenerationRef = useRef11(0);
|
|
1851
|
+
const lastEmittedRef = useRef11(null);
|
|
1746
1852
|
const totalPages = spread?.totalPages ?? 0;
|
|
1747
1853
|
const totalSpreads = navigationCount(totalPages, single);
|
|
1748
1854
|
const firstPage = firstPageOf(spreadIdx, single);
|
|
1749
1855
|
const layoutSpreadIdx = Math.floor(firstPage / 2);
|
|
1750
1856
|
const singleSide = single ? firstPage % 2 === 0 ? "right" : "left" : null;
|
|
1751
|
-
const refresh =
|
|
1857
|
+
const refresh = useCallback6(() => {
|
|
1752
1858
|
if (!layoutRef.current) {
|
|
1753
1859
|
setSpread(null);
|
|
1754
1860
|
return;
|
|
@@ -1771,7 +1877,7 @@ function useSpread(layout, options = {}) {
|
|
|
1771
1877
|
}
|
|
1772
1878
|
setSpread(layout.getSpread(0));
|
|
1773
1879
|
}, [layout]);
|
|
1774
|
-
|
|
1880
|
+
useEffect8(() => {
|
|
1775
1881
|
if (!layout) return;
|
|
1776
1882
|
setSpread(layout.getSpread(layoutSpreadIdx));
|
|
1777
1883
|
const last = lastEmittedRef.current;
|
|
@@ -1784,12 +1890,12 @@ function useSpread(layout, options = {}) {
|
|
|
1784
1890
|
}
|
|
1785
1891
|
lastEmittedRef.current = { layout, spreadIdx };
|
|
1786
1892
|
}, [layout, spreadIdx, layoutSpreadIdx]);
|
|
1787
|
-
const navigationTarget =
|
|
1893
|
+
const navigationTarget = useCallback6(() => {
|
|
1788
1894
|
const pending = turnTargetRef.current;
|
|
1789
1895
|
if (!pending) return spreadIdxRef.current;
|
|
1790
1896
|
return indexOfPageIn(firstPageOf(pending.index, pending.single), singleRef.current);
|
|
1791
1897
|
}, []);
|
|
1792
|
-
const goTo =
|
|
1898
|
+
const goTo = useCallback6(
|
|
1793
1899
|
(index) => {
|
|
1794
1900
|
if (!layoutRef.current) return;
|
|
1795
1901
|
const targetSingle = singleRef.current;
|
|
@@ -1823,7 +1929,7 @@ function useSpread(layout, options = {}) {
|
|
|
1823
1929
|
},
|
|
1824
1930
|
[turnDuration, navigationTarget]
|
|
1825
1931
|
);
|
|
1826
|
-
const setSpreadIndex =
|
|
1932
|
+
const setSpreadIndex = useCallback6((index) => {
|
|
1827
1933
|
if (!layoutRef.current) return;
|
|
1828
1934
|
if (turnTimerRef.current) {
|
|
1829
1935
|
clearTimeout(turnTimerRef.current);
|
|
@@ -1834,17 +1940,17 @@ function useSpread(layout, options = {}) {
|
|
|
1834
1940
|
const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
|
|
1835
1941
|
setSpreadIdx(Math.max(0, Math.min(max, index)));
|
|
1836
1942
|
}, []);
|
|
1837
|
-
const indexOfPage =
|
|
1838
|
-
const anchorAt =
|
|
1943
|
+
const indexOfPage = useCallback6((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
|
|
1944
|
+
const anchorAt = useCallback6(
|
|
1839
1945
|
(index) => {
|
|
1840
1946
|
if (!layout || index < 0 || index >= navigationCount(layout.totalPages, single)) return null;
|
|
1841
1947
|
return firstTextAnchorFrom(layout, firstPageOf(index, single));
|
|
1842
1948
|
},
|
|
1843
1949
|
[layout, single]
|
|
1844
1950
|
);
|
|
1845
|
-
const next =
|
|
1846
|
-
const prev =
|
|
1847
|
-
|
|
1951
|
+
const next = useCallback6(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
|
|
1952
|
+
const prev = useCallback6(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
|
|
1953
|
+
useEffect8(() => {
|
|
1848
1954
|
if (!enableKeyboard) return;
|
|
1849
1955
|
const onKey = (e) => {
|
|
1850
1956
|
if (!layoutRef.current) return;
|
|
@@ -1855,7 +1961,7 @@ function useSpread(layout, options = {}) {
|
|
|
1855
1961
|
window.addEventListener("keydown", onKey);
|
|
1856
1962
|
return () => window.removeEventListener("keydown", onKey);
|
|
1857
1963
|
}, [enableKeyboard, next, prev]);
|
|
1858
|
-
|
|
1964
|
+
useEffect8(
|
|
1859
1965
|
() => () => {
|
|
1860
1966
|
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1861
1967
|
},
|
|
@@ -1949,6 +2055,10 @@ function createAnchorResolver(nav) {
|
|
|
1949
2055
|
const update = (target) => {
|
|
1950
2056
|
const current = pending;
|
|
1951
2057
|
if (!(current && target.book)) return;
|
|
2058
|
+
if (current.moved) {
|
|
2059
|
+
settle();
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
1952
2062
|
const { anchor } = current;
|
|
1953
2063
|
if (!(anchor.chapter >= 0 && anchor.chapter < target.book.chapters.length)) {
|
|
1954
2064
|
settle();
|
|
@@ -1965,13 +2075,16 @@ function createAnchorResolver(nav) {
|
|
|
1965
2075
|
paragraph: anchor.paragraph,
|
|
1966
2076
|
charIndex: anchor.charIndex
|
|
1967
2077
|
});
|
|
1968
|
-
if (loc
|
|
2078
|
+
if (loc && nav.goToPage(loc.pageIdx)) {
|
|
2079
|
+
current.moved = true;
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
1969
2082
|
settle();
|
|
1970
2083
|
};
|
|
1971
2084
|
return {
|
|
1972
2085
|
request: (anchor, target) => new Promise((resolve) => {
|
|
1973
2086
|
settle();
|
|
1974
|
-
pending = { anchor, resolve, chapterRequested: false };
|
|
2087
|
+
pending = { anchor, resolve, chapterRequested: false, moved: false };
|
|
1975
2088
|
update(target);
|
|
1976
2089
|
}),
|
|
1977
2090
|
update,
|
|
@@ -2024,8 +2137,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2024
2137
|
onPageRead,
|
|
2025
2138
|
onChapterCompleted
|
|
2026
2139
|
} = props;
|
|
2027
|
-
const surfaceRef =
|
|
2028
|
-
const fileRef =
|
|
2140
|
+
const surfaceRef = useRef12(null);
|
|
2141
|
+
const fileRef = useRef12(null);
|
|
2029
2142
|
const [settingsOpen, setSettingsOpen] = useState8(false);
|
|
2030
2143
|
const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
|
|
2031
2144
|
const chapterIsUncontrolled = chapterProp == null;
|
|
@@ -2034,7 +2147,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2034
2147
|
null
|
|
2035
2148
|
);
|
|
2036
2149
|
const [autoSingle, setAutoSingle] = useState8(false);
|
|
2037
|
-
|
|
2150
|
+
useEffect9(() => {
|
|
2038
2151
|
if (!settingsOpen) return;
|
|
2039
2152
|
const onKey = (e2) => {
|
|
2040
2153
|
if (e2.key === "Escape") setSettingsOpen(false);
|
|
@@ -2042,20 +2155,23 @@ function MejiroReaderInner(props, ref) {
|
|
|
2042
2155
|
window.addEventListener("keydown", onKey);
|
|
2043
2156
|
return () => window.removeEventListener("keydown", onKey);
|
|
2044
2157
|
}, [settingsOpen]);
|
|
2045
|
-
|
|
2158
|
+
useEffect9(() => {
|
|
2046
2159
|
if (spreadMode !== "auto") return;
|
|
2047
2160
|
const surface = surfaceRef.current;
|
|
2048
2161
|
if (!surface) return;
|
|
2049
2162
|
const update = () => {
|
|
2050
2163
|
const rect = surface.getBoundingClientRect();
|
|
2051
|
-
setAutoSingle(rect.width < rect.height);
|
|
2164
|
+
setAutoSingle(rect.width < (fit === "width" ? window.innerHeight : rect.height));
|
|
2052
2165
|
};
|
|
2053
2166
|
update();
|
|
2054
|
-
|
|
2055
|
-
const observer = new ResizeObserver(update);
|
|
2056
|
-
observer
|
|
2057
|
-
return () =>
|
|
2058
|
-
|
|
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]);
|
|
2059
2175
|
const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
|
|
2060
2176
|
const resolvedGeometry = useMemo5(() => {
|
|
2061
2177
|
const columns = effectiveSingle ? 1 : 2;
|
|
@@ -2082,9 +2198,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2082
2198
|
() => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
|
|
2083
2199
|
[optionsProp]
|
|
2084
2200
|
);
|
|
2085
|
-
const onErrorRef =
|
|
2201
|
+
const onErrorRef = useRef12(onError);
|
|
2086
2202
|
onErrorRef.current = onError;
|
|
2087
|
-
const reportError =
|
|
2203
|
+
const reportError = useCallback7((error) => onErrorRef.current?.(error), []);
|
|
2088
2204
|
const {
|
|
2089
2205
|
book,
|
|
2090
2206
|
options: bookOptions,
|
|
@@ -2093,8 +2209,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2093
2209
|
debounceMs: OPTIONS_DEBOUNCE_MS,
|
|
2094
2210
|
onError: reportError
|
|
2095
2211
|
});
|
|
2096
|
-
const lastApplyRef =
|
|
2097
|
-
const setOptions =
|
|
2212
|
+
const lastApplyRef = useRef12(Promise.resolve());
|
|
2213
|
+
const setOptions = useCallback7(
|
|
2098
2214
|
(partial) => {
|
|
2099
2215
|
const applied = applyBookOptions(partial);
|
|
2100
2216
|
lastApplyRef.current = applied;
|
|
@@ -2103,10 +2219,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
2103
2219
|
[applyBookOptions]
|
|
2104
2220
|
);
|
|
2105
2221
|
const optionsPropKey = JSON.stringify(resolvedOptions);
|
|
2106
|
-
const syncedOptionsPropKeyRef =
|
|
2107
|
-
const resolvedOptionsRef =
|
|
2222
|
+
const syncedOptionsPropKeyRef = useRef12(optionsPropKey);
|
|
2223
|
+
const resolvedOptionsRef = useRef12(resolvedOptions);
|
|
2108
2224
|
resolvedOptionsRef.current = resolvedOptions;
|
|
2109
|
-
|
|
2225
|
+
useEffect9(() => {
|
|
2110
2226
|
if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
|
|
2111
2227
|
syncedOptionsPropKeyRef.current = optionsPropKey;
|
|
2112
2228
|
void setOptions(resolvedOptionsRef.current);
|
|
@@ -2125,7 +2241,6 @@ function MejiroReaderInner(props, ref) {
|
|
|
2125
2241
|
limits,
|
|
2126
2242
|
fetchEpub: fetchEpubFn,
|
|
2127
2243
|
onLoad: (b) => {
|
|
2128
|
-
if (chapterProp == null) setChapterState(0);
|
|
2129
2244
|
clearImagesRef.current();
|
|
2130
2245
|
onLoad?.(b);
|
|
2131
2246
|
}
|
|
@@ -2133,22 +2248,43 @@ function MejiroReaderInner(props, ref) {
|
|
|
2133
2248
|
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
2134
2249
|
const isManuscriptSource = manuscriptProp !== void 0;
|
|
2135
2250
|
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
2136
|
-
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
|
|
2251
|
+
const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : bookIdentity(epubProp);
|
|
2137
2252
|
const bookKey = controlled ? sourceKey : epubCtx.epub;
|
|
2138
|
-
const
|
|
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]);
|
|
2139
2273
|
const filePicker = enableDropZone && !controlled;
|
|
2140
|
-
|
|
2274
|
+
useEffect9(() => {
|
|
2141
2275
|
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
2142
2276
|
}, [epubCtx.error]);
|
|
2143
|
-
const pageGeometryRef =
|
|
2277
|
+
const pageGeometryRef = useRef12(resolvedGeometry);
|
|
2144
2278
|
pageGeometryRef.current = resolvedGeometry;
|
|
2145
|
-
const spreadAnchorRef =
|
|
2146
|
-
const spreadIdxPropRef =
|
|
2279
|
+
const spreadAnchorRef = useRef12(() => null);
|
|
2280
|
+
const spreadIdxPropRef = useRef12(spreadIdxProp);
|
|
2147
2281
|
spreadIdxPropRef.current = spreadIdxProp;
|
|
2148
|
-
const visibleAnchorRef =
|
|
2282
|
+
const visibleAnchorRef = useRef12(
|
|
2149
2283
|
null
|
|
2150
2284
|
);
|
|
2151
|
-
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, {
|
|
2152
2288
|
pageGeometry: () => pageGeometryRef.current,
|
|
2153
2289
|
// Preserve the reading position across a reflow re-layout (size / option
|
|
2154
2290
|
// changes), but only in uncontrolled mode — when `spreadIdx` is controlled
|
|
@@ -2161,8 +2297,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2161
2297
|
},
|
|
2162
2298
|
onError: reportError
|
|
2163
2299
|
});
|
|
2164
|
-
const reportedSpreadIdxRef =
|
|
2165
|
-
const spreadChangedRef =
|
|
2300
|
+
const reportedSpreadIdxRef = useRef12(null);
|
|
2301
|
+
const spreadChangedRef = useRef12(void 0);
|
|
2166
2302
|
spreadChangedRef.current = (i) => {
|
|
2167
2303
|
reportedSpreadIdxRef.current = i;
|
|
2168
2304
|
onSpreadChange?.(i);
|
|
@@ -2174,9 +2310,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2174
2310
|
onChange: (i) => spreadChangedRef.current?.(i)
|
|
2175
2311
|
});
|
|
2176
2312
|
spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
|
|
2177
|
-
const indexOfPageRef =
|
|
2313
|
+
const indexOfPageRef = useRef12(spreadCtx.indexOfPage);
|
|
2178
2314
|
indexOfPageRef.current = spreadCtx.indexOfPage;
|
|
2179
|
-
const anchorSingleRef =
|
|
2315
|
+
const anchorSingleRef = useRef12(effectiveSingle);
|
|
2180
2316
|
useLayoutEffect4(() => {
|
|
2181
2317
|
const l = layoutCtx.layout;
|
|
2182
2318
|
const flipped = anchorSingleRef.current !== effectiveSingle;
|
|
@@ -2184,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2184
2320
|
if (flipped && l && visibleAnchorRef.current?.layout === l) return;
|
|
2185
2321
|
visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2186
2322
|
}, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
|
|
2187
|
-
const setSpreadRef =
|
|
2323
|
+
const setSpreadRef = useRef12(spreadCtx.setSpread);
|
|
2188
2324
|
setSpreadRef.current = spreadCtx.setSpread;
|
|
2189
2325
|
useLayoutEffect4(() => {
|
|
2190
2326
|
const anchor = layoutCtx.pendingRestore.current;
|
|
@@ -2195,10 +2331,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
2195
2331
|
visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
|
|
2196
2332
|
}, [layoutCtx.layout]);
|
|
2197
2333
|
const optionsKey = reflowOptionsKey(bookOptions);
|
|
2198
|
-
const optionsKeyRef =
|
|
2199
|
-
const recomputeRef =
|
|
2334
|
+
const optionsKeyRef = useRef12(optionsKey);
|
|
2335
|
+
const recomputeRef = useRef12(layoutCtx.recompute);
|
|
2200
2336
|
recomputeRef.current = layoutCtx.recompute;
|
|
2201
|
-
|
|
2337
|
+
useEffect9(() => {
|
|
2202
2338
|
if (optionsKeyRef.current === optionsKey) return;
|
|
2203
2339
|
optionsKeyRef.current = optionsKey;
|
|
2204
2340
|
const timer = setTimeout(() => {
|
|
@@ -2214,8 +2350,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2214
2350
|
return () => clearTimeout(timer);
|
|
2215
2351
|
}, [optionsKey, reportError]);
|
|
2216
2352
|
const geometryKey = JSON.stringify(resolvedGeometry ?? null);
|
|
2217
|
-
const geometryKeyRef =
|
|
2218
|
-
|
|
2353
|
+
const geometryKeyRef = useRef12(geometryKey);
|
|
2354
|
+
useEffect9(() => {
|
|
2219
2355
|
if (geometryKeyRef.current === geometryKey) return;
|
|
2220
2356
|
geometryKeyRef.current = geometryKey;
|
|
2221
2357
|
void recomputeRef.current({ blank: false });
|
|
@@ -2237,30 +2373,29 @@ function MejiroReaderInner(props, ref) {
|
|
|
2237
2373
|
}
|
|
2238
2374
|
return result;
|
|
2239
2375
|
}, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
|
|
2240
|
-
const clearImagesRef =
|
|
2376
|
+
const clearImagesRef = useRef12(imageCtx.clearImages);
|
|
2241
2377
|
clearImagesRef.current = imageCtx.clearImages;
|
|
2242
|
-
const onLoadRef =
|
|
2378
|
+
const onLoadRef = useRef12(onLoad);
|
|
2243
2379
|
onLoadRef.current = onLoad;
|
|
2244
|
-
const eRef =
|
|
2380
|
+
const eRef = useRef12(e);
|
|
2245
2381
|
eRef.current = e;
|
|
2246
|
-
|
|
2382
|
+
useEffect9(() => {
|
|
2247
2383
|
if (!controlled) return;
|
|
2248
|
-
if (chapterIsUncontrolled) setChapterState(0);
|
|
2249
2384
|
clearImagesRef.current();
|
|
2250
2385
|
book.clearCache();
|
|
2251
2386
|
if (eRef.current) onLoadRef.current?.(eRef.current);
|
|
2252
|
-
}, [controlled, sourceKey, book
|
|
2253
|
-
const imagesChapterRef =
|
|
2254
|
-
|
|
2387
|
+
}, [controlled, sourceKey, book]);
|
|
2388
|
+
const imagesChapterRef = useRef12(chapter);
|
|
2389
|
+
useEffect9(() => {
|
|
2255
2390
|
if (imagesChapterRef.current === chapter) return;
|
|
2256
2391
|
imagesChapterRef.current = chapter;
|
|
2257
2392
|
clearImagesRef.current();
|
|
2258
2393
|
}, [chapter]);
|
|
2259
|
-
const spreadGoToRef =
|
|
2394
|
+
const spreadGoToRef = useRef12(spreadCtx.goTo);
|
|
2260
2395
|
spreadGoToRef.current = spreadCtx.goTo;
|
|
2261
|
-
const appliedSpreadIdxPropRef =
|
|
2396
|
+
const appliedSpreadIdxPropRef = useRef12(spreadIdxProp);
|
|
2262
2397
|
const [hostTurn, setHostTurn] = useState8(0);
|
|
2263
|
-
|
|
2398
|
+
useEffect9(() => {
|
|
2264
2399
|
const reported = reportedSpreadIdxRef.current;
|
|
2265
2400
|
reportedSpreadIdxRef.current = null;
|
|
2266
2401
|
if (spreadIdxProp == null) {
|
|
@@ -2274,12 +2409,17 @@ function MejiroReaderInner(props, ref) {
|
|
|
2274
2409
|
else if (reported === spreadCtx.spreadIdx) setHostTurn((n) => n + 1);
|
|
2275
2410
|
else setSpreadRef.current(spreadIdxProp);
|
|
2276
2411
|
}, [spreadIdxProp, spreadCtx.spreadIdx, hostTurn]);
|
|
2412
|
+
const restoredForRef = useRef12(null);
|
|
2277
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;
|
|
2278
2418
|
const next = spreadIdxPropRef.current;
|
|
2279
|
-
if (next == null
|
|
2419
|
+
if (next == null) return;
|
|
2280
2420
|
setSpreadRef.current(next);
|
|
2281
2421
|
}, [layoutCtx.layout]);
|
|
2282
|
-
const onChapter =
|
|
2422
|
+
const onChapter = useCallback7(
|
|
2283
2423
|
(index) => {
|
|
2284
2424
|
const i = clampChapter(index, e);
|
|
2285
2425
|
if (i === chapter) return;
|
|
@@ -2288,10 +2428,10 @@ function MejiroReaderInner(props, ref) {
|
|
|
2288
2428
|
},
|
|
2289
2429
|
[chapter, chapterProp, onChapterChange, e]
|
|
2290
2430
|
);
|
|
2291
|
-
const onChapterRef =
|
|
2431
|
+
const onChapterRef = useRef12(onChapter);
|
|
2292
2432
|
onChapterRef.current = onChapter;
|
|
2293
|
-
const listenersRef =
|
|
2294
|
-
const emit =
|
|
2433
|
+
const listenersRef = useRef12(/* @__PURE__ */ new Map());
|
|
2434
|
+
const emit = useCallback7(
|
|
2295
2435
|
(event, payload) => {
|
|
2296
2436
|
const set = listenersRef.current.get(event);
|
|
2297
2437
|
if (!set) return;
|
|
@@ -2299,7 +2439,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2299
2439
|
},
|
|
2300
2440
|
[]
|
|
2301
2441
|
);
|
|
2302
|
-
const layoutOwnerRef =
|
|
2442
|
+
const layoutOwnerRef = useRef12({
|
|
2303
2443
|
layout: null,
|
|
2304
2444
|
chapter,
|
|
2305
2445
|
book: bookKey
|
|
@@ -2310,22 +2450,29 @@ function MejiroReaderInner(props, ref) {
|
|
|
2310
2450
|
const owner = layoutOwnerRef.current;
|
|
2311
2451
|
const layout = owner.chapter === chapter && owner.book === bookKey ? layoutCtx.layout : null;
|
|
2312
2452
|
const anchorTarget = { book: epubCtx.loading ? null : e, chapter, layout };
|
|
2313
|
-
const anchorTargetRef =
|
|
2453
|
+
const anchorTargetRef = useRef12(anchorTarget);
|
|
2314
2454
|
anchorTargetRef.current = anchorTarget;
|
|
2315
|
-
const
|
|
2455
|
+
const spreadIdxNowRef = useRef12(spreadCtx.spreadIdx);
|
|
2456
|
+
spreadIdxNowRef.current = spreadCtx.spreadIdx;
|
|
2457
|
+
const anchorResolverRef = useRef12(null);
|
|
2316
2458
|
anchorResolverRef.current ??= createAnchorResolver({
|
|
2317
2459
|
goToChapter: (i) => onChapterRef.current(i),
|
|
2318
|
-
goToPage: (pageIdx) =>
|
|
2460
|
+
goToPage: (pageIdx) => {
|
|
2461
|
+
const target = indexOfPageRef.current(pageIdx);
|
|
2462
|
+
if (target === spreadIdxNowRef.current) return false;
|
|
2463
|
+
setSpreadRef.current(target);
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2319
2466
|
});
|
|
2320
|
-
|
|
2467
|
+
useEffect9(() => {
|
|
2321
2468
|
anchorResolverRef.current?.update(anchorTargetRef.current);
|
|
2322
|
-
}, [anchorTarget.book, chapter, layout]);
|
|
2323
|
-
|
|
2324
|
-
const onPageReadRef =
|
|
2469
|
+
}, [anchorTarget.book, chapter, layout, spreadCtx.spreadIdx]);
|
|
2470
|
+
useEffect9(() => () => anchorResolverRef.current?.dispose(), []);
|
|
2471
|
+
const onPageReadRef = useRef12(onPageRead);
|
|
2325
2472
|
onPageReadRef.current = onPageRead;
|
|
2326
|
-
const onChapterCompletedRef =
|
|
2473
|
+
const onChapterCompletedRef = useRef12(onChapterCompleted);
|
|
2327
2474
|
onChapterCompletedRef.current = onChapterCompleted;
|
|
2328
|
-
const trackLifecycleRef =
|
|
2475
|
+
const trackLifecycleRef = useRef12(null);
|
|
2329
2476
|
trackLifecycleRef.current ??= createLifecycleTracker({
|
|
2330
2477
|
spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
|
|
2331
2478
|
chapterFinished: (ch) => {
|
|
@@ -2334,7 +2481,7 @@ function MejiroReaderInner(props, ref) {
|
|
|
2334
2481
|
},
|
|
2335
2482
|
pageRead: (anchor, dwellMs) => onPageReadRef.current?.(anchor, dwellMs)
|
|
2336
2483
|
});
|
|
2337
|
-
|
|
2484
|
+
useEffect9(() => {
|
|
2338
2485
|
const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
|
|
2339
2486
|
trackLifecycleRef.current?.(bookKey, chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
|
|
2340
2487
|
}, [
|
|
@@ -2346,8 +2493,8 @@ function MejiroReaderInner(props, ref) {
|
|
|
2346
2493
|
layout,
|
|
2347
2494
|
spreadIdxProp
|
|
2348
2495
|
]);
|
|
2349
|
-
const prevTurningRef =
|
|
2350
|
-
|
|
2496
|
+
const prevTurningRef = useRef12(false);
|
|
2497
|
+
useEffect9(() => {
|
|
2351
2498
|
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
2352
2499
|
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
2353
2500
|
} else if (!spreadCtx.turning && prevTurningRef.current) {
|
|
@@ -2500,11 +2647,9 @@ function MejiroReaderInner(props, ref) {
|
|
|
2500
2647
|
] })
|
|
2501
2648
|
] }) : null;
|
|
2502
2649
|
const currentSpread = spreadCtx.layoutSpreadIdx;
|
|
2503
|
-
const rightPage = currentSpread * 2 + 1;
|
|
2504
2650
|
const leftPage = currentSpread * 2 + 2;
|
|
2505
2651
|
const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
|
|
2506
|
-
const
|
|
2507
|
-
const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
|
|
2652
|
+
const pageIndicator = enablePageIndicator ? /* @__PURE__ */ jsx13(MejiroPageIndicator, { current: spreadCtx.spreadIdx + 1, total: spreadCtx.totalSpreads }) : null;
|
|
2508
2653
|
return /* @__PURE__ */ jsx13(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs8(
|
|
2509
2654
|
"div",
|
|
2510
2655
|
{
|
|
@@ -2549,18 +2694,25 @@ function MejiroReaderInner(props, ref) {
|
|
|
2549
2694
|
),
|
|
2550
2695
|
onVisiblePageChange: (pageIdx, source) => {
|
|
2551
2696
|
if (source === "programmatic") return;
|
|
2552
|
-
|
|
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
|
+
}
|
|
2553
2703
|
const target = spreadCtx.indexOfPage(pageIdx);
|
|
2554
2704
|
if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
|
|
2555
2705
|
},
|
|
2556
2706
|
selectionRects: annotationRects.length ? annotationRects : void 0,
|
|
2557
2707
|
spreadIdx: spreadCtx.layoutSpreadIdx,
|
|
2558
2708
|
images: imageCtx.currentImages,
|
|
2709
|
+
pageNumbers,
|
|
2559
2710
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
2560
2711
|
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
2561
2712
|
onImageClose: imageCtx.removeImage
|
|
2562
2713
|
}
|
|
2563
2714
|
),
|
|
2715
|
+
layoutReady && mode === "scroll" && layoutCtx.layout && pageIndicator,
|
|
2564
2716
|
layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
|
|
2565
2717
|
MejiroSpread,
|
|
2566
2718
|
{
|
|
@@ -2578,20 +2730,14 @@ function MejiroReaderInner(props, ref) {
|
|
|
2578
2730
|
turning: spreadCtx.turning,
|
|
2579
2731
|
rightHeader: {
|
|
2580
2732
|
title: runningTitleRight,
|
|
2581
|
-
pageNumber:
|
|
2733
|
+
pageNumber: pageNumberFor2(pageNumbers, currentSpread * 2)
|
|
2582
2734
|
},
|
|
2583
2735
|
leftHeader: {
|
|
2584
2736
|
title: runningTitleLeft,
|
|
2585
|
-
pageNumber: showLeft
|
|
2737
|
+
pageNumber: showLeft ? pageNumberFor2(pageNumbers, currentSpread * 2 + 1) : null
|
|
2586
2738
|
},
|
|
2587
2739
|
images: imageCtx.currentImages,
|
|
2588
|
-
indicator:
|
|
2589
|
-
MejiroPageIndicator,
|
|
2590
|
-
{
|
|
2591
|
-
current: spreadCtx.spreadIdx + 1,
|
|
2592
|
-
total: spreadCtx.totalSpreads
|
|
2593
|
-
}
|
|
2594
|
-
) : null,
|
|
2740
|
+
indicator: pageIndicator,
|
|
2595
2741
|
onPrev: spreadCtx.prev,
|
|
2596
2742
|
onNext: spreadCtx.next,
|
|
2597
2743
|
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
@@ -2697,6 +2843,14 @@ function watermarkedBook(book, watermark) {
|
|
|
2697
2843
|
const copy = cloneEditableEpubBook(book);
|
|
2698
2844
|
const text = `${WATERMARK_PREFIX} ${watermark.text}`;
|
|
2699
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
|
+
}
|
|
2700
2854
|
chapter.blocks.unshift({
|
|
2701
2855
|
kind: "paragraph",
|
|
2702
2856
|
id: watermarkBlockId(chapter),
|
|
@@ -2704,10 +2858,19 @@ function watermarkedBook(book, watermark) {
|
|
|
2704
2858
|
inlineAnnotations: []
|
|
2705
2859
|
});
|
|
2706
2860
|
chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
|
|
2707
|
-
chapter.isDirty = true;
|
|
2708
2861
|
}
|
|
2709
2862
|
return copy;
|
|
2710
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
|
+
}
|
|
2711
2874
|
function watermarkBlockId(chapter) {
|
|
2712
2875
|
const used = new Set(chapter.blocks.map((block) => block.id));
|
|
2713
2876
|
let id = WATERMARK_BLOCK_ID;
|
|
@@ -2731,9 +2894,9 @@ function MejiroEditor({
|
|
|
2731
2894
|
onError
|
|
2732
2895
|
}) {
|
|
2733
2896
|
const messages = useI18n();
|
|
2734
|
-
const imageInputRef =
|
|
2735
|
-
const textareaRef =
|
|
2736
|
-
const readerRef =
|
|
2897
|
+
const imageInputRef = useRef13(null);
|
|
2898
|
+
const textareaRef = useRef13(null);
|
|
2899
|
+
const readerRef = useRef13(null);
|
|
2737
2900
|
const [editor, setEditor] = useState9(null);
|
|
2738
2901
|
const [loading, setLoading] = useState9(false);
|
|
2739
2902
|
const [error, setError] = useState9(null);
|
|
@@ -2743,10 +2906,10 @@ function MejiroEditor({
|
|
|
2743
2906
|
const [rubyStart, setRubyStart] = useState9(0);
|
|
2744
2907
|
const [rubyEnd, setRubyEnd] = useState9(1);
|
|
2745
2908
|
const [rubyText, setRubyText] = useState9("");
|
|
2746
|
-
const
|
|
2747
|
-
const onLoadRef =
|
|
2748
|
-
const onErrorRef =
|
|
2749
|
-
const limitsRef =
|
|
2909
|
+
const request = useRequestToken();
|
|
2910
|
+
const onLoadRef = useRef13(onLoad);
|
|
2911
|
+
const onErrorRef = useRef13(onError);
|
|
2912
|
+
const limitsRef = useRef13(limits);
|
|
2750
2913
|
limitsRef.current = limits;
|
|
2751
2914
|
const book = editor?.book ?? null;
|
|
2752
2915
|
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
@@ -2755,75 +2918,77 @@ function MejiroEditor({
|
|
|
2755
2918
|
const rubyRange = clampRubyRange(rubyStart, rubyEnd, [...text].length);
|
|
2756
2919
|
const previewBook = useMemo6(() => {
|
|
2757
2920
|
void revision;
|
|
2758
|
-
return book ? cloneEditableEpubBook(book) : null;
|
|
2759
|
-
}, [book, revision]);
|
|
2760
|
-
|
|
2921
|
+
return book && editor ? markBookIdentity(cloneEditableEpubBook(book), editor) : null;
|
|
2922
|
+
}, [book, editor, revision]);
|
|
2923
|
+
useEffect10(() => {
|
|
2761
2924
|
onLoadRef.current = onLoad;
|
|
2762
2925
|
}, [onLoad]);
|
|
2763
|
-
|
|
2926
|
+
useEffect10(() => {
|
|
2764
2927
|
onErrorRef.current = onError;
|
|
2765
2928
|
}, [onError]);
|
|
2766
|
-
const reportError =
|
|
2929
|
+
const reportError = useCallback8((nextError) => {
|
|
2767
2930
|
setError(nextError);
|
|
2768
2931
|
onErrorRef.current?.(nextError);
|
|
2769
2932
|
}, []);
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2933
|
+
const clearError = useCallback8(() => setError(null), []);
|
|
2934
|
+
const loadBufferForRequest = useCallback8(
|
|
2935
|
+
async (buffer, token) => {
|
|
2772
2936
|
setLoading(true);
|
|
2773
|
-
|
|
2937
|
+
clearError();
|
|
2774
2938
|
try {
|
|
2775
2939
|
const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
|
|
2776
|
-
if (
|
|
2940
|
+
if (!request.isCurrent(token)) return;
|
|
2777
2941
|
setEditor(next);
|
|
2778
2942
|
setSelection({ chapter: 0, paragraph: 0 });
|
|
2779
2943
|
setRevision((value) => value + 1);
|
|
2780
2944
|
onLoadRef.current?.(next);
|
|
2781
2945
|
} catch (err) {
|
|
2782
|
-
if (
|
|
2946
|
+
if (request.isCurrent(token)) reportError(toError(err));
|
|
2783
2947
|
} finally {
|
|
2784
|
-
if (
|
|
2948
|
+
if (request.isCurrent(token)) setLoading(false);
|
|
2785
2949
|
}
|
|
2786
2950
|
},
|
|
2787
|
-
[reportError]
|
|
2951
|
+
[clearError, reportError, request]
|
|
2788
2952
|
);
|
|
2789
2953
|
async function loadFile(file) {
|
|
2790
|
-
const
|
|
2954
|
+
const token = request.begin();
|
|
2791
2955
|
setLoading(true);
|
|
2792
|
-
|
|
2956
|
+
clearError();
|
|
2793
2957
|
try {
|
|
2794
|
-
await loadBufferForRequest(await readEpubFile(file, limitsRef.current),
|
|
2958
|
+
await loadBufferForRequest(await readEpubFile(file, limitsRef.current), token);
|
|
2795
2959
|
} catch (err) {
|
|
2796
|
-
if (
|
|
2960
|
+
if (request.isCurrent(token)) {
|
|
2797
2961
|
reportError(toError(err));
|
|
2798
2962
|
setLoading(false);
|
|
2799
2963
|
}
|
|
2800
2964
|
}
|
|
2801
2965
|
}
|
|
2802
|
-
|
|
2966
|
+
useEffect10(() => {
|
|
2803
2967
|
if (!epubUrl) return;
|
|
2804
|
-
|
|
2805
|
-
const requestId = ++loadRequestIdRef.current;
|
|
2968
|
+
const token = request.begin();
|
|
2806
2969
|
void (async () => {
|
|
2807
2970
|
setLoading(true);
|
|
2808
|
-
|
|
2971
|
+
clearError();
|
|
2809
2972
|
try {
|
|
2810
2973
|
const buffer = await fetchEpubBuffer(epubUrl);
|
|
2811
|
-
if (
|
|
2812
|
-
await loadBufferForRequest(buffer,
|
|
2974
|
+
if (!request.isCurrent(token)) return;
|
|
2975
|
+
await loadBufferForRequest(buffer, token);
|
|
2813
2976
|
} catch (err) {
|
|
2814
|
-
if (
|
|
2977
|
+
if (request.isCurrent(token)) reportError(toError(err));
|
|
2815
2978
|
} finally {
|
|
2816
|
-
if (
|
|
2979
|
+
if (request.isCurrent(token)) setLoading(false);
|
|
2817
2980
|
}
|
|
2818
2981
|
})();
|
|
2819
2982
|
return () => {
|
|
2820
|
-
|
|
2983
|
+
if (!request.isCurrent(token)) return;
|
|
2984
|
+
request.invalidate();
|
|
2985
|
+
setLoading(false);
|
|
2821
2986
|
};
|
|
2822
|
-
}, [epubUrl, loadBufferForRequest, reportError]);
|
|
2823
|
-
const syncedRef =
|
|
2987
|
+
}, [epubUrl, clearError, loadBufferForRequest, reportError, request]);
|
|
2988
|
+
const syncedRef = useRef13(
|
|
2824
2989
|
null
|
|
2825
2990
|
);
|
|
2826
|
-
|
|
2991
|
+
useEffect10(() => {
|
|
2827
2992
|
const nextText = paragraph?.text ?? "";
|
|
2828
2993
|
const target = `${selection.chapter}:${selection.paragraph}`;
|
|
2829
2994
|
const prev = syncedRef.current;
|
|
@@ -2837,7 +3002,7 @@ function MejiroEditor({
|
|
|
2837
3002
|
setRubyEnd(Math.min(1, [...nextText].length));
|
|
2838
3003
|
setRubyText("");
|
|
2839
3004
|
}, [editor, selection.chapter, selection.paragraph, paragraph]);
|
|
2840
|
-
|
|
3005
|
+
useEffect10(() => {
|
|
2841
3006
|
if (!previewBook) return;
|
|
2842
3007
|
void readerRef.current?.goToAnchor({
|
|
2843
3008
|
chapter: selection.chapter,
|
|
@@ -2885,6 +3050,7 @@ function MejiroEditor({
|
|
|
2885
3050
|
afterBlockId
|
|
2886
3051
|
});
|
|
2887
3052
|
setRevision((value) => value + 1);
|
|
3053
|
+
clearError();
|
|
2888
3054
|
}, reportError);
|
|
2889
3055
|
}
|
|
2890
3056
|
async function exportEpub() {
|
|
@@ -2898,10 +3064,20 @@ function MejiroEditor({
|
|
|
2898
3064
|
const decision = await onBeforeExport?.(buffer);
|
|
2899
3065
|
onExport?.(buffer);
|
|
2900
3066
|
const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
|
|
2901
|
-
if (
|
|
2902
|
-
|
|
3067
|
+
if (allowDownload) downloadEpub(buffer, `${editor.title || "edited"}.epub`);
|
|
3068
|
+
clearError();
|
|
2903
3069
|
}, reportError);
|
|
2904
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
|
+
);
|
|
2905
3081
|
return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
|
|
2906
3082
|
/* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
|
|
2907
3083
|
previewBook ? /* @__PURE__ */ jsx14(
|
|
@@ -2911,6 +3087,7 @@ function MejiroEditor({
|
|
|
2911
3087
|
epub: previewBook,
|
|
2912
3088
|
chapter: selection.chapter,
|
|
2913
3089
|
onChapterChange: (index) => selectParagraph(index, 0),
|
|
3090
|
+
onError: reportError,
|
|
2914
3091
|
fonts,
|
|
2915
3092
|
subtitle: messages.editorPreviewSubtitle,
|
|
2916
3093
|
chapterNavMode: "panel",
|
|
@@ -2948,16 +3125,7 @@ function MejiroEditor({
|
|
|
2948
3125
|
] }),
|
|
2949
3126
|
enableProofread && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
|
|
2950
3127
|
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
|
|
2951
|
-
|
|
2952
|
-
"textarea",
|
|
2953
|
-
{
|
|
2954
|
-
ref: textareaRef,
|
|
2955
|
-
value: text,
|
|
2956
|
-
readOnly: sceneBreak,
|
|
2957
|
-
onChange: (event) => setText(event.target.value),
|
|
2958
|
-
onSelect: captureRubyRange
|
|
2959
|
-
}
|
|
2960
|
-
),
|
|
3128
|
+
paragraphText,
|
|
2961
3129
|
/* @__PURE__ */ jsx14(
|
|
2962
3130
|
"button",
|
|
2963
3131
|
{
|
|
@@ -2972,6 +3140,7 @@ function MejiroEditor({
|
|
|
2972
3140
|
enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
|
|
2973
3141
|
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
|
|
2974
3142
|
/* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
|
|
3143
|
+
!enableProofread && paragraphText,
|
|
2975
3144
|
/* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
|
|
2976
3145
|
start: rubyRange?.start ?? 0,
|
|
2977
3146
|
end: rubyRange?.end ?? 0,
|
|
@@ -3038,8 +3207,8 @@ function paragraphBlock(chapter, paragraphIndex) {
|
|
|
3038
3207
|
}
|
|
3039
3208
|
|
|
3040
3209
|
// src/MejiroManuscriptEditor.tsx
|
|
3041
|
-
import { EpubProject } from "@libraz/mejiro/epub";
|
|
3042
|
-
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";
|
|
3043
3212
|
|
|
3044
3213
|
// src/MejiroNotationHighlighter.tsx
|
|
3045
3214
|
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
@@ -3047,7 +3216,7 @@ import {
|
|
|
3047
3216
|
forwardRef as forwardRef2,
|
|
3048
3217
|
useImperativeHandle as useImperativeHandle2,
|
|
3049
3218
|
useMemo as useMemo7,
|
|
3050
|
-
useRef as
|
|
3219
|
+
useRef as useRef14,
|
|
3051
3220
|
useState as useState10
|
|
3052
3221
|
} from "react";
|
|
3053
3222
|
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
@@ -3064,12 +3233,12 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
|
|
|
3064
3233
|
onCompositionEnd,
|
|
3065
3234
|
...rest
|
|
3066
3235
|
}, ref) {
|
|
3067
|
-
const textareaRef =
|
|
3068
|
-
const overlayRef =
|
|
3236
|
+
const textareaRef = useRef14(null);
|
|
3237
|
+
const overlayRef = useRef14(null);
|
|
3069
3238
|
useImperativeHandle2(ref, () => textareaRef.current, []);
|
|
3070
3239
|
const [isComposing, setIsComposing] = useState10(false);
|
|
3071
3240
|
const [composingValue, setComposingValue] = useState10("");
|
|
3072
|
-
const lastSegmentsRef =
|
|
3241
|
+
const lastSegmentsRef = useRef14(null);
|
|
3073
3242
|
const segments = useMemo7(() => {
|
|
3074
3243
|
if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
|
|
3075
3244
|
const next = buildSegments(value, dialect);
|
|
@@ -3162,83 +3331,349 @@ function buildSegments(text, dialect) {
|
|
|
3162
3331
|
return segments;
|
|
3163
3332
|
}
|
|
3164
3333
|
|
|
3165
|
-
// src/
|
|
3166
|
-
import {
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
autosaveKey,
|
|
3178
|
-
defaultChapterTitle,
|
|
3179
|
-
defaultChapterBody
|
|
3180
|
-
} = options;
|
|
3181
|
-
const titleForRef = useRef14(defaultChapterTitle);
|
|
3182
|
-
titleForRef.current = defaultChapterTitle;
|
|
3183
|
-
const bodyForRef = useRef14(defaultChapterBody);
|
|
3184
|
-
bodyForRef.current = defaultChapterBody;
|
|
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
|
+
);
|
|
3185
3346
|
const [chapters, setChaptersState] = useState11(
|
|
3186
|
-
() => options.
|
|
3347
|
+
() => options.chapters?.length ? options.chapters : [defaultChapter([], defaultTitle, defaultBody)]
|
|
3187
3348
|
);
|
|
3188
|
-
const [
|
|
3189
|
-
const [
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
const
|
|
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);
|
|
3195
3365
|
chaptersRef.current = chapters;
|
|
3196
|
-
const selectedRef =
|
|
3197
|
-
selectedRef.current =
|
|
3198
|
-
const
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
const
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
const plain = snapshotChapters(chaptersRef.current);
|
|
3215
|
-
const payload = payloadRef.current ? payloadRef.current(plain) : plain;
|
|
3216
|
-
const markSaved = () => {
|
|
3217
|
-
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
3218
|
-
};
|
|
3219
|
-
let saved;
|
|
3220
|
-
try {
|
|
3221
|
-
saved = callback(payload);
|
|
3222
|
-
} catch (err) {
|
|
3223
|
-
saved = Promise.reject(err);
|
|
3224
|
-
}
|
|
3225
|
-
if (saved === void 0) {
|
|
3226
|
-
markSaved();
|
|
3227
|
-
return;
|
|
3228
|
-
}
|
|
3229
|
-
inFlightRef.current = true;
|
|
3230
|
-
void Promise.resolve(saved).then(markSaved).catch((err) => {
|
|
3231
|
-
if (!mountedRef.current) return;
|
|
3232
|
-
setAutosaveError(toError(err));
|
|
3233
|
-
}).finally(() => {
|
|
3234
|
-
inFlightRef.current = false;
|
|
3235
|
-
if (!flushQueuedRef.current) return;
|
|
3236
|
-
flushQueuedRef.current = false;
|
|
3237
|
-
flushAutosave();
|
|
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 } : {}
|
|
3238
3384
|
});
|
|
3385
|
+
for (const asset of assetsRef.current) project.addAsset(asset);
|
|
3386
|
+
return project;
|
|
3239
3387
|
}, []);
|
|
3240
|
-
|
|
3241
|
-
|
|
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
|
+
|
|
3600
|
+
// src/useManuscriptDraft.ts
|
|
3601
|
+
import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState12 } from "react";
|
|
3602
|
+
var DEFAULT_DELAY = 800;
|
|
3603
|
+
function defaultChapter2(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
|
|
3604
|
+
const index = existing.length;
|
|
3605
|
+
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
3606
|
+
}
|
|
3607
|
+
function useManuscriptDraft(options = {}) {
|
|
3608
|
+
const {
|
|
3609
|
+
onAutosave,
|
|
3610
|
+
autosaveDelay = DEFAULT_DELAY,
|
|
3611
|
+
autosavePayload,
|
|
3612
|
+
autosaveKey,
|
|
3613
|
+
defaultChapterTitle,
|
|
3614
|
+
defaultChapterBody
|
|
3615
|
+
} = options;
|
|
3616
|
+
const titleForRef = useRef16(defaultChapterTitle);
|
|
3617
|
+
titleForRef.current = defaultChapterTitle;
|
|
3618
|
+
const bodyForRef = useRef16(defaultChapterBody);
|
|
3619
|
+
bodyForRef.current = defaultChapterBody;
|
|
3620
|
+
const [chapters, setChaptersState] = useState12(
|
|
3621
|
+
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter2([], defaultChapterTitle, defaultChapterBody)]
|
|
3622
|
+
);
|
|
3623
|
+
const [selected, setSelectedState] = useState12(0);
|
|
3624
|
+
const [autosaveError, setAutosaveError] = useState12(null);
|
|
3625
|
+
const saveRef = useRef16(onAutosave);
|
|
3626
|
+
saveRef.current = onAutosave;
|
|
3627
|
+
const payloadRef = useRef16(autosavePayload);
|
|
3628
|
+
payloadRef.current = autosavePayload;
|
|
3629
|
+
const chaptersRef = useRef16(chapters);
|
|
3630
|
+
chaptersRef.current = chapters;
|
|
3631
|
+
const selectedRef = useRef16(selected);
|
|
3632
|
+
selectedRef.current = selected;
|
|
3633
|
+
const trackerRef = useRef16(null);
|
|
3634
|
+
if (!trackerRef.current)
|
|
3635
|
+
trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
|
|
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(() => {
|
|
3642
|
+
const callback = saveRef.current;
|
|
3643
|
+
if (!(callback && dirtyRef.current)) return;
|
|
3644
|
+
if (inFlightRef.current) {
|
|
3645
|
+
flushQueuedRef.current = true;
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
const revision = revisionRef.current;
|
|
3649
|
+
const plain = snapshotChapters(chaptersRef.current);
|
|
3650
|
+
const payload = payloadRef.current ? payloadRef.current(plain) : plain;
|
|
3651
|
+
const markSaved = () => {
|
|
3652
|
+
if (revisionRef.current === revision) dirtyRef.current = false;
|
|
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) => {
|
|
3666
|
+
if (!mountedRef.current) return;
|
|
3667
|
+
setAutosaveError(toError(err));
|
|
3668
|
+
}).finally(() => {
|
|
3669
|
+
inFlightRef.current = false;
|
|
3670
|
+
if (!flushQueuedRef.current) return;
|
|
3671
|
+
flushQueuedRef.current = false;
|
|
3672
|
+
flushAutosave();
|
|
3673
|
+
});
|
|
3674
|
+
}, []);
|
|
3675
|
+
useEffect12(() => {
|
|
3676
|
+
if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
|
|
3242
3677
|
dirtyRef.current = true;
|
|
3243
3678
|
revisionRef.current += 1;
|
|
3244
3679
|
setAutosaveError(null);
|
|
@@ -3249,7 +3684,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
3249
3684
|
}, autosaveDelay);
|
|
3250
3685
|
return () => clearTimeout(timer);
|
|
3251
3686
|
}, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
|
|
3252
|
-
|
|
3687
|
+
useEffect12(() => {
|
|
3253
3688
|
mountedRef.current = true;
|
|
3254
3689
|
const detachPageHide = flushOnPageHide(flushAutosave);
|
|
3255
3690
|
return () => {
|
|
@@ -3258,20 +3693,20 @@ function useManuscriptDraft(options = {}) {
|
|
|
3258
3693
|
detachPageHide();
|
|
3259
3694
|
};
|
|
3260
3695
|
}, [flushAutosave]);
|
|
3261
|
-
const commit =
|
|
3696
|
+
const commit = useCallback10((nextChapters, nextSelected) => {
|
|
3262
3697
|
chaptersRef.current = nextChapters;
|
|
3263
3698
|
selectedRef.current = nextSelected;
|
|
3264
3699
|
setChaptersState(nextChapters);
|
|
3265
3700
|
setSelectedState(nextSelected);
|
|
3266
3701
|
}, []);
|
|
3267
|
-
const setSelected =
|
|
3702
|
+
const setSelected = useCallback10((index) => {
|
|
3268
3703
|
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
3269
3704
|
selectedRef.current = next;
|
|
3270
3705
|
setSelectedState(next);
|
|
3271
3706
|
}, []);
|
|
3272
|
-
const setChapters =
|
|
3707
|
+
const setChapters = useCallback10(
|
|
3273
3708
|
(next) => {
|
|
3274
|
-
const normalized = next.length ? next : [
|
|
3709
|
+
const normalized = next.length ? next : [defaultChapter2([], titleForRef.current, bodyForRef.current)];
|
|
3275
3710
|
const prev = selectedRef.current;
|
|
3276
3711
|
const selectedId = chaptersRef.current[prev]?.id;
|
|
3277
3712
|
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
@@ -3282,17 +3717,17 @@ function useManuscriptDraft(options = {}) {
|
|
|
3282
3717
|
},
|
|
3283
3718
|
[commit]
|
|
3284
3719
|
);
|
|
3285
|
-
const patchChapter =
|
|
3720
|
+
const patchChapter = useCallback10((index, patch) => {
|
|
3286
3721
|
const next = chaptersRef.current.map(
|
|
3287
3722
|
(chapter, i) => i === index ? { ...chapter, ...patch } : chapter
|
|
3288
3723
|
);
|
|
3289
3724
|
chaptersRef.current = next;
|
|
3290
3725
|
setChaptersState(next);
|
|
3291
3726
|
}, []);
|
|
3292
|
-
const addChapter =
|
|
3727
|
+
const addChapter = useCallback10(
|
|
3293
3728
|
(chapter = {}) => {
|
|
3294
3729
|
const current = chaptersRef.current;
|
|
3295
|
-
const generated =
|
|
3730
|
+
const generated = defaultChapter2(current, titleForRef.current, bodyForRef.current);
|
|
3296
3731
|
const next = [
|
|
3297
3732
|
...current,
|
|
3298
3733
|
{
|
|
@@ -3305,7 +3740,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
3305
3740
|
},
|
|
3306
3741
|
[commit]
|
|
3307
3742
|
);
|
|
3308
|
-
const removeChapter =
|
|
3743
|
+
const removeChapter = useCallback10(
|
|
3309
3744
|
(index) => {
|
|
3310
3745
|
const current = chaptersRef.current;
|
|
3311
3746
|
if (current.length <= 1) return;
|
|
@@ -3319,7 +3754,7 @@ function useManuscriptDraft(options = {}) {
|
|
|
3319
3754
|
},
|
|
3320
3755
|
[commit]
|
|
3321
3756
|
);
|
|
3322
|
-
const reorderChapters =
|
|
3757
|
+
const reorderChapters = useCallback10(
|
|
3323
3758
|
(from, to) => {
|
|
3324
3759
|
const current = chaptersRef.current;
|
|
3325
3760
|
if (from < 0 || from >= current.length) return;
|
|
@@ -3372,7 +3807,7 @@ function coverAssetHref(file) {
|
|
|
3372
3807
|
const filename = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3373
3808
|
return `OPS/Images/${filename && !/^\.+$/u.test(filename) ? filename : `cover${coverExtension(file.type)}`}`;
|
|
3374
3809
|
}
|
|
3375
|
-
function
|
|
3810
|
+
function defaultChapter3(messages) {
|
|
3376
3811
|
return {
|
|
3377
3812
|
id: `chapter-${Date.now()}-0`,
|
|
3378
3813
|
title: format(messages.manuscriptDefaultChapterTitle, { n: 1 }),
|
|
@@ -3398,41 +3833,41 @@ function MejiroManuscriptEditor({
|
|
|
3398
3833
|
panelSide = "right"
|
|
3399
3834
|
}) {
|
|
3400
3835
|
const messages = useI18n();
|
|
3401
|
-
const coverInputRef =
|
|
3402
|
-
const bodyTextareaRef =
|
|
3836
|
+
const coverInputRef = useRef17(null);
|
|
3837
|
+
const bodyTextareaRef = useRef17(null);
|
|
3403
3838
|
const titleControlled = onTitleChange !== void 0;
|
|
3404
3839
|
const authorControlled = onAuthorChange !== void 0;
|
|
3405
3840
|
const coverControlled = onCoverChange !== void 0;
|
|
3406
|
-
const [titleState, setTitleState] =
|
|
3407
|
-
const [authorState, setAuthorState] =
|
|
3408
|
-
const [coverState, setCoverState] =
|
|
3841
|
+
const [titleState, setTitleState] = useState13(titleProp ?? messages.manuscriptDefaultTitle);
|
|
3842
|
+
const [authorState, setAuthorState] = useState13(authorProp ?? "");
|
|
3843
|
+
const [coverState, setCoverState] = useState13(coverProp ?? null);
|
|
3409
3844
|
const title = titleControlled ? titleProp ?? "" : titleState;
|
|
3410
3845
|
const author = authorControlled ? authorProp ?? "" : authorState;
|
|
3411
3846
|
const cover = coverControlled ? coverProp ?? null : coverState;
|
|
3412
|
-
|
|
3847
|
+
useEffect13(() => {
|
|
3413
3848
|
if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
|
|
3414
3849
|
}, [titleControlled, titleProp]);
|
|
3415
|
-
|
|
3850
|
+
useEffect13(() => {
|
|
3416
3851
|
if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
|
|
3417
3852
|
}, [authorControlled, authorProp]);
|
|
3418
|
-
|
|
3853
|
+
useEffect13(() => {
|
|
3419
3854
|
if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
|
|
3420
3855
|
}, [coverControlled, coverProp]);
|
|
3421
|
-
const setTitle =
|
|
3856
|
+
const setTitle = useCallback11(
|
|
3422
3857
|
(next) => {
|
|
3423
3858
|
if (!titleControlled) setTitleState(next);
|
|
3424
3859
|
onTitleChange?.(next);
|
|
3425
3860
|
},
|
|
3426
3861
|
[titleControlled, onTitleChange]
|
|
3427
3862
|
);
|
|
3428
|
-
const setAuthor =
|
|
3863
|
+
const setAuthor = useCallback11(
|
|
3429
3864
|
(next) => {
|
|
3430
3865
|
if (!authorControlled) setAuthorState(next);
|
|
3431
3866
|
onAuthorChange?.(next);
|
|
3432
3867
|
},
|
|
3433
3868
|
[authorControlled, onAuthorChange]
|
|
3434
3869
|
);
|
|
3435
|
-
const setCover =
|
|
3870
|
+
const setCover = useCallback11(
|
|
3436
3871
|
(next) => {
|
|
3437
3872
|
if (!coverControlled) setCoverState(next);
|
|
3438
3873
|
onCoverChange?.(next);
|
|
@@ -3440,7 +3875,7 @@ function MejiroManuscriptEditor({
|
|
|
3440
3875
|
[coverControlled, onCoverChange]
|
|
3441
3876
|
);
|
|
3442
3877
|
const draft = useManuscriptDraft({
|
|
3443
|
-
initialChapters: initialChapters?.length ? initialChapters : [
|
|
3878
|
+
initialChapters: initialChapters?.length ? initialChapters : [defaultChapter3(messages)],
|
|
3444
3879
|
onAutosave,
|
|
3445
3880
|
autosavePayload: (savedChapters) => ({ title, author, cover, chapters: savedChapters }),
|
|
3446
3881
|
autosaveKey: `${title}\0${author}\0${cover?.name ?? ""}\0${cover?.size ?? 0}\0${cover?.lastModified ?? 0}`,
|
|
@@ -3457,8 +3892,16 @@ function MejiroManuscriptEditor({
|
|
|
3457
3892
|
reorderChapters
|
|
3458
3893
|
} = draft;
|
|
3459
3894
|
const current = chapters[selected] ?? chapters[0];
|
|
3460
|
-
const [draggingIndex, setDraggingIndex] =
|
|
3461
|
-
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), []);
|
|
3462
3905
|
function wrapSelection(open, close) {
|
|
3463
3906
|
const el = bodyTextareaRef.current;
|
|
3464
3907
|
if (!el) return;
|
|
@@ -3476,36 +3919,30 @@ function MejiroManuscriptEditor({
|
|
|
3476
3919
|
bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
|
|
3477
3920
|
});
|
|
3478
3921
|
}
|
|
3479
|
-
const [identifier] =
|
|
3480
|
-
const exportEpub =
|
|
3481
|
-
await withErrorReporting(
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
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()
|
|
3491
3939
|
});
|
|
3492
|
-
if (cover) {
|
|
3493
|
-
project.setCover({
|
|
3494
|
-
href: coverAssetHref(cover),
|
|
3495
|
-
mediaType: cover.type || void 0,
|
|
3496
|
-
data: await cover.arrayBuffer()
|
|
3497
|
-
});
|
|
3498
|
-
}
|
|
3499
|
-
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
3500
|
-
setExportError(null);
|
|
3501
|
-
onExport?.(buffer);
|
|
3502
|
-
downloadEpub(buffer, `${title || "book"}.epub`);
|
|
3503
|
-
},
|
|
3504
|
-
(error) => {
|
|
3505
|
-
setExportError(error);
|
|
3506
|
-
onError?.(error);
|
|
3507
3940
|
}
|
|
3508
|
-
|
|
3941
|
+
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
3942
|
+
clearError();
|
|
3943
|
+
onExport?.(buffer);
|
|
3944
|
+
downloadEpub(buffer, `${title || "book"}.epub`);
|
|
3945
|
+
}, reportError);
|
|
3509
3946
|
}, [
|
|
3510
3947
|
assetResolver,
|
|
3511
3948
|
author,
|
|
@@ -3514,11 +3951,12 @@ function MejiroManuscriptEditor({
|
|
|
3514
3951
|
dialect,
|
|
3515
3952
|
identifier,
|
|
3516
3953
|
messages.untitled,
|
|
3517
|
-
onError,
|
|
3518
3954
|
onExport,
|
|
3955
|
+
clearError,
|
|
3956
|
+
reportError,
|
|
3519
3957
|
title
|
|
3520
3958
|
]);
|
|
3521
|
-
const previewManuscript =
|
|
3959
|
+
const previewManuscript = useMemo9(
|
|
3522
3960
|
() => chapters.map((chapter) => ({
|
|
3523
3961
|
id: chapter.id,
|
|
3524
3962
|
title: chapter.title || messages.untitled,
|
|
@@ -3543,6 +3981,7 @@ function MejiroManuscriptEditor({
|
|
|
3543
3981
|
fonts,
|
|
3544
3982
|
chapter: selected,
|
|
3545
3983
|
onChapterChange: setSelected,
|
|
3984
|
+
onError: reportError,
|
|
3546
3985
|
enableImageOverlay: false
|
|
3547
3986
|
}
|
|
3548
3987
|
) }),
|
|
@@ -3566,7 +4005,11 @@ function MejiroManuscriptEditor({
|
|
|
3566
4005
|
type: "file",
|
|
3567
4006
|
accept: "image/*",
|
|
3568
4007
|
hidden: true,
|
|
3569
|
-
onChange: (event) =>
|
|
4008
|
+
onChange: (event) => {
|
|
4009
|
+
const file = event.target.files?.[0] ?? null;
|
|
4010
|
+
event.target.value = "";
|
|
4011
|
+
setCover(file);
|
|
4012
|
+
}
|
|
3570
4013
|
}
|
|
3571
4014
|
)
|
|
3572
4015
|
] }),
|
|
@@ -3585,6 +4028,7 @@ function MejiroManuscriptEditor({
|
|
|
3585
4028
|
onDragStart: (event) => {
|
|
3586
4029
|
setDraggingIndex(index);
|
|
3587
4030
|
event.dataTransfer.effectAllowed = "move";
|
|
4031
|
+
event.dataTransfer.setData("text/plain", chapter.id);
|
|
3588
4032
|
},
|
|
3589
4033
|
onDragEnd: () => setDraggingIndex(null),
|
|
3590
4034
|
onDragOver: (event) => {
|
|
@@ -3692,7 +4136,7 @@ function MejiroShelf({
|
|
|
3692
4136
|
}
|
|
3693
4137
|
|
|
3694
4138
|
// src/MejiroToc.tsx
|
|
3695
|
-
import { useMemo as
|
|
4139
|
+
import { useMemo as useMemo10, useState as useState14 } from "react";
|
|
3696
4140
|
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3697
4141
|
function buildEntries(epub, messages) {
|
|
3698
4142
|
return epub.chapters.map((ch, i) => {
|
|
@@ -3715,9 +4159,9 @@ function MejiroToc({
|
|
|
3715
4159
|
const messages = useI18n();
|
|
3716
4160
|
const resolvedTitle = title ?? messages.tocTitle;
|
|
3717
4161
|
const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
|
|
3718
|
-
const [query, setQuery] =
|
|
3719
|
-
const entries =
|
|
3720
|
-
const filtered =
|
|
4162
|
+
const [query, setQuery] = useState14("");
|
|
4163
|
+
const entries = useMemo10(() => buildEntries(epub, messages), [epub, messages]);
|
|
4164
|
+
const filtered = useMemo10(() => {
|
|
3721
4165
|
if (!query) return entries;
|
|
3722
4166
|
const needle = query.toLowerCase();
|
|
3723
4167
|
return entries.filter(
|
|
@@ -3767,34 +4211,41 @@ import {
|
|
|
3767
4211
|
serializeAnnotations,
|
|
3768
4212
|
sortAnnotations
|
|
3769
4213
|
} from "@libraz/mejiro";
|
|
3770
|
-
import { useCallback as
|
|
4214
|
+
import { useCallback as useCallback12, useEffect as useEffect14, useRef as useRef18, useState as useState15 } from "react";
|
|
3771
4215
|
function parseSorted(raw) {
|
|
3772
4216
|
return sortAnnotations(parseAnnotations(raw));
|
|
3773
4217
|
}
|
|
3774
4218
|
function useAnnotations(options) {
|
|
3775
4219
|
const { key, throttleMs = 250 } = options;
|
|
3776
4220
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
3777
|
-
const
|
|
3778
|
-
() => readStorage(storage, key, parseSorted, [])
|
|
3779
|
-
);
|
|
3780
|
-
const storageRef = useRef16(storage);
|
|
4221
|
+
const storageRef = useRef18(storage);
|
|
3781
4222
|
storageRef.current = storage;
|
|
3782
|
-
const
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
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);
|
|
3788
4242
|
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
3789
4243
|
const pending = pendingRef.current;
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
return pending.flush;
|
|
3794
|
-
}, [key, applyNext, pending]);
|
|
3795
|
-
const onChangeRef = useRef16(options.onChange);
|
|
4244
|
+
useEffect14(() => flushOnPageHide(pending.flush), [pending]);
|
|
4245
|
+
useEffect14(() => pending.flush, [key, pending]);
|
|
4246
|
+
const onChangeRef = useRef18(options.onChange);
|
|
3796
4247
|
onChangeRef.current = options.onChange;
|
|
3797
|
-
const commit =
|
|
4248
|
+
const commit = useCallback12(
|
|
3798
4249
|
(next) => {
|
|
3799
4250
|
const currentStorage = storageRef.current;
|
|
3800
4251
|
if (currentStorage) {
|
|
@@ -3806,7 +4257,7 @@ function useAnnotations(options) {
|
|
|
3806
4257
|
},
|
|
3807
4258
|
[key, throttleMs, pending]
|
|
3808
4259
|
);
|
|
3809
|
-
const add =
|
|
4260
|
+
const add = useCallback12(
|
|
3810
4261
|
(input) => {
|
|
3811
4262
|
const annotation = {
|
|
3812
4263
|
...input,
|
|
@@ -3820,7 +4271,7 @@ function useAnnotations(options) {
|
|
|
3820
4271
|
},
|
|
3821
4272
|
[commit, applyNext]
|
|
3822
4273
|
);
|
|
3823
|
-
const remove =
|
|
4274
|
+
const remove = useCallback12(
|
|
3824
4275
|
(id) => {
|
|
3825
4276
|
const current = annotationsRef.current;
|
|
3826
4277
|
const next = current.filter((annotation) => annotation.id !== id);
|
|
@@ -3830,7 +4281,7 @@ function useAnnotations(options) {
|
|
|
3830
4281
|
},
|
|
3831
4282
|
[commit, applyNext]
|
|
3832
4283
|
);
|
|
3833
|
-
const update =
|
|
4284
|
+
const update = useCallback12(
|
|
3834
4285
|
(id, patch) => {
|
|
3835
4286
|
let changed = false;
|
|
3836
4287
|
const next = annotationsRef.current.map((annotation) => {
|
|
@@ -3845,7 +4296,8 @@ function useAnnotations(options) {
|
|
|
3845
4296
|
},
|
|
3846
4297
|
[commit, applyNext]
|
|
3847
4298
|
);
|
|
3848
|
-
const clear =
|
|
4299
|
+
const clear = useCallback12(() => {
|
|
4300
|
+
if (annotationsRef.current.length === 0) return;
|
|
3849
4301
|
applyNext([]);
|
|
3850
4302
|
pending.cancel();
|
|
3851
4303
|
removeStorage(storageRef.current, key);
|
|
@@ -3860,39 +4312,40 @@ import {
|
|
|
3860
4312
|
cloneEditableEpubBook as cloneEditableEpubBook2,
|
|
3861
4313
|
EditableEpub as EditableEpub2
|
|
3862
4314
|
} from "@libraz/mejiro/epub";
|
|
3863
|
-
import { useCallback as
|
|
4315
|
+
import { useCallback as useCallback13, useEffect as useEffect15, useMemo as useMemo11, useRef as useRef19, useState as useState16 } from "react";
|
|
3864
4316
|
function useEditableEpub(options = {}) {
|
|
3865
|
-
const [editor, setEditor] =
|
|
3866
|
-
const [loading, setLoading] =
|
|
3867
|
-
const [exporting, setExporting] =
|
|
3868
|
-
const [error, setError] =
|
|
3869
|
-
const [revision, setRevision] =
|
|
3870
|
-
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({
|
|
3871
4323
|
chapter: 0,
|
|
3872
4324
|
paragraph: 0
|
|
3873
4325
|
});
|
|
3874
|
-
const
|
|
3875
|
-
const
|
|
3876
|
-
const
|
|
3877
|
-
const
|
|
3878
|
-
const
|
|
3879
|
-
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);
|
|
3880
4333
|
onLoadRef.current = options.onLoad;
|
|
3881
4334
|
onErrorRef.current = options.onError;
|
|
3882
4335
|
onExportRef.current = options.onExport;
|
|
3883
4336
|
limitsRef.current = options.limits;
|
|
3884
4337
|
assetResolverRef.current = options.assetResolver;
|
|
3885
|
-
const editorRef =
|
|
4338
|
+
const editorRef = useRef19(editor);
|
|
3886
4339
|
editorRef.current = editor;
|
|
3887
|
-
const selectionRef =
|
|
4340
|
+
const selectionRef = useRef19(selection);
|
|
3888
4341
|
selectionRef.current = selection;
|
|
3889
|
-
const loadBufferWithRequest =
|
|
3890
|
-
async (buffer,
|
|
4342
|
+
const loadBufferWithRequest = useCallback13(
|
|
4343
|
+
async (buffer, currentRequest) => {
|
|
3891
4344
|
setLoading(true);
|
|
3892
4345
|
setError(null);
|
|
3893
4346
|
try {
|
|
3894
4347
|
const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
|
|
3895
|
-
if (
|
|
4348
|
+
if (!request.isCurrent(currentRequest)) return null;
|
|
3896
4349
|
const firstParagraph = { chapter: 0, paragraph: 0 };
|
|
3897
4350
|
editorRef.current = next;
|
|
3898
4351
|
selectionRef.current = firstParagraph;
|
|
@@ -3902,34 +4355,37 @@ function useEditableEpub(options = {}) {
|
|
|
3902
4355
|
onLoadRef.current?.(next);
|
|
3903
4356
|
return next;
|
|
3904
4357
|
} catch (err) {
|
|
3905
|
-
if (
|
|
4358
|
+
if (request.isCurrent(currentRequest)) {
|
|
3906
4359
|
const nextError = toError(err);
|
|
3907
4360
|
setError(nextError);
|
|
3908
4361
|
onErrorRef.current?.(nextError);
|
|
3909
4362
|
}
|
|
3910
4363
|
return null;
|
|
3911
4364
|
} finally {
|
|
3912
|
-
if (
|
|
4365
|
+
if (request.isCurrent(currentRequest)) setLoading(false);
|
|
3913
4366
|
}
|
|
3914
4367
|
},
|
|
3915
|
-
[]
|
|
4368
|
+
[request]
|
|
3916
4369
|
);
|
|
3917
|
-
const loadBuffer =
|
|
4370
|
+
const loadBuffer = useCallback13(
|
|
3918
4371
|
async (buffer) => {
|
|
3919
|
-
const
|
|
3920
|
-
return loadBufferWithRequest(buffer,
|
|
4372
|
+
const currentRequest = request.begin();
|
|
4373
|
+
return loadBufferWithRequest(buffer, currentRequest);
|
|
3921
4374
|
},
|
|
3922
|
-
[loadBufferWithRequest]
|
|
4375
|
+
[loadBufferWithRequest, request]
|
|
3923
4376
|
);
|
|
3924
|
-
const loadFile =
|
|
4377
|
+
const loadFile = useCallback13(
|
|
3925
4378
|
async (file) => {
|
|
3926
|
-
const
|
|
4379
|
+
const currentRequest = request.begin();
|
|
3927
4380
|
setLoading(true);
|
|
3928
4381
|
setError(null);
|
|
3929
4382
|
try {
|
|
3930
|
-
return await loadBufferWithRequest(
|
|
4383
|
+
return await loadBufferWithRequest(
|
|
4384
|
+
await readEpubFile(file, limitsRef.current),
|
|
4385
|
+
currentRequest
|
|
4386
|
+
);
|
|
3931
4387
|
} catch (err) {
|
|
3932
|
-
if (
|
|
4388
|
+
if (request.isCurrent(currentRequest)) {
|
|
3933
4389
|
const nextError = toError(err);
|
|
3934
4390
|
setError(nextError);
|
|
3935
4391
|
onErrorRef.current?.(nextError);
|
|
@@ -3938,17 +4394,17 @@ function useEditableEpub(options = {}) {
|
|
|
3938
4394
|
return null;
|
|
3939
4395
|
}
|
|
3940
4396
|
},
|
|
3941
|
-
[loadBufferWithRequest]
|
|
4397
|
+
[loadBufferWithRequest, request]
|
|
3942
4398
|
);
|
|
3943
|
-
const loadUrl =
|
|
4399
|
+
const loadUrl = useCallback13(
|
|
3944
4400
|
async (url) => {
|
|
3945
|
-
const
|
|
4401
|
+
const currentRequest = request.begin();
|
|
3946
4402
|
setLoading(true);
|
|
3947
4403
|
setError(null);
|
|
3948
4404
|
try {
|
|
3949
|
-
return await loadBufferWithRequest(await fetchEpubBuffer(url),
|
|
4405
|
+
return await loadBufferWithRequest(await fetchEpubBuffer(url), currentRequest);
|
|
3950
4406
|
} catch (err) {
|
|
3951
|
-
if (
|
|
4407
|
+
if (request.isCurrent(currentRequest)) {
|
|
3952
4408
|
const nextError = toError(err);
|
|
3953
4409
|
setError(nextError);
|
|
3954
4410
|
onErrorRef.current?.(nextError);
|
|
@@ -3957,24 +4413,32 @@ function useEditableEpub(options = {}) {
|
|
|
3957
4413
|
return null;
|
|
3958
4414
|
}
|
|
3959
4415
|
},
|
|
3960
|
-
[loadBufferWithRequest]
|
|
4416
|
+
[loadBufferWithRequest, request]
|
|
3961
4417
|
);
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
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]);
|
|
3965
4429
|
const book = editor?.book ?? null;
|
|
3966
4430
|
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3967
|
-
const previewBook =
|
|
4431
|
+
const previewBook = useMemo11(() => {
|
|
3968
4432
|
void revision;
|
|
3969
|
-
return book ? cloneEditableEpubBook2(book) : null;
|
|
3970
|
-
}, [book, revision]);
|
|
4433
|
+
return book && editor ? markBookIdentity(cloneEditableEpubBook2(book), editor) : null;
|
|
4434
|
+
}, [book, editor, revision]);
|
|
3971
4435
|
const history = editor?.history ?? null;
|
|
3972
|
-
const setSelection =
|
|
4436
|
+
const setSelection = useCallback13((nextSelection) => {
|
|
3973
4437
|
const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
|
|
3974
4438
|
selectionRef.current = next;
|
|
3975
4439
|
setSelectionState(next);
|
|
3976
4440
|
}, []);
|
|
3977
|
-
const updateParagraph =
|
|
4441
|
+
const updateParagraph = useCallback13(
|
|
3978
4442
|
(text, inlineAnnotations) => {
|
|
3979
4443
|
const current = editorRef.current;
|
|
3980
4444
|
if (!current) return;
|
|
@@ -3984,37 +4448,38 @@ function useEditableEpub(options = {}) {
|
|
|
3984
4448
|
},
|
|
3985
4449
|
[]
|
|
3986
4450
|
);
|
|
3987
|
-
const setInlineAnnotations =
|
|
4451
|
+
const setInlineAnnotations = useCallback13((inlineAnnotations) => {
|
|
3988
4452
|
const current = editorRef.current;
|
|
3989
4453
|
if (!current) return;
|
|
3990
4454
|
const { chapter, paragraph } = selectionRef.current;
|
|
3991
4455
|
current.setInlineAnnotations(chapter, paragraph, inlineAnnotations);
|
|
3992
4456
|
setRevision((value) => value + 1);
|
|
3993
4457
|
}, []);
|
|
3994
|
-
const addImage =
|
|
4458
|
+
const addImage = useCallback13((image) => {
|
|
3995
4459
|
const current = editorRef.current;
|
|
3996
4460
|
if (!current) return;
|
|
3997
4461
|
current.addImage(selectionRef.current.chapter, image);
|
|
3998
4462
|
setRevision((value) => value + 1);
|
|
3999
4463
|
}, []);
|
|
4000
|
-
const undo =
|
|
4464
|
+
const undo = useCallback13(() => {
|
|
4001
4465
|
const current = editorRef.current;
|
|
4002
4466
|
if (!current) return false;
|
|
4003
4467
|
const changed = current.undo();
|
|
4004
4468
|
if (changed) setRevision((value) => value + 1);
|
|
4005
4469
|
return changed;
|
|
4006
4470
|
}, []);
|
|
4007
|
-
const redo =
|
|
4471
|
+
const redo = useCallback13(() => {
|
|
4008
4472
|
const current = editorRef.current;
|
|
4009
4473
|
if (!current) return false;
|
|
4010
4474
|
const changed = current.redo();
|
|
4011
4475
|
if (changed) setRevision((value) => value + 1);
|
|
4012
4476
|
return changed;
|
|
4013
4477
|
}, []);
|
|
4014
|
-
const exportEpub =
|
|
4478
|
+
const exportEpub = useCallback13(
|
|
4015
4479
|
async (exportOptions) => {
|
|
4016
4480
|
const current = editorRef.current;
|
|
4017
4481
|
if (!current) return null;
|
|
4482
|
+
exportsInFlightRef.current++;
|
|
4018
4483
|
setExporting(true);
|
|
4019
4484
|
try {
|
|
4020
4485
|
const buffer = await current.export(
|
|
@@ -4023,7 +4488,7 @@ function useEditableEpub(options = {}) {
|
|
|
4023
4488
|
onExportRef.current?.(buffer);
|
|
4024
4489
|
return buffer;
|
|
4025
4490
|
} finally {
|
|
4026
|
-
setExporting(
|
|
4491
|
+
setExporting(--exportsInFlightRef.current > 0);
|
|
4027
4492
|
}
|
|
4028
4493
|
},
|
|
4029
4494
|
[]
|
|
@@ -4056,263 +4521,12 @@ function withAssetResolver(options, resolver) {
|
|
|
4056
4521
|
return { ...options, assetResolver: resolver };
|
|
4057
4522
|
}
|
|
4058
4523
|
|
|
4059
|
-
// src/useEpubProject.ts
|
|
4060
|
-
import {
|
|
4061
|
-
EpubProject as EpubProject2,
|
|
4062
|
-
parseEpub as parseEpub2
|
|
4063
|
-
} from "@libraz/mejiro/epub";
|
|
4064
|
-
import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef18, useState as useState16 } from "react";
|
|
4065
|
-
function useEpubProject(options = {}) {
|
|
4066
|
-
const defaultTitle = options.defaultChapterTitle;
|
|
4067
|
-
const defaultBody = options.defaultChapterBody;
|
|
4068
|
-
const [metadata, setMetadataState] = useState16(
|
|
4069
|
-
() => initialMetadata(options.metadata)
|
|
4070
|
-
);
|
|
4071
|
-
const [chapters, setChaptersState] = useState16(
|
|
4072
|
-
() => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
|
|
4073
|
-
);
|
|
4074
|
-
const [selectedChapter, setSelectedChapterState] = useState16(0);
|
|
4075
|
-
const [cover, setCoverState] = useState16(options.cover ?? null);
|
|
4076
|
-
const [assets, setAssetsState] = useState16(options.assets ?? []);
|
|
4077
|
-
const [previewBook, setPreviewBook] = useState16(null);
|
|
4078
|
-
const [previewError, setPreviewError] = useState16(null);
|
|
4079
|
-
const [previewing, setPreviewing] = useState16(false);
|
|
4080
|
-
const previewRequestIdRef = useRef18(0);
|
|
4081
|
-
const onPreviewRef = useRef18(options.onPreview);
|
|
4082
|
-
const onExportRef = useRef18(options.onExport);
|
|
4083
|
-
onPreviewRef.current = options.onPreview;
|
|
4084
|
-
onExportRef.current = options.onExport;
|
|
4085
|
-
const assetResolverRef = useRef18(options.assetResolver);
|
|
4086
|
-
assetResolverRef.current = options.assetResolver;
|
|
4087
|
-
const metadataRef = useRef18(metadata);
|
|
4088
|
-
metadataRef.current = metadata;
|
|
4089
|
-
const chaptersRef = useRef18(chapters);
|
|
4090
|
-
chaptersRef.current = chapters;
|
|
4091
|
-
const selectedRef = useRef18(selectedChapter);
|
|
4092
|
-
selectedRef.current = selectedChapter;
|
|
4093
|
-
const coverRef = useRef18(cover);
|
|
4094
|
-
coverRef.current = cover;
|
|
4095
|
-
const assetsRef = useRef18(assets);
|
|
4096
|
-
assetsRef.current = assets;
|
|
4097
|
-
const buildProject = useCallback12(() => {
|
|
4098
|
-
const currentCover = coverRef.current;
|
|
4099
|
-
const project = EpubProject2.fromManuscript({
|
|
4100
|
-
metadata: metadataRef.current,
|
|
4101
|
-
includeTitlePage: false,
|
|
4102
|
-
includeTitleInFirstChapter: true,
|
|
4103
|
-
chapters: chaptersRef.current.map((chapter) => ({
|
|
4104
|
-
id: chapter.id,
|
|
4105
|
-
title: chapter.title || "Untitled",
|
|
4106
|
-
body: chapter.body
|
|
4107
|
-
})),
|
|
4108
|
-
...currentCover ? { cover: currentCover } : {}
|
|
4109
|
-
});
|
|
4110
|
-
for (const asset of assetsRef.current) project.addAsset(asset);
|
|
4111
|
-
return project;
|
|
4112
|
-
}, []);
|
|
4113
|
-
const exportOptions = useCallback12((signal) => {
|
|
4114
|
-
const resolver = assetResolverRef.current;
|
|
4115
|
-
return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
|
|
4116
|
-
}, []);
|
|
4117
|
-
useEffect14(() => {
|
|
4118
|
-
const requestId = ++previewRequestIdRef.current;
|
|
4119
|
-
const controller = new AbortController();
|
|
4120
|
-
let cancelled = false;
|
|
4121
|
-
setPreviewing(true);
|
|
4122
|
-
const timer = setTimeout(() => {
|
|
4123
|
-
void (async () => {
|
|
4124
|
-
try {
|
|
4125
|
-
const book = await parseEpub2(
|
|
4126
|
-
await buildProject().export(exportOptions(controller.signal))
|
|
4127
|
-
);
|
|
4128
|
-
if (cancelled || requestId !== previewRequestIdRef.current) return;
|
|
4129
|
-
setPreviewBook(book);
|
|
4130
|
-
setPreviewError(null);
|
|
4131
|
-
onPreviewRef.current?.(book);
|
|
4132
|
-
} catch (err) {
|
|
4133
|
-
if (!cancelled && requestId === previewRequestIdRef.current) {
|
|
4134
|
-
setPreviewError(toError(err));
|
|
4135
|
-
}
|
|
4136
|
-
} finally {
|
|
4137
|
-
if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
|
|
4138
|
-
}
|
|
4139
|
-
})();
|
|
4140
|
-
}, options.debounceMs ?? 250);
|
|
4141
|
-
return () => {
|
|
4142
|
-
cancelled = true;
|
|
4143
|
-
previewRequestIdRef.current++;
|
|
4144
|
-
clearTimeout(timer);
|
|
4145
|
-
controller.abort();
|
|
4146
|
-
};
|
|
4147
|
-
}, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
|
|
4148
|
-
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
4149
|
-
const setMetadata = useCallback12((patch) => {
|
|
4150
|
-
const next = mergeMetadata(metadataRef.current, patch);
|
|
4151
|
-
metadataRef.current = next;
|
|
4152
|
-
setMetadataState(next);
|
|
4153
|
-
}, []);
|
|
4154
|
-
const commit = useCallback12((nextChapters, nextSelected) => {
|
|
4155
|
-
chaptersRef.current = nextChapters;
|
|
4156
|
-
selectedRef.current = nextSelected;
|
|
4157
|
-
setChaptersState(nextChapters);
|
|
4158
|
-
setSelectedChapterState(nextSelected);
|
|
4159
|
-
}, []);
|
|
4160
|
-
const setChapters = useCallback12(
|
|
4161
|
-
(next) => {
|
|
4162
|
-
const normalized = next.length ? next : [defaultChapter3([], defaultTitle, defaultBody)];
|
|
4163
|
-
const prev = selectedRef.current;
|
|
4164
|
-
const selectedId = chaptersRef.current[prev]?.id;
|
|
4165
|
-
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
4166
|
-
commit(
|
|
4167
|
-
normalized,
|
|
4168
|
-
nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
|
|
4169
|
-
);
|
|
4170
|
-
},
|
|
4171
|
-
[commit, defaultBody, defaultTitle]
|
|
4172
|
-
);
|
|
4173
|
-
const setCover = useCallback12((asset) => {
|
|
4174
|
-
coverRef.current = asset;
|
|
4175
|
-
setCoverState(asset);
|
|
4176
|
-
}, []);
|
|
4177
|
-
const setAssets = useCallback12((next) => {
|
|
4178
|
-
assetsRef.current = next;
|
|
4179
|
-
setAssetsState(next);
|
|
4180
|
-
}, []);
|
|
4181
|
-
const setSelectedChapter = useCallback12((index) => {
|
|
4182
|
-
const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
|
|
4183
|
-
selectedRef.current = next;
|
|
4184
|
-
setSelectedChapterState(next);
|
|
4185
|
-
}, []);
|
|
4186
|
-
const patchChapter = useCallback12((index, patch) => {
|
|
4187
|
-
const next = chaptersRef.current.map(
|
|
4188
|
-
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
4189
|
-
);
|
|
4190
|
-
chaptersRef.current = next;
|
|
4191
|
-
setChaptersState(next);
|
|
4192
|
-
}, []);
|
|
4193
|
-
const addChapter = useCallback12(
|
|
4194
|
-
(chapter = {}) => {
|
|
4195
|
-
const current = chaptersRef.current;
|
|
4196
|
-
const generated = defaultChapter3(current, defaultTitle, defaultBody);
|
|
4197
|
-
const next = [
|
|
4198
|
-
...current,
|
|
4199
|
-
{
|
|
4200
|
-
id: chapter.id ?? generated.id,
|
|
4201
|
-
title: chapter.title ?? generated.title,
|
|
4202
|
-
body: chapter.body ?? generated.body
|
|
4203
|
-
}
|
|
4204
|
-
];
|
|
4205
|
-
commit(next, next.length - 1);
|
|
4206
|
-
},
|
|
4207
|
-
[commit, defaultBody, defaultTitle]
|
|
4208
|
-
);
|
|
4209
|
-
const removeChapter = useCallback12(
|
|
4210
|
-
(index = selectedRef.current) => {
|
|
4211
|
-
const current = chaptersRef.current;
|
|
4212
|
-
if (current.length <= 1) return;
|
|
4213
|
-
const next = current.filter((_, chapterIndex) => chapterIndex !== index);
|
|
4214
|
-
const prev = selectedRef.current;
|
|
4215
|
-
let nextSelected;
|
|
4216
|
-
if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
|
|
4217
|
-
else if (index < prev) nextSelected = prev - 1;
|
|
4218
|
-
else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
|
|
4219
|
-
commit(next, nextSelected);
|
|
4220
|
-
},
|
|
4221
|
-
[commit]
|
|
4222
|
-
);
|
|
4223
|
-
const reorderChapters = useCallback12(
|
|
4224
|
-
(from, to) => {
|
|
4225
|
-
const current = chaptersRef.current;
|
|
4226
|
-
if (from < 0 || from >= current.length) return;
|
|
4227
|
-
const next = [...current];
|
|
4228
|
-
const [moved] = next.splice(from, 1);
|
|
4229
|
-
const target = Math.max(0, Math.min(next.length, to));
|
|
4230
|
-
next.splice(target, 0, moved);
|
|
4231
|
-
const prev = selectedRef.current;
|
|
4232
|
-
let nextSelected = prev;
|
|
4233
|
-
if (prev === from) nextSelected = target;
|
|
4234
|
-
else if (from < prev && target >= prev) nextSelected = prev - 1;
|
|
4235
|
-
else if (from > prev && target <= prev) nextSelected = prev + 1;
|
|
4236
|
-
commit(next, nextSelected);
|
|
4237
|
-
},
|
|
4238
|
-
[commit]
|
|
4239
|
-
);
|
|
4240
|
-
const exportEpub = useCallback12(async () => {
|
|
4241
|
-
const buffer = await buildProject().export(exportOptions());
|
|
4242
|
-
onExportRef.current?.(buffer);
|
|
4243
|
-
return buffer;
|
|
4244
|
-
}, [buildProject, exportOptions]);
|
|
4245
|
-
return useMemo11(
|
|
4246
|
-
() => ({
|
|
4247
|
-
metadata,
|
|
4248
|
-
chapters,
|
|
4249
|
-
selectedChapter,
|
|
4250
|
-
currentChapter,
|
|
4251
|
-
cover,
|
|
4252
|
-
assets,
|
|
4253
|
-
previewBook,
|
|
4254
|
-
previewError,
|
|
4255
|
-
previewing,
|
|
4256
|
-
setMetadata,
|
|
4257
|
-
setChapters,
|
|
4258
|
-
setSelectedChapter,
|
|
4259
|
-
setCover,
|
|
4260
|
-
setAssets,
|
|
4261
|
-
patchChapter,
|
|
4262
|
-
addChapter,
|
|
4263
|
-
removeChapter,
|
|
4264
|
-
reorderChapters,
|
|
4265
|
-
buildProject,
|
|
4266
|
-
exportEpub
|
|
4267
|
-
}),
|
|
4268
|
-
[
|
|
4269
|
-
addChapter,
|
|
4270
|
-
assets,
|
|
4271
|
-
buildProject,
|
|
4272
|
-
chapters,
|
|
4273
|
-
cover,
|
|
4274
|
-
currentChapter,
|
|
4275
|
-
exportEpub,
|
|
4276
|
-
metadata,
|
|
4277
|
-
patchChapter,
|
|
4278
|
-
previewBook,
|
|
4279
|
-
previewError,
|
|
4280
|
-
previewing,
|
|
4281
|
-
reorderChapters,
|
|
4282
|
-
removeChapter,
|
|
4283
|
-
selectedChapter,
|
|
4284
|
-
setAssets,
|
|
4285
|
-
setChapters,
|
|
4286
|
-
setCover,
|
|
4287
|
-
setMetadata,
|
|
4288
|
-
setSelectedChapter
|
|
4289
|
-
]
|
|
4290
|
-
);
|
|
4291
|
-
}
|
|
4292
|
-
function mergeMetadata(current, patch) {
|
|
4293
|
-
const next = { ...current, ...patch };
|
|
4294
|
-
if (!next.identifier?.trim()) next.identifier = current.identifier;
|
|
4295
|
-
return next;
|
|
4296
|
-
}
|
|
4297
|
-
function initialMetadata(patch = {}) {
|
|
4298
|
-
const metadata = mergeDefined(
|
|
4299
|
-
{ title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
|
|
4300
|
-
patch
|
|
4301
|
-
);
|
|
4302
|
-
if (!metadata.identifier?.trim()) metadata.identifier = `urn:uuid:${crypto.randomUUID()}`;
|
|
4303
|
-
return metadata;
|
|
4304
|
-
}
|
|
4305
|
-
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" : "") {
|
|
4306
|
-
const index = existing.length;
|
|
4307
|
-
return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
4524
|
// src/useImageOverlay.ts
|
|
4311
4525
|
import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
|
|
4312
4526
|
import {
|
|
4313
|
-
useCallback as
|
|
4314
|
-
useEffect as
|
|
4315
|
-
useRef as
|
|
4527
|
+
useCallback as useCallback14,
|
|
4528
|
+
useEffect as useEffect16,
|
|
4529
|
+
useRef as useRef20,
|
|
4316
4530
|
useState as useState17
|
|
4317
4531
|
} from "react";
|
|
4318
4532
|
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
@@ -4322,33 +4536,33 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4322
4536
|
const defY = options?.defaultY ?? 100;
|
|
4323
4537
|
const margin = options?.margin;
|
|
4324
4538
|
const [imageRect, setImageRect] = useState17(null);
|
|
4325
|
-
const rectRef =
|
|
4539
|
+
const rectRef = useRef20(imageRect);
|
|
4326
4540
|
rectRef.current = imageRect;
|
|
4327
|
-
const layoutRef =
|
|
4541
|
+
const layoutRef = useRef20(layout);
|
|
4328
4542
|
layoutRef.current = layout;
|
|
4329
|
-
const spreadRef =
|
|
4543
|
+
const spreadRef = useRef20(spreadIdx);
|
|
4330
4544
|
spreadRef.current = spreadIdx;
|
|
4331
|
-
const onUpdateRef =
|
|
4545
|
+
const onUpdateRef = useRef20(onUpdate);
|
|
4332
4546
|
onUpdateRef.current = onUpdate;
|
|
4333
|
-
const marginRef =
|
|
4547
|
+
const marginRef = useRef20(margin);
|
|
4334
4548
|
marginRef.current = margin;
|
|
4335
|
-
const activeDragCleanupsRef =
|
|
4336
|
-
|
|
4549
|
+
const activeDragCleanupsRef = useRef20(/* @__PURE__ */ new Set());
|
|
4550
|
+
useEffect16(
|
|
4337
4551
|
() => () => {
|
|
4338
4552
|
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
4339
4553
|
activeDragCleanupsRef.current.clear();
|
|
4340
4554
|
},
|
|
4341
4555
|
[]
|
|
4342
4556
|
);
|
|
4343
|
-
const syncToLayout =
|
|
4557
|
+
const syncToLayout = useCallback14((rect) => {
|
|
4344
4558
|
const lo = layoutRef.current;
|
|
4345
4559
|
if (!lo) return;
|
|
4346
4560
|
const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: marginRef.current }] : void 0;
|
|
4347
4561
|
const spread = lo.syncImages(spreadRef.current, images);
|
|
4348
4562
|
onUpdateRef.current(spread);
|
|
4349
4563
|
}, []);
|
|
4350
|
-
const syncedRef =
|
|
4351
|
-
|
|
4564
|
+
const syncedRef = useRef20({ layout, spreadIdx, margin });
|
|
4565
|
+
useEffect16(() => {
|
|
4352
4566
|
const prev = syncedRef.current;
|
|
4353
4567
|
syncedRef.current = { layout, spreadIdx, margin };
|
|
4354
4568
|
if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
|
|
@@ -4358,7 +4572,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4358
4572
|
}
|
|
4359
4573
|
syncToLayout(rectRef.current);
|
|
4360
4574
|
}, [layout, spreadIdx, margin, syncToLayout]);
|
|
4361
|
-
const toggleImage =
|
|
4575
|
+
const toggleImage = useCallback14(() => {
|
|
4362
4576
|
if (rectRef.current) {
|
|
4363
4577
|
for (const cancel of [...activeDragCleanupsRef.current]) cancel();
|
|
4364
4578
|
rectRef.current = null;
|
|
@@ -4371,7 +4585,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4371
4585
|
syncToLayout(r);
|
|
4372
4586
|
}
|
|
4373
4587
|
}, [defX, defY, defW, defH, syncToLayout]);
|
|
4374
|
-
const applyRect =
|
|
4588
|
+
const applyRect = useCallback14(
|
|
4375
4589
|
(rect) => {
|
|
4376
4590
|
if (rectRef.current === null) return;
|
|
4377
4591
|
rectRef.current = rect;
|
|
@@ -4380,7 +4594,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4380
4594
|
},
|
|
4381
4595
|
[syncToLayout]
|
|
4382
4596
|
);
|
|
4383
|
-
const onOverlayPointerDown =
|
|
4597
|
+
const onOverlayPointerDown = useCallback14(
|
|
4384
4598
|
(e) => {
|
|
4385
4599
|
if (!isPrimaryPointerPress3(e)) return;
|
|
4386
4600
|
e.preventDefault();
|
|
@@ -4402,7 +4616,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4402
4616
|
},
|
|
4403
4617
|
[applyRect]
|
|
4404
4618
|
);
|
|
4405
|
-
const onResizePointerDown =
|
|
4619
|
+
const onResizePointerDown = useCallback14(
|
|
4406
4620
|
(e) => {
|
|
4407
4621
|
if (!isPrimaryPointerPress3(e)) return;
|
|
4408
4622
|
e.preventDefault();
|
|
@@ -4435,7 +4649,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
4435
4649
|
}
|
|
4436
4650
|
|
|
4437
4651
|
// src/useLibrary.ts
|
|
4438
|
-
import { useCallback as
|
|
4652
|
+
import { useCallback as useCallback15, useEffect as useEffect17, useMemo as useMemo12, useState as useState18 } from "react";
|
|
4439
4653
|
function resolveCurrentId(volumes, currentId, preferredId) {
|
|
4440
4654
|
if (volumes.length === 0) return null;
|
|
4441
4655
|
if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
|
|
@@ -4449,7 +4663,7 @@ function useLibrary(options) {
|
|
|
4449
4663
|
);
|
|
4450
4664
|
const [navigated, setNavigated] = useState18(false);
|
|
4451
4665
|
const resolvedId = resolveCurrentId(volumes, currentId, navigated ? void 0 : initialVolumeId);
|
|
4452
|
-
|
|
4666
|
+
useEffect17(() => {
|
|
4453
4667
|
if (resolvedId === currentId) return;
|
|
4454
4668
|
setCurrentId(resolvedId);
|
|
4455
4669
|
const volume = volumes.find((v) => v.id === resolvedId);
|
|
@@ -4463,7 +4677,7 @@ function useLibrary(options) {
|
|
|
4463
4677
|
() => index >= 0 && index < volumes.length ? volumes[index] : null,
|
|
4464
4678
|
[index, volumes]
|
|
4465
4679
|
);
|
|
4466
|
-
const activate =
|
|
4680
|
+
const activate = useCallback15(
|
|
4467
4681
|
(i) => {
|
|
4468
4682
|
const v = volumes[i];
|
|
4469
4683
|
if (!v) return;
|
|
@@ -4473,17 +4687,17 @@ function useLibrary(options) {
|
|
|
4473
4687
|
},
|
|
4474
4688
|
[volumes, onChange]
|
|
4475
4689
|
);
|
|
4476
|
-
const next =
|
|
4690
|
+
const next = useCallback15(() => {
|
|
4477
4691
|
if (index < 0) return;
|
|
4478
4692
|
const ni = Math.min(volumes.length - 1, index + 1);
|
|
4479
4693
|
if (ni !== index) activate(ni);
|
|
4480
4694
|
}, [index, activate, volumes.length]);
|
|
4481
|
-
const prev =
|
|
4695
|
+
const prev = useCallback15(() => {
|
|
4482
4696
|
if (index < 0) return;
|
|
4483
4697
|
const ni = Math.max(0, index - 1);
|
|
4484
4698
|
if (ni !== index) activate(ni);
|
|
4485
4699
|
}, [index, activate]);
|
|
4486
|
-
const goTo =
|
|
4700
|
+
const goTo = useCallback15(
|
|
4487
4701
|
(id) => {
|
|
4488
4702
|
const ni = volumes.findIndex((v) => v.id === id);
|
|
4489
4703
|
if (ni < 0) return;
|
|
@@ -4495,19 +4709,27 @@ function useLibrary(options) {
|
|
|
4495
4709
|
}
|
|
4496
4710
|
|
|
4497
4711
|
// src/useManuscriptLayout.ts
|
|
4498
|
-
import { useMemo as useMemo13 } 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
|
+
}
|
|
4499
4716
|
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
4500
4717
|
const dialect = options.dialect ?? "mejiro";
|
|
4718
|
+
const signature = chapterSignature(chapter);
|
|
4719
|
+
const chapterRef = useRef21(chapter);
|
|
4720
|
+
chapterRef.current = chapter;
|
|
4501
4721
|
const source = useMemo13(() => {
|
|
4502
|
-
if (
|
|
4722
|
+
if (signature === null) return null;
|
|
4503
4723
|
return {
|
|
4504
4724
|
measure: (el) => book.computePageSize(el),
|
|
4505
4725
|
layout: async () => {
|
|
4506
|
-
const
|
|
4726
|
+
const current = chapterRef.current;
|
|
4727
|
+
if (!current) return null;
|
|
4728
|
+
const layouts = await book.layoutManuscript({ chapters: [current], dialect });
|
|
4507
4729
|
return layouts.values().next().value ?? null;
|
|
4508
4730
|
}
|
|
4509
4731
|
};
|
|
4510
|
-
}, [book,
|
|
4732
|
+
}, [book, signature, dialect]);
|
|
4511
4733
|
return useSurfaceLayout(source, surface, {
|
|
4512
4734
|
...options,
|
|
4513
4735
|
enableResize: options.enableResize ?? true,
|
|
@@ -4520,28 +4742,30 @@ import {
|
|
|
4520
4742
|
parseReadingPosition,
|
|
4521
4743
|
serializeReadingPosition
|
|
4522
4744
|
} from "@libraz/mejiro";
|
|
4523
|
-
import { useCallback as
|
|
4745
|
+
import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef22, useState as useState19 } from "react";
|
|
4524
4746
|
function useReadingPosition(options) {
|
|
4525
4747
|
const { key, throttleMs = 250 } = options;
|
|
4526
4748
|
const storage = options.storage ?? resolveDefaultStorage();
|
|
4527
|
-
const
|
|
4528
|
-
() => readStorage(storage, key, parseReadingPosition, null)
|
|
4529
|
-
);
|
|
4530
|
-
const storageRef = useRef20(storage);
|
|
4749
|
+
const storageRef = useRef22(storage);
|
|
4531
4750
|
storageRef.current = storage;
|
|
4532
|
-
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);
|
|
4533
4760
|
if (!pendingRef.current) pendingRef.current = createPendingWrite();
|
|
4534
4761
|
const pending = pendingRef.current;
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
return pending.flush;
|
|
4539
|
-
}, [key, pending]);
|
|
4540
|
-
const onChangeRef = useRef20(options.onChange);
|
|
4762
|
+
useEffect18(() => flushOnPageHide(pending.flush), [pending]);
|
|
4763
|
+
useEffect18(() => pending.flush, [key, pending]);
|
|
4764
|
+
const onChangeRef = useRef22(options.onChange);
|
|
4541
4765
|
onChangeRef.current = options.onChange;
|
|
4542
|
-
const save =
|
|
4766
|
+
const save = useCallback16(
|
|
4543
4767
|
(next) => {
|
|
4544
|
-
|
|
4768
|
+
setHydrated({ key, value: next });
|
|
4545
4769
|
const currentStorage = storageRef.current;
|
|
4546
4770
|
if (currentStorage) {
|
|
4547
4771
|
pending.schedule(() => {
|
|
@@ -4552,8 +4776,8 @@ function useReadingPosition(options) {
|
|
|
4552
4776
|
},
|
|
4553
4777
|
[key, throttleMs, pending]
|
|
4554
4778
|
);
|
|
4555
|
-
const clear =
|
|
4556
|
-
|
|
4779
|
+
const clear = useCallback16(() => {
|
|
4780
|
+
setHydrated({ key, value: null });
|
|
4557
4781
|
pending.cancel();
|
|
4558
4782
|
removeStorage(storageRef.current, key);
|
|
4559
4783
|
onChangeRef.current?.(null);
|