@libraz/mejiro-react 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -63
- package/dist/index.d.ts +1546 -38
- package/dist/index.js +3685 -37
- package/package.json +35 -8
package/dist/index.js
CHANGED
|
@@ -1,27 +1,357 @@
|
|
|
1
|
+
// src/i18n.tsx
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var enMessages = {
|
|
5
|
+
logoSubtitle: "Vertical Reader",
|
|
6
|
+
openButton: "Open",
|
|
7
|
+
settingsButton: "Settings",
|
|
8
|
+
imageButton: "Image",
|
|
9
|
+
imageRemoveButton: "Remove image",
|
|
10
|
+
loading: "Loading...",
|
|
11
|
+
prevSpread: "Previous spread",
|
|
12
|
+
nextSpread: "Next spread",
|
|
13
|
+
settingsFont: "Font",
|
|
14
|
+
settingsSize: "Size",
|
|
15
|
+
settingsSizeDown: "Decrease font size",
|
|
16
|
+
settingsSizeUp: "Increase font size",
|
|
17
|
+
settingsLayout: "Layout",
|
|
18
|
+
settingsKinsoku: "Kinsoku",
|
|
19
|
+
settingsHanging: "Hanging",
|
|
20
|
+
settingsLineSpacing: "Line spacing",
|
|
21
|
+
settingsStrict: "Strict",
|
|
22
|
+
settingsLoose: "Loose",
|
|
23
|
+
toggleOn: "On",
|
|
24
|
+
toggleOff: "Off",
|
|
25
|
+
dropZoneTitle: "Drop an EPUB here or click to open",
|
|
26
|
+
dropZoneHint: ".epub files only",
|
|
27
|
+
tocTitle: "Contents",
|
|
28
|
+
tocSearchPlaceholder: "Search chapters\u2026",
|
|
29
|
+
tocEmpty: 'No chapters match "{query}".',
|
|
30
|
+
shelfTitle: "Library",
|
|
31
|
+
chapterN: "Chapter {n}",
|
|
32
|
+
spreadAnnouncement: "Spread {spread} of {total}",
|
|
33
|
+
editorPreviewSubtitle: "Editor Preview",
|
|
34
|
+
editorTitle: "EPUB Editor",
|
|
35
|
+
editorNoBookLoaded: "No book loaded",
|
|
36
|
+
editorParagraphs: "Paragraphs",
|
|
37
|
+
editorProofread: "Proofread",
|
|
38
|
+
editorApplyText: "Apply text",
|
|
39
|
+
editorRuby: "Ruby",
|
|
40
|
+
editorRubyHint: "Select base text in the textarea, then type the ruby reading below.",
|
|
41
|
+
editorRubyRange: "Base: {start}-{end} ({count} chars)",
|
|
42
|
+
editorRubyPlaceholder: "furigana",
|
|
43
|
+
editorApplyRuby: "Apply ruby",
|
|
44
|
+
editorImages: "Images",
|
|
45
|
+
editorInsertImageAfterParagraph: "Insert image after paragraph",
|
|
46
|
+
editorExportEpub: "Export EPUB",
|
|
47
|
+
manuscriptPreviewSubtitle: "Manuscript Preview",
|
|
48
|
+
manuscriptTitle: "Manuscript EPUB",
|
|
49
|
+
manuscriptRubyHint: "Use \uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B or \u6F22\u5B57\u300A\u304B\u3093\u3058\u300B for ruby.",
|
|
50
|
+
manuscriptMetadata: "Metadata",
|
|
51
|
+
manuscriptChapters: "Chapters",
|
|
52
|
+
manuscriptDraft: "Draft",
|
|
53
|
+
manuscriptChooseCoverImage: "Choose cover image",
|
|
54
|
+
manuscriptDefaultTitle: "New work",
|
|
55
|
+
manuscriptDefaultChapterTitle: "Chapter {n}",
|
|
56
|
+
manuscriptDefaultBody: "This is a ruby example: \uFF5Ckanji\u300Areading\u300B.\n\nPaste your draft here.",
|
|
57
|
+
untitled: "Untitled",
|
|
58
|
+
manuscriptAddChapter: "Add chapter",
|
|
59
|
+
manuscriptRemove: "Remove",
|
|
60
|
+
manuscriptEmphasisDots: "Emphasis dots",
|
|
61
|
+
manuscriptTcy: "Tate-chu-yoko",
|
|
62
|
+
manuscriptEm: "Em",
|
|
63
|
+
manuscriptStrong: "Strong",
|
|
64
|
+
manuscriptReorderHandle: "Reorder {title}"
|
|
65
|
+
};
|
|
66
|
+
var jaMessages = {
|
|
67
|
+
logoSubtitle: "\u7E26\u66F8\u304D\u30EA\u30FC\u30C0\u30FC",
|
|
68
|
+
openButton: "\u958B\u304F",
|
|
69
|
+
settingsButton: "\u8A2D\u5B9A",
|
|
70
|
+
imageButton: "\u753B\u50CF",
|
|
71
|
+
imageRemoveButton: "\u753B\u50CF\u3092\u524A\u9664",
|
|
72
|
+
loading: "\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
|
|
73
|
+
prevSpread: "\u524D\u306E\u898B\u958B\u304D",
|
|
74
|
+
nextSpread: "\u6B21\u306E\u898B\u958B\u304D",
|
|
75
|
+
settingsFont: "\u30D5\u30A9\u30F3\u30C8",
|
|
76
|
+
settingsSize: "\u30B5\u30A4\u30BA",
|
|
77
|
+
settingsSizeDown: "\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA\u3092\u4E0B\u3052\u308B",
|
|
78
|
+
settingsSizeUp: "\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA\u3092\u4E0A\u3052\u308B",
|
|
79
|
+
settingsLayout: "\u30EC\u30A4\u30A2\u30A6\u30C8",
|
|
80
|
+
settingsKinsoku: "\u7981\u5247",
|
|
81
|
+
settingsHanging: "\u3076\u3089\u4E0B\u3052",
|
|
82
|
+
settingsLineSpacing: "\u884C\u9593",
|
|
83
|
+
settingsStrict: "\u53B3\u683C",
|
|
84
|
+
settingsLoose: "\u7DE9\u548C",
|
|
85
|
+
toggleOn: "\u30AA\u30F3",
|
|
86
|
+
toggleOff: "\u30AA\u30D5",
|
|
87
|
+
dropZoneTitle: "EPUB \u3092\u30C9\u30ED\u30C3\u30D7\u307E\u305F\u306F\u30AF\u30EA\u30C3\u30AF\u3067\u958B\u304F",
|
|
88
|
+
dropZoneHint: ".epub \u30D5\u30A1\u30A4\u30EB\u306E\u307F",
|
|
89
|
+
tocTitle: "\u76EE\u6B21",
|
|
90
|
+
tocSearchPlaceholder: "\u7AE0\u3092\u691C\u7D22\u2026",
|
|
91
|
+
tocEmpty: "\u300C{query}\u300D\u306B\u4E00\u81F4\u3059\u308B\u7AE0\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
92
|
+
shelfTitle: "\u672C\u68DA",
|
|
93
|
+
chapterN: "\u7B2C{n}\u7AE0",
|
|
94
|
+
spreadAnnouncement: "{total}\u898B\u958B\u304D\u4E2D{spread}\u898B\u958B\u304D\u76EE",
|
|
95
|
+
editorPreviewSubtitle: "\u7DE8\u96C6\u30D7\u30EC\u30D3\u30E5\u30FC",
|
|
96
|
+
editorTitle: "EPUB \u30A8\u30C7\u30A3\u30BF\u30FC",
|
|
97
|
+
editorNoBookLoaded: "\u672C\u304C\u8AAD\u307F\u8FBC\u307E\u308C\u3066\u3044\u307E\u305B\u3093",
|
|
98
|
+
editorParagraphs: "\u6BB5\u843D",
|
|
99
|
+
editorProofread: "\u6821\u6B63",
|
|
100
|
+
editorApplyText: "\u672C\u6587\u3092\u9069\u7528",
|
|
101
|
+
editorRuby: "\u30EB\u30D3",
|
|
102
|
+
editorRubyHint: "\u30C6\u30AD\u30B9\u30C8\u30A8\u30EA\u30A2\u3067\u89AA\u6587\u5B57\u3092\u9078\u629E\u3057\u3066\u304B\u3089\u3001\u4E0B\u306B\u30EB\u30D3\u3092\u5165\u529B\u3057\u307E\u3059\u3002",
|
|
103
|
+
editorRubyRange: "\u89AA\u6587\u5B57: {start}-{end}\uFF08{count}\u6587\u5B57\uFF09",
|
|
104
|
+
editorRubyPlaceholder: "\u3075\u308A\u304C\u306A",
|
|
105
|
+
editorApplyRuby: "\u30EB\u30D3\u3092\u9069\u7528",
|
|
106
|
+
editorImages: "\u753B\u50CF",
|
|
107
|
+
editorInsertImageAfterParagraph: "\u6BB5\u843D\u306E\u5F8C\u306B\u753B\u50CF\u3092\u633F\u5165",
|
|
108
|
+
editorExportEpub: "EPUB \u3092\u66F8\u304D\u51FA\u3059",
|
|
109
|
+
manuscriptPreviewSubtitle: "\u539F\u7A3F\u30D7\u30EC\u30D3\u30E5\u30FC",
|
|
110
|
+
manuscriptTitle: "\u539F\u7A3F EPUB",
|
|
111
|
+
manuscriptRubyHint: "\u30EB\u30D3\u306B\u306F \uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B \u307E\u305F\u306F \u6F22\u5B57\u300A\u304B\u3093\u3058\u300B \u3092\u4F7F\u3044\u307E\u3059\u3002",
|
|
112
|
+
manuscriptMetadata: "\u30E1\u30BF\u30C7\u30FC\u30BF",
|
|
113
|
+
manuscriptChapters: "\u7AE0",
|
|
114
|
+
manuscriptDraft: "\u539F\u7A3F",
|
|
115
|
+
manuscriptChooseCoverImage: "\u8868\u7D19\u753B\u50CF\u3092\u9078\u629E",
|
|
116
|
+
manuscriptDefaultTitle: "\u65B0\u3057\u3044\u4F5C\u54C1",
|
|
117
|
+
manuscriptDefaultChapterTitle: "\u7B2C{n}\u8A71",
|
|
118
|
+
manuscriptDefaultBody: "\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",
|
|
119
|
+
untitled: "\u7121\u984C",
|
|
120
|
+
manuscriptAddChapter: "\u7AE0\u3092\u8FFD\u52A0",
|
|
121
|
+
manuscriptRemove: "\u524A\u9664",
|
|
122
|
+
manuscriptEmphasisDots: "\u508D\u70B9",
|
|
123
|
+
manuscriptTcy: "\u7E26\u4E2D\u6A2A",
|
|
124
|
+
manuscriptEm: "\u5F37\u8ABF",
|
|
125
|
+
manuscriptStrong: "\u592A\u5B57",
|
|
126
|
+
manuscriptReorderHandle: "{title} \u306E\u9806\u756A\u3092\u5909\u66F4"
|
|
127
|
+
};
|
|
128
|
+
var CATALOGS = {
|
|
129
|
+
en: enMessages,
|
|
130
|
+
ja: jaMessages
|
|
131
|
+
};
|
|
132
|
+
var I18nContext = createContext(enMessages);
|
|
133
|
+
function MejiroI18nProvider({
|
|
134
|
+
locale,
|
|
135
|
+
messages,
|
|
136
|
+
children
|
|
137
|
+
}) {
|
|
138
|
+
const fromContext = useContext(I18nContext);
|
|
139
|
+
const value = useMemo(() => {
|
|
140
|
+
const base = locale != null ? CATALOGS[locale] : fromContext;
|
|
141
|
+
return messages ? { ...base, ...messages } : base;
|
|
142
|
+
}, [locale, messages, fromContext]);
|
|
143
|
+
return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
|
|
144
|
+
}
|
|
145
|
+
function useI18n(options = {}) {
|
|
146
|
+
const fromContext = useContext(I18nContext);
|
|
147
|
+
const { locale, messages } = options;
|
|
148
|
+
return useMemo(() => {
|
|
149
|
+
if (locale == null && messages == null) return fromContext;
|
|
150
|
+
const base = locale != null ? CATALOGS[locale] : fromContext;
|
|
151
|
+
return messages ? { ...base, ...messages } : base;
|
|
152
|
+
}, [locale, messages, fromContext]);
|
|
153
|
+
}
|
|
154
|
+
function resolveMessages(locale, overrides) {
|
|
155
|
+
const base = locale != null ? CATALOGS[locale] : enMessages;
|
|
156
|
+
return overrides ? { ...base, ...overrides } : base;
|
|
157
|
+
}
|
|
158
|
+
function format(template, vars) {
|
|
159
|
+
return template.replace(
|
|
160
|
+
/\{(\w+)\}/g,
|
|
161
|
+
(_, k) => Object.hasOwn(vars, k) ? String(vars[k]) : `{${k}}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/MejiroChapterNav.tsx
|
|
166
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
167
|
+
function textPreview(text, max = 72) {
|
|
168
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
169
|
+
if (compact.length <= max) return compact;
|
|
170
|
+
return `${compact.slice(0, max - 1)}...`;
|
|
171
|
+
}
|
|
172
|
+
function MejiroChapterNav({
|
|
173
|
+
epub,
|
|
174
|
+
chapter,
|
|
175
|
+
onChange,
|
|
176
|
+
variant = "select"
|
|
177
|
+
}) {
|
|
178
|
+
const messages = useI18n();
|
|
179
|
+
if (variant === "panel") {
|
|
180
|
+
return /* @__PURE__ */ jsxs("nav", { className: "mejiro-reader-chapter-panel", "aria-label": messages.tocTitle, children: [
|
|
181
|
+
/* @__PURE__ */ jsxs("div", { className: "mejiro-reader-chapter-panel-head", children: [
|
|
182
|
+
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-panel-kicker", children: messages.tocTitle }),
|
|
183
|
+
/* @__PURE__ */ jsx2("strong", { children: epub.title }),
|
|
184
|
+
epub.author && /* @__PURE__ */ jsx2("span", { children: epub.author })
|
|
185
|
+
] }),
|
|
186
|
+
/* @__PURE__ */ jsx2("ol", { className: "mejiro-reader-chapter-list", children: epub.chapters.map((ch, i) => {
|
|
187
|
+
const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
|
|
188
|
+
const preview = ch.paragraphs.find((p) => !p.headingLevel && p.text.trim())?.text;
|
|
189
|
+
const chapterKey = `${title}-${ch.paragraphs.map((p) => p.text).join("|").slice(0, 120)}`;
|
|
190
|
+
const headings = ch.paragraphs.filter((p) => p.headingLevel && p.text.trim() && p.text.trim() !== title).slice(0, 3);
|
|
191
|
+
return /* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
|
|
192
|
+
"button",
|
|
193
|
+
{
|
|
194
|
+
type: "button",
|
|
195
|
+
className: `mejiro-reader-chapter-card${i === chapter ? " is-active" : ""}`,
|
|
196
|
+
"aria-current": i === chapter ? "true" : void 0,
|
|
197
|
+
onClick: () => onChange(i),
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-number", children: String(i + 1).padStart(2, "0") }),
|
|
200
|
+
/* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
|
|
201
|
+
/* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
|
|
202
|
+
preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
|
|
203
|
+
headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.text)) })
|
|
204
|
+
] })
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
) }, chapterKey);
|
|
208
|
+
}) })
|
|
209
|
+
] });
|
|
210
|
+
}
|
|
211
|
+
return /* @__PURE__ */ jsx2("div", { className: "mejiro-reader-chapter-nav", children: /* @__PURE__ */ jsx2("select", { value: chapter, onChange: (e) => onChange(Number(e.target.value)), children: epub.chapters.map((ch, i) => /* @__PURE__ */ jsx2("option", { value: i, children: ch.title ?? format(messages.chapterN, { n: i + 1 }) }, ch.title ?? `chapter-${i}`)) }) });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/MejiroDropZone.tsx
|
|
215
|
+
import { useRef, useState } from "react";
|
|
216
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
217
|
+
function MejiroDropZone({
|
|
218
|
+
accept = ".epub",
|
|
219
|
+
validateFile,
|
|
220
|
+
onFile,
|
|
221
|
+
children,
|
|
222
|
+
className,
|
|
223
|
+
style
|
|
224
|
+
}) {
|
|
225
|
+
const messages = useI18n();
|
|
226
|
+
const inputRef = useRef(null);
|
|
227
|
+
const [dragover, setDragover] = useState(false);
|
|
228
|
+
const isValid = (file) => {
|
|
229
|
+
if (validateFile) return validateFile(file);
|
|
230
|
+
return file.name.endsWith(".epub");
|
|
231
|
+
};
|
|
232
|
+
const rootClass = ["mejiro-reader-drop-zone", dragover ? "is-dragover" : "", className ?? ""].filter(Boolean).join(" ");
|
|
233
|
+
return /* @__PURE__ */ jsxs2(
|
|
234
|
+
"button",
|
|
235
|
+
{
|
|
236
|
+
type: "button",
|
|
237
|
+
className: rootClass,
|
|
238
|
+
style,
|
|
239
|
+
onClick: () => inputRef.current?.click(),
|
|
240
|
+
onDragOver: (e) => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
setDragover(true);
|
|
243
|
+
},
|
|
244
|
+
onDragLeave: () => setDragover(false),
|
|
245
|
+
onDrop: (e) => {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
setDragover(false);
|
|
248
|
+
const file = e.dataTransfer?.files[0];
|
|
249
|
+
if (file && isValid(file)) onFile(file);
|
|
250
|
+
},
|
|
251
|
+
children: [
|
|
252
|
+
children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
253
|
+
/* @__PURE__ */ jsx3("div", { className: "mejiro-reader-drop-zone-icon", children: "\u{1F4D6}" }),
|
|
254
|
+
/* @__PURE__ */ jsx3("div", { className: "mejiro-reader-drop-zone-text", children: /* @__PURE__ */ jsx3("strong", { children: messages.dropZoneTitle }) }),
|
|
255
|
+
/* @__PURE__ */ jsx3("div", { className: "mejiro-reader-drop-zone-hint", children: messages.dropZoneHint })
|
|
256
|
+
] }),
|
|
257
|
+
/* @__PURE__ */ jsx3(
|
|
258
|
+
"input",
|
|
259
|
+
{
|
|
260
|
+
ref: inputRef,
|
|
261
|
+
type: "file",
|
|
262
|
+
accept,
|
|
263
|
+
hidden: true,
|
|
264
|
+
onChange: (e) => {
|
|
265
|
+
const file = e.target.files?.[0];
|
|
266
|
+
if (file && isValid(file)) onFile(file);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/MejiroEditor.tsx
|
|
276
|
+
import { EditableEpub } from "@libraz/mejiro/epub";
|
|
277
|
+
import { useCallback as useCallback7, useEffect as useEffect7, useRef as useRef10, useState as useState8 } from "react";
|
|
278
|
+
|
|
279
|
+
// src/MejiroReader.tsx
|
|
280
|
+
import { DEFAULT_BOOK_OPTIONS } from "@libraz/mejiro/book";
|
|
281
|
+
import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
|
|
282
|
+
import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
|
|
283
|
+
import {
|
|
284
|
+
forwardRef,
|
|
285
|
+
useCallback as useCallback6,
|
|
286
|
+
useEffect as useEffect6,
|
|
287
|
+
useImperativeHandle,
|
|
288
|
+
useMemo as useMemo4,
|
|
289
|
+
useRef as useRef9,
|
|
290
|
+
useState as useState7
|
|
291
|
+
} from "react";
|
|
292
|
+
|
|
293
|
+
// src/MejiroPageIndicator.tsx
|
|
294
|
+
import { jsxs as jsxs3 } from "react/jsx-runtime";
|
|
295
|
+
function MejiroPageIndicator({ current, total }) {
|
|
296
|
+
return /* @__PURE__ */ jsxs3("div", { className: "mejiro-reader-page-indicator", children: [
|
|
297
|
+
current,
|
|
298
|
+
" / ",
|
|
299
|
+
total
|
|
300
|
+
] });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/MejiroScrollView.tsx
|
|
304
|
+
import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
|
|
305
|
+
import {
|
|
306
|
+
useEffect,
|
|
307
|
+
useLayoutEffect,
|
|
308
|
+
useMemo as useMemo2,
|
|
309
|
+
useRef as useRef2
|
|
310
|
+
} from "react";
|
|
311
|
+
|
|
312
|
+
// src/MejiroPageView.tsx
|
|
313
|
+
import { normalizeFontFamily } from "@libraz/mejiro/browser";
|
|
314
|
+
import { Fragment as Fragment3 } from "react";
|
|
315
|
+
|
|
1
316
|
// src/MejiroPage.tsx
|
|
2
|
-
import {
|
|
317
|
+
import { Fragment as Fragment2 } from "react";
|
|
318
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3
319
|
function renderSegment(segment, key) {
|
|
4
|
-
|
|
5
|
-
|
|
320
|
+
switch (segment.type) {
|
|
321
|
+
case "text":
|
|
322
|
+
return /* @__PURE__ */ jsx4(Fragment2, { children: segment.text }, key);
|
|
323
|
+
case "ruby":
|
|
324
|
+
return /* @__PURE__ */ jsxs4("ruby", { children: [
|
|
325
|
+
segment.base,
|
|
326
|
+
/* @__PURE__ */ jsx4("rt", { children: segment.rubyText })
|
|
327
|
+
] }, key);
|
|
328
|
+
case "emphasis":
|
|
329
|
+
return /* @__PURE__ */ jsx4("span", { className: `mejiro-emphasis mejiro-emphasis--${segment.style}`, children: segment.text }, key);
|
|
330
|
+
case "tcy":
|
|
331
|
+
return /* @__PURE__ */ jsx4("span", { className: "mejiro-tcy", children: segment.text }, key);
|
|
332
|
+
case "em":
|
|
333
|
+
return /* @__PURE__ */ jsx4("em", { children: segment.text }, key);
|
|
334
|
+
case "strong":
|
|
335
|
+
return /* @__PURE__ */ jsx4("strong", { children: segment.text }, key);
|
|
336
|
+
case "link":
|
|
337
|
+
return /* @__PURE__ */ jsx4("a", { href: segment.href, title: segment.title, children: segment.text }, key);
|
|
338
|
+
case "footnote-ref":
|
|
339
|
+
return /* @__PURE__ */ jsx4("a", { className: "mejiro-footnote-ref", href: `#${segment.noteId}`, children: segment.text }, key);
|
|
6
340
|
}
|
|
7
|
-
return /* @__PURE__ */ jsxs("ruby", { children: [
|
|
8
|
-
segment.base,
|
|
9
|
-
/* @__PURE__ */ jsx("rt", { children: segment.rubyText })
|
|
10
|
-
] }, key);
|
|
11
341
|
}
|
|
12
342
|
function renderLine(line, lineIndex) {
|
|
13
343
|
const nodes = [];
|
|
14
344
|
if (lineIndex > 0) {
|
|
15
|
-
nodes.push(/* @__PURE__ */
|
|
345
|
+
nodes.push(/* @__PURE__ */ jsx4("br", {}, `br-${lineIndex}`));
|
|
16
346
|
}
|
|
17
347
|
for (let i = 0; i < line.segments.length; i++) {
|
|
18
|
-
nodes.push(renderSegment(line.segments[i], i));
|
|
348
|
+
nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
|
|
19
349
|
}
|
|
20
350
|
return nodes;
|
|
21
351
|
}
|
|
22
352
|
function MejiroPage({ page, className, style }) {
|
|
23
353
|
const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
|
|
24
|
-
return /* @__PURE__ */
|
|
354
|
+
return /* @__PURE__ */ jsx4("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
|
|
25
355
|
let paraClass = "mejiro-paragraph";
|
|
26
356
|
if (paragraph.headingLevel != null) {
|
|
27
357
|
paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
|
|
@@ -30,19 +360,35 @@ function MejiroPage({ page, className, style }) {
|
|
|
30
360
|
}
|
|
31
361
|
return (
|
|
32
362
|
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
|
|
33
|
-
/* @__PURE__ */
|
|
363
|
+
/* @__PURE__ */ jsx4("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
|
|
34
364
|
);
|
|
35
365
|
}) });
|
|
36
366
|
}
|
|
37
367
|
|
|
38
368
|
// src/MejiroPageView.tsx
|
|
39
|
-
import { jsx as
|
|
369
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
40
370
|
function renderSlotSegment(segment, key) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
371
|
+
switch (segment.type) {
|
|
372
|
+
case "text":
|
|
373
|
+
return /* @__PURE__ */ jsx5(Fragment3, { children: segment.text }, key);
|
|
374
|
+
case "ruby":
|
|
375
|
+
return /* @__PURE__ */ jsxs5("ruby", { children: [
|
|
376
|
+
segment.base,
|
|
377
|
+
/* @__PURE__ */ jsx5("rt", { children: segment.rubyText })
|
|
378
|
+
] }, key);
|
|
379
|
+
case "emphasis":
|
|
380
|
+
return /* @__PURE__ */ jsx5("span", { className: `mejiro-emphasis mejiro-emphasis--${segment.style}`, children: segment.text }, key);
|
|
381
|
+
case "tcy":
|
|
382
|
+
return /* @__PURE__ */ jsx5("span", { className: "mejiro-tcy", children: segment.text }, key);
|
|
383
|
+
case "em":
|
|
384
|
+
return /* @__PURE__ */ jsx5("em", { children: segment.text }, key);
|
|
385
|
+
case "strong":
|
|
386
|
+
return /* @__PURE__ */ jsx5("strong", { children: segment.text }, key);
|
|
387
|
+
case "link":
|
|
388
|
+
return /* @__PURE__ */ jsx5("a", { href: segment.href, title: segment.title, children: segment.text }, key);
|
|
389
|
+
case "footnote-ref":
|
|
390
|
+
return /* @__PURE__ */ jsx5("a", { className: "mejiro-footnote-ref", href: `#${segment.noteId}`, children: segment.text }, key);
|
|
391
|
+
}
|
|
46
392
|
}
|
|
47
393
|
function MejiroPageView({
|
|
48
394
|
result,
|
|
@@ -54,7 +400,8 @@ function MejiroPageView({
|
|
|
54
400
|
}) {
|
|
55
401
|
if (result.hasImages || slotMode) {
|
|
56
402
|
const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
|
|
57
|
-
|
|
403
|
+
const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
|
|
404
|
+
return /* @__PURE__ */ jsx5("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
|
|
58
405
|
const slot = result.slots[i];
|
|
59
406
|
if (!slot || slot.height <= 0) return null;
|
|
60
407
|
const colStyle = {
|
|
@@ -66,38 +413,3039 @@ function MejiroPageView({
|
|
|
66
413
|
top: slot.yStart,
|
|
67
414
|
height: slot.height,
|
|
68
415
|
fontSize: line.fontSize,
|
|
69
|
-
fontFamily,
|
|
416
|
+
fontFamily: fontFamilyCss,
|
|
70
417
|
lineHeight: lineSpacing,
|
|
71
418
|
fontWeight: line.headingLevel != null ? 700 : void 0
|
|
72
419
|
};
|
|
73
420
|
return (
|
|
74
421
|
// biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
|
|
75
|
-
/* @__PURE__ */
|
|
422
|
+
/* @__PURE__ */ jsx5("div", { style: colStyle, children: line.segments.map((seg, si) => renderSlotSegment(seg, `${i}-${si}`)) }, i)
|
|
76
423
|
);
|
|
77
424
|
}) });
|
|
78
425
|
}
|
|
79
|
-
return /* @__PURE__ */
|
|
426
|
+
return /* @__PURE__ */ jsx5(MejiroPage, { page: result.page, className, style });
|
|
80
427
|
}
|
|
81
428
|
|
|
82
|
-
// src/
|
|
429
|
+
// src/MejiroScrollView.tsx
|
|
430
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
431
|
+
function MejiroScrollView({
|
|
432
|
+
layout,
|
|
433
|
+
pageWidth,
|
|
434
|
+
pageHeight,
|
|
435
|
+
contentHeight,
|
|
436
|
+
fontFamily,
|
|
437
|
+
fontSize,
|
|
438
|
+
lineSpacing,
|
|
439
|
+
onVisiblePageChange,
|
|
440
|
+
scrollToPage,
|
|
441
|
+
pageGap = 24
|
|
442
|
+
}) {
|
|
443
|
+
const containerRef = useRef2(null);
|
|
444
|
+
const pageRefs = useRef2([]);
|
|
445
|
+
const onVisiblePageChangeRef = useRef2(onVisiblePageChange);
|
|
446
|
+
onVisiblePageChangeRef.current = onVisiblePageChange;
|
|
447
|
+
const pages = useMemo2(() => {
|
|
448
|
+
const total = layout.totalPages;
|
|
449
|
+
return Array.from({ length: total }, (_, i) => layout.getPage(i));
|
|
450
|
+
}, [layout]);
|
|
451
|
+
const useSlot = true;
|
|
452
|
+
const contentStyle = { height: contentHeight };
|
|
453
|
+
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
|
|
454
|
+
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
455
|
+
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
const container = containerRef.current;
|
|
458
|
+
if (!container) return;
|
|
459
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
460
|
+
let mostVisibleIdx = -1;
|
|
461
|
+
let mostVisibleRatio = 0;
|
|
462
|
+
const observer = new IntersectionObserver(
|
|
463
|
+
(entries) => {
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
const idx = Number(entry.target.dataset.pageIdx);
|
|
466
|
+
if (Number.isNaN(idx)) continue;
|
|
467
|
+
if (entry.intersectionRatio > mostVisibleRatio || idx === mostVisibleIdx) {
|
|
468
|
+
if (entry.isIntersecting) {
|
|
469
|
+
mostVisibleRatio = entry.intersectionRatio;
|
|
470
|
+
mostVisibleIdx = idx;
|
|
471
|
+
} else if (idx === mostVisibleIdx) {
|
|
472
|
+
mostVisibleIdx = -1;
|
|
473
|
+
mostVisibleRatio = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (mostVisibleIdx >= 0) onVisiblePageChangeRef.current?.(mostVisibleIdx);
|
|
478
|
+
},
|
|
479
|
+
{ root: container, threshold: [0.25, 0.5, 0.75] }
|
|
480
|
+
);
|
|
481
|
+
for (const el of pageRefs.current) {
|
|
482
|
+
if (el) observer.observe(el);
|
|
483
|
+
}
|
|
484
|
+
return () => observer.disconnect();
|
|
485
|
+
}, []);
|
|
486
|
+
useLayoutEffect(() => {
|
|
487
|
+
if (scrollToPage == null) return;
|
|
488
|
+
const el = pageRefs.current[scrollToPage];
|
|
489
|
+
if (!(el && containerRef.current)) return;
|
|
490
|
+
containerRef.current.scrollTo({
|
|
491
|
+
top: el.offsetTop,
|
|
492
|
+
behavior: "auto"
|
|
493
|
+
});
|
|
494
|
+
}, [scrollToPage]);
|
|
495
|
+
return /* @__PURE__ */ jsx6("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx6(
|
|
496
|
+
"div",
|
|
497
|
+
{
|
|
498
|
+
className: "mejiro-reader-scroll-track",
|
|
499
|
+
style: { display: "flex", flexDirection: "column", gap: pageGap },
|
|
500
|
+
children: pages.map((result, i) => /* @__PURE__ */ jsxs6(
|
|
501
|
+
"div",
|
|
502
|
+
{
|
|
503
|
+
ref: (el) => {
|
|
504
|
+
pageRefs.current[i] = el;
|
|
505
|
+
},
|
|
506
|
+
"data-page-idx": i,
|
|
507
|
+
className: "mejiro-reader-page",
|
|
508
|
+
style: { width: pageWidth, height: pageHeight, flexShrink: 0 },
|
|
509
|
+
children: [
|
|
510
|
+
/* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-rule" }),
|
|
511
|
+
/* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-page-header", children: [
|
|
512
|
+
/* @__PURE__ */ jsx6("span", { className: "mejiro-reader-page-header-title" }),
|
|
513
|
+
/* @__PURE__ */ jsx6("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
|
|
514
|
+
] }),
|
|
515
|
+
/* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: /* @__PURE__ */ jsx6(
|
|
516
|
+
MejiroPageView,
|
|
517
|
+
{
|
|
518
|
+
result,
|
|
519
|
+
slotMode: useSlot,
|
|
520
|
+
fontFamily,
|
|
521
|
+
lineSpacing,
|
|
522
|
+
className: "mejiro-reader-page-content",
|
|
523
|
+
style: contentStyle
|
|
524
|
+
}
|
|
525
|
+
) }) })
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
i
|
|
529
|
+
))
|
|
530
|
+
}
|
|
531
|
+
) });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/MejiroSettingsPanel.tsx
|
|
535
|
+
import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
|
|
536
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
537
|
+
var DEFAULT_FONTS = [
|
|
538
|
+
{ value: "serif", label: "System Serif" },
|
|
539
|
+
{ value: "sans-serif", label: "System Sans" }
|
|
540
|
+
];
|
|
541
|
+
function MejiroSettingsPanel({
|
|
542
|
+
open,
|
|
543
|
+
settings,
|
|
544
|
+
onChange,
|
|
545
|
+
fonts = DEFAULT_FONTS,
|
|
546
|
+
minFontSize = 10,
|
|
547
|
+
maxFontSize = 48
|
|
548
|
+
}) {
|
|
549
|
+
const messages = useI18n();
|
|
550
|
+
const patch = (next) => onChange({ ...settings, ...next });
|
|
551
|
+
return /* @__PURE__ */ jsx7("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-inner", children: [
|
|
552
|
+
/* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-group", children: [
|
|
553
|
+
/* @__PURE__ */ jsx7("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
|
|
554
|
+
/* @__PURE__ */ jsx7("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx7(
|
|
555
|
+
"select",
|
|
556
|
+
{
|
|
557
|
+
value: normalizeFontFamily3(settings.fontFamily),
|
|
558
|
+
onChange: (e) => patch({ fontFamily: e.target.value }),
|
|
559
|
+
children: fonts.map((f) => /* @__PURE__ */ jsx7("option", { value: f.value, children: f.label }, f.value))
|
|
560
|
+
}
|
|
561
|
+
) }),
|
|
562
|
+
/* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
|
|
563
|
+
/* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
|
|
564
|
+
/* @__PURE__ */ jsx7(
|
|
565
|
+
"button",
|
|
566
|
+
{
|
|
567
|
+
type: "button",
|
|
568
|
+
className: "mejiro-reader-btn mejiro-reader-btn--icon",
|
|
569
|
+
"aria-label": messages.settingsSizeDown,
|
|
570
|
+
onClick: () => patch({ fontSize: Math.max(minFontSize, settings.fontSize - 1) }),
|
|
571
|
+
children: "A\u2212"
|
|
572
|
+
}
|
|
573
|
+
),
|
|
574
|
+
/* @__PURE__ */ jsx7(
|
|
575
|
+
"input",
|
|
576
|
+
{
|
|
577
|
+
id: "mejiro-reader-font-size",
|
|
578
|
+
type: "number",
|
|
579
|
+
value: settings.fontSize,
|
|
580
|
+
min: minFontSize,
|
|
581
|
+
max: maxFontSize,
|
|
582
|
+
onChange: (e) => patch({ fontSize: Number(e.target.value) })
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
/* @__PURE__ */ jsx7(
|
|
586
|
+
"button",
|
|
587
|
+
{
|
|
588
|
+
type: "button",
|
|
589
|
+
className: "mejiro-reader-btn mejiro-reader-btn--icon",
|
|
590
|
+
"aria-label": messages.settingsSizeUp,
|
|
591
|
+
onClick: () => patch({ fontSize: Math.min(maxFontSize, settings.fontSize + 1) }),
|
|
592
|
+
children: "A+"
|
|
593
|
+
}
|
|
594
|
+
)
|
|
595
|
+
] })
|
|
596
|
+
] }),
|
|
597
|
+
/* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-group", children: [
|
|
598
|
+
/* @__PURE__ */ jsx7("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
|
|
599
|
+
/* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
|
|
600
|
+
/* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
|
|
601
|
+
/* @__PURE__ */ jsxs7(
|
|
602
|
+
"select",
|
|
603
|
+
{
|
|
604
|
+
id: "mejiro-reader-kinsoku",
|
|
605
|
+
value: settings.mode ?? "strict",
|
|
606
|
+
onChange: (e) => patch({ mode: e.target.value }),
|
|
607
|
+
children: [
|
|
608
|
+
/* @__PURE__ */ jsx7("option", { value: "strict", children: messages.settingsStrict }),
|
|
609
|
+
/* @__PURE__ */ jsx7("option", { value: "loose", children: messages.settingsLoose })
|
|
610
|
+
]
|
|
611
|
+
}
|
|
612
|
+
)
|
|
613
|
+
] }),
|
|
614
|
+
/* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
|
|
615
|
+
/* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
|
|
616
|
+
/* @__PURE__ */ jsxs7(
|
|
617
|
+
"select",
|
|
618
|
+
{
|
|
619
|
+
id: "mejiro-reader-hanging",
|
|
620
|
+
value: String(settings.enableHanging ?? true),
|
|
621
|
+
onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ jsx7("option", { value: "true", children: messages.toggleOn }),
|
|
624
|
+
/* @__PURE__ */ jsx7("option", { value: "false", children: messages.toggleOff })
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
)
|
|
628
|
+
] }),
|
|
629
|
+
/* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
|
|
630
|
+
/* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
|
|
631
|
+
/* @__PURE__ */ jsx7(
|
|
632
|
+
"input",
|
|
633
|
+
{
|
|
634
|
+
id: "mejiro-reader-line-spacing",
|
|
635
|
+
className: "mejiro-reader-control--wide",
|
|
636
|
+
type: "number",
|
|
637
|
+
value: settings.lineSpacing ?? 1.8,
|
|
638
|
+
min: 1,
|
|
639
|
+
max: 3,
|
|
640
|
+
step: 0.1,
|
|
641
|
+
onChange: (e) => patch({ lineSpacing: Number(e.target.value) })
|
|
642
|
+
}
|
|
643
|
+
)
|
|
644
|
+
] })
|
|
645
|
+
] })
|
|
646
|
+
] }) });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/MejiroSpread.tsx
|
|
650
|
+
import { normalizeFontFamily as normalizeFontFamily4 } from "@libraz/mejiro/browser";
|
|
651
|
+
import {
|
|
652
|
+
useRef as useRef3
|
|
653
|
+
} from "react";
|
|
654
|
+
|
|
655
|
+
// src/MejiroImageOverlay.tsx
|
|
656
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
657
|
+
function MejiroImageOverlay({
|
|
658
|
+
rect,
|
|
659
|
+
label,
|
|
660
|
+
onOverlayPointerDown,
|
|
661
|
+
onResizePointerDown,
|
|
662
|
+
onClose
|
|
663
|
+
}) {
|
|
664
|
+
const messages = useI18n();
|
|
665
|
+
const resolvedLabel = label ?? messages.imageButton;
|
|
666
|
+
return /* @__PURE__ */ jsxs8(
|
|
667
|
+
"div",
|
|
668
|
+
{
|
|
669
|
+
className: "mejiro-reader-image-overlay",
|
|
670
|
+
style: {
|
|
671
|
+
left: rect.x,
|
|
672
|
+
top: rect.y,
|
|
673
|
+
width: rect.w,
|
|
674
|
+
height: rect.h
|
|
675
|
+
},
|
|
676
|
+
onPointerDown: onOverlayPointerDown,
|
|
677
|
+
children: [
|
|
678
|
+
/* @__PURE__ */ jsxs8("div", { className: "mejiro-reader-image-overlay-label", children: [
|
|
679
|
+
/* @__PURE__ */ jsx8("div", { className: "mejiro-reader-image-overlay-icon" }),
|
|
680
|
+
/* @__PURE__ */ jsx8("span", { children: resolvedLabel })
|
|
681
|
+
] }),
|
|
682
|
+
/* @__PURE__ */ jsx8("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
|
|
683
|
+
/* @__PURE__ */ jsx8(
|
|
684
|
+
"button",
|
|
685
|
+
{
|
|
686
|
+
type: "button",
|
|
687
|
+
"aria-label": messages.imageRemoveButton,
|
|
688
|
+
className: "mejiro-reader-image-overlay-close",
|
|
689
|
+
onPointerDown: (e) => {
|
|
690
|
+
e.stopPropagation();
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
onClose?.();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
)
|
|
696
|
+
]
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/MejiroSelectionLayer.tsx
|
|
702
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
703
|
+
function MejiroSelectionLayer({
|
|
704
|
+
rects,
|
|
705
|
+
side,
|
|
706
|
+
className,
|
|
707
|
+
style,
|
|
708
|
+
rectClassName,
|
|
709
|
+
rectStyle
|
|
710
|
+
}) {
|
|
711
|
+
const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
|
|
712
|
+
const rectClass = rectClassName ?? "mejiro-selection-rect";
|
|
713
|
+
return /* @__PURE__ */ jsx9(
|
|
714
|
+
"div",
|
|
715
|
+
{
|
|
716
|
+
className: wrapperClass,
|
|
717
|
+
style: {
|
|
718
|
+
position: "absolute",
|
|
719
|
+
inset: 0,
|
|
720
|
+
pointerEvents: "none",
|
|
721
|
+
...style
|
|
722
|
+
},
|
|
723
|
+
children: rects.map((r, i) => {
|
|
724
|
+
if (r.side !== side) return null;
|
|
725
|
+
const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
|
|
726
|
+
return /* @__PURE__ */ jsx9(
|
|
727
|
+
"div",
|
|
728
|
+
{
|
|
729
|
+
className: rectClass,
|
|
730
|
+
style: {
|
|
731
|
+
position: "absolute",
|
|
732
|
+
width: r.width,
|
|
733
|
+
height: r.height,
|
|
734
|
+
...posStyle,
|
|
735
|
+
...rectStyle
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
i
|
|
739
|
+
);
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/MejiroSpread.tsx
|
|
746
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
747
|
+
function defaultHeader(data) {
|
|
748
|
+
return /* @__PURE__ */ jsxs9("div", { className: "mejiro-reader-page-header", children: [
|
|
749
|
+
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
|
|
750
|
+
/* @__PURE__ */ jsx10("span", { className: "mejiro-reader-page-header-num", children: data.pageNumber != null ? String(data.pageNumber) : "" })
|
|
751
|
+
] });
|
|
752
|
+
}
|
|
753
|
+
function MejiroSpread({
|
|
754
|
+
spread,
|
|
755
|
+
pageWidth,
|
|
756
|
+
pageHeight,
|
|
757
|
+
contentHeight,
|
|
758
|
+
fontFamily,
|
|
759
|
+
fontSize,
|
|
760
|
+
lineSpacing,
|
|
761
|
+
turning = false,
|
|
762
|
+
rightHeader = {},
|
|
763
|
+
leftHeader = {},
|
|
764
|
+
images = [],
|
|
765
|
+
renderPageHeader,
|
|
766
|
+
indicator,
|
|
767
|
+
onPrev,
|
|
768
|
+
onNext,
|
|
769
|
+
onImagePointerDown,
|
|
770
|
+
onImageResizePointerDown,
|
|
771
|
+
onImageClose,
|
|
772
|
+
anchorAtCoord,
|
|
773
|
+
selectionRects,
|
|
774
|
+
onSelectionChange,
|
|
775
|
+
singlePage = false,
|
|
776
|
+
onSwipe,
|
|
777
|
+
onSurfaceTap
|
|
778
|
+
}) {
|
|
779
|
+
const messages = useI18n();
|
|
780
|
+
const hasImages = images.length > 0;
|
|
781
|
+
const useSlot = true;
|
|
782
|
+
const selectionStartRef = useRef3(null);
|
|
783
|
+
const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
|
|
784
|
+
const gestureRef = useRef3(null);
|
|
785
|
+
const gestureEnabled = onSwipe != null || onSurfaceTap != null;
|
|
786
|
+
const SWIPE_THRESHOLD = 40;
|
|
787
|
+
const TAP_MOVE_THRESHOLD = 8;
|
|
788
|
+
function handleGesturePointerDown(e) {
|
|
789
|
+
if (!gestureEnabled) return;
|
|
790
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
791
|
+
gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
|
|
792
|
+
}
|
|
793
|
+
function handleGesturePointerUp(e) {
|
|
794
|
+
if (!gestureEnabled) return;
|
|
795
|
+
const start = gestureRef.current;
|
|
796
|
+
gestureRef.current = null;
|
|
797
|
+
if (!start) return;
|
|
798
|
+
const dx = e.clientX - start.x;
|
|
799
|
+
const dy = e.clientY - start.y;
|
|
800
|
+
const ax = Math.abs(dx);
|
|
801
|
+
const ay = Math.abs(dy);
|
|
802
|
+
if (ax >= SWIPE_THRESHOLD && ax >= ay * 1.4) {
|
|
803
|
+
onSwipe?.(dx < 0 ? "next" : "prev");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (ax < TAP_MOVE_THRESHOLD && ay < TAP_MOVE_THRESHOLD) {
|
|
807
|
+
const target = e.target;
|
|
808
|
+
if (target?.closest("button, a, .mejiro-reader-image-overlay")) return;
|
|
809
|
+
onSurfaceTap?.();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function resolvePointer(e) {
|
|
813
|
+
if (!anchorAtCoord) return null;
|
|
814
|
+
const target = e.target;
|
|
815
|
+
if (!target) return null;
|
|
816
|
+
const content = target.closest(".mejiro-reader-page-content");
|
|
817
|
+
if (!content) return null;
|
|
818
|
+
const pageEl = content.closest(".mejiro-reader-page");
|
|
819
|
+
const isRight = pageEl?.classList.contains("mejiro-reader-page--right") ?? true;
|
|
820
|
+
const rect = content.getBoundingClientRect();
|
|
821
|
+
const offsetX = e.clientX - rect.left;
|
|
822
|
+
const offsetY = e.clientY - rect.top;
|
|
823
|
+
const spreadX = isRight ? offsetX : offsetX - rect.width;
|
|
824
|
+
return anchorAtCoord(spreadX, offsetY);
|
|
825
|
+
}
|
|
826
|
+
function handlePointerDown(e) {
|
|
827
|
+
if (!selectionEnabled) return;
|
|
828
|
+
const anchor = resolvePointer(e);
|
|
829
|
+
if (!anchor) return;
|
|
830
|
+
e.preventDefault();
|
|
831
|
+
selectionStartRef.current = anchor;
|
|
832
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
833
|
+
onSelectionChange?.(null);
|
|
834
|
+
}
|
|
835
|
+
function handlePointerMove(e) {
|
|
836
|
+
if (!selectionEnabled) return;
|
|
837
|
+
const start = selectionStartRef.current;
|
|
838
|
+
if (!start) return;
|
|
839
|
+
const end = resolvePointer(e);
|
|
840
|
+
if (!end) return;
|
|
841
|
+
if (end.paragraph === start.paragraph && end.charIndex === start.charIndex) return;
|
|
842
|
+
onSelectionChange?.({ start, end });
|
|
843
|
+
}
|
|
844
|
+
function handlePointerUp(e) {
|
|
845
|
+
if (!selectionEnabled) return;
|
|
846
|
+
selectionStartRef.current = null;
|
|
847
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
848
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const contentStyle = { height: contentHeight };
|
|
852
|
+
if (fontFamily) contentStyle.fontFamily = normalizeFontFamily4(fontFamily);
|
|
853
|
+
if (fontSize != null) contentStyle.fontSize = fontSize;
|
|
854
|
+
if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
|
|
855
|
+
const renderPage = (side) => {
|
|
856
|
+
const isRight = side === "right";
|
|
857
|
+
const result = isRight ? spread.right : spread.left;
|
|
858
|
+
const header = isRight ? rightHeader : leftHeader;
|
|
859
|
+
const pageKey = `${side}-${header.pageNumber ?? "blank"}`;
|
|
860
|
+
const pageStyle = {
|
|
861
|
+
width: pageWidth,
|
|
862
|
+
height: pageHeight
|
|
863
|
+
};
|
|
864
|
+
if (isRight && hasImages) pageStyle.overflow = "visible";
|
|
865
|
+
return /* @__PURE__ */ jsxs9(
|
|
866
|
+
"div",
|
|
867
|
+
{
|
|
868
|
+
className: `mejiro-reader-page mejiro-reader-page--${side}`,
|
|
869
|
+
style: pageStyle,
|
|
870
|
+
children: [
|
|
871
|
+
/* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-rule" }),
|
|
872
|
+
renderPageHeader ? renderPageHeader(side, header) : defaultHeader(header),
|
|
873
|
+
/* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs9("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
|
|
874
|
+
/* @__PURE__ */ jsx10(
|
|
875
|
+
MejiroPageView,
|
|
876
|
+
{
|
|
877
|
+
result,
|
|
878
|
+
slotMode: useSlot,
|
|
879
|
+
fontFamily,
|
|
880
|
+
lineSpacing,
|
|
881
|
+
className: "mejiro-reader-page-content",
|
|
882
|
+
style: contentStyle
|
|
883
|
+
},
|
|
884
|
+
pageKey
|
|
885
|
+
),
|
|
886
|
+
selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx10(MejiroSelectionLayer, { rects: selectionRects, side })
|
|
887
|
+
] }) }),
|
|
888
|
+
isRight && images.map((item) => /* @__PURE__ */ jsx10(
|
|
889
|
+
MejiroImageOverlay,
|
|
890
|
+
{
|
|
891
|
+
rect: item.rect,
|
|
892
|
+
onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
|
|
893
|
+
onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
|
|
894
|
+
onClose: () => onImageClose?.(item.id)
|
|
895
|
+
},
|
|
896
|
+
item.id
|
|
897
|
+
))
|
|
898
|
+
]
|
|
899
|
+
},
|
|
900
|
+
pageKey
|
|
901
|
+
);
|
|
902
|
+
};
|
|
903
|
+
function combinedPointerDown(e) {
|
|
904
|
+
if (gestureEnabled) handleGesturePointerDown(e);
|
|
905
|
+
if (selectionEnabled) handlePointerDown(e);
|
|
906
|
+
}
|
|
907
|
+
function combinedPointerMove(e) {
|
|
908
|
+
if (selectionEnabled) handlePointerMove(e);
|
|
909
|
+
}
|
|
910
|
+
function combinedPointerUp(e) {
|
|
911
|
+
if (gestureEnabled) handleGesturePointerUp(e);
|
|
912
|
+
if (selectionEnabled) handlePointerUp(e);
|
|
913
|
+
}
|
|
914
|
+
const useCombined = selectionEnabled || gestureEnabled;
|
|
915
|
+
return /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs9(
|
|
916
|
+
"div",
|
|
917
|
+
{
|
|
918
|
+
className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
|
|
919
|
+
onPointerDown: useCombined ? combinedPointerDown : void 0,
|
|
920
|
+
onPointerMove: useCombined ? combinedPointerMove : void 0,
|
|
921
|
+
onPointerUp: useCombined ? combinedPointerUp : void 0,
|
|
922
|
+
onPointerCancel: useCombined ? combinedPointerUp : void 0,
|
|
923
|
+
children: [
|
|
924
|
+
renderPage("right"),
|
|
925
|
+
!singlePage && renderPage("left"),
|
|
926
|
+
/* @__PURE__ */ jsx10(
|
|
927
|
+
"button",
|
|
928
|
+
{
|
|
929
|
+
type: "button",
|
|
930
|
+
"aria-label": messages.prevSpread,
|
|
931
|
+
className: "mejiro-reader-nav-zone mejiro-reader-nav-zone--prev",
|
|
932
|
+
onClick: onPrev
|
|
933
|
+
}
|
|
934
|
+
),
|
|
935
|
+
/* @__PURE__ */ jsx10(
|
|
936
|
+
"button",
|
|
937
|
+
{
|
|
938
|
+
type: "button",
|
|
939
|
+
"aria-label": messages.nextSpread,
|
|
940
|
+
className: "mejiro-reader-nav-zone mejiro-reader-nav-zone--next",
|
|
941
|
+
onClick: onNext
|
|
942
|
+
}
|
|
943
|
+
),
|
|
944
|
+
indicator
|
|
945
|
+
]
|
|
946
|
+
}
|
|
947
|
+
) });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/MejiroStats.tsx
|
|
951
|
+
import { estimateReadingTime, formatReadingTime } from "@libraz/mejiro/book";
|
|
952
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
953
|
+
function MejiroStats({
|
|
954
|
+
chapter,
|
|
955
|
+
totalPages,
|
|
956
|
+
elapsedMs,
|
|
957
|
+
fontLabel,
|
|
958
|
+
showReadingTime = false,
|
|
959
|
+
cpm = 600,
|
|
960
|
+
readingTimeLocale = "ja"
|
|
961
|
+
}) {
|
|
962
|
+
if (!chapter) return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats" });
|
|
963
|
+
const totalChars = chapter.paragraphs.reduce((s, p) => s + p.text.length, 0);
|
|
964
|
+
const totalRuby = chapter.paragraphs.reduce(
|
|
965
|
+
(s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
|
|
966
|
+
0
|
|
967
|
+
);
|
|
968
|
+
const readingTimeLabel = showReadingTime ? formatReadingTime(estimateReadingTime(chapter, { cpm }), readingTimeLocale) : null;
|
|
969
|
+
const parts = [
|
|
970
|
+
`${totalChars}ch`,
|
|
971
|
+
`${totalPages}pp`,
|
|
972
|
+
totalRuby > 0 ? `${totalRuby}ruby` : null,
|
|
973
|
+
readingTimeLabel,
|
|
974
|
+
fontLabel || null,
|
|
975
|
+
`${elapsedMs.toFixed(0)}ms`
|
|
976
|
+
];
|
|
977
|
+
return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/useChapterLayout.ts
|
|
981
|
+
import { useCallback, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState2 } from "react";
|
|
982
|
+
function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
|
|
983
|
+
const enableResize = options.enableResize ?? true;
|
|
984
|
+
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
985
|
+
const [layout, setLayout] = useState2(null);
|
|
986
|
+
const [pageWidth, setPageWidth] = useState2(0);
|
|
987
|
+
const [pageHeight, setPageHeight] = useState2(0);
|
|
988
|
+
const [contentHeight, setContentHeight] = useState2(0);
|
|
989
|
+
const [elapsedMs, setElapsedMs] = useState2(0);
|
|
990
|
+
const layoutRef = useRef4(null);
|
|
991
|
+
const requestIdRef = useRef4(0);
|
|
992
|
+
layoutRef.current = layout;
|
|
993
|
+
const recompute = useCallback(async () => {
|
|
994
|
+
const requestId = ++requestIdRef.current;
|
|
995
|
+
const chapter = epub?.chapters[chapterIndex];
|
|
996
|
+
if (!(chapter && surface.current)) {
|
|
997
|
+
setLayout(null);
|
|
998
|
+
setPageWidth(0);
|
|
999
|
+
setPageHeight(0);
|
|
1000
|
+
setContentHeight(0);
|
|
1001
|
+
setElapsedMs(0);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
setLayout(null);
|
|
1005
|
+
const dims = book.computePageSize(surface.current);
|
|
1006
|
+
setPageWidth(dims.pageWidth);
|
|
1007
|
+
setPageHeight(dims.pageHeight);
|
|
1008
|
+
setContentHeight(dims.contentHeight);
|
|
1009
|
+
const t0 = performance.now();
|
|
1010
|
+
const l = await book.layoutChapter(chapter);
|
|
1011
|
+
if (requestId !== requestIdRef.current) return;
|
|
1012
|
+
setLayout(l);
|
|
1013
|
+
setElapsedMs(performance.now() - t0);
|
|
1014
|
+
}, [book, epub, chapterIndex, surface]);
|
|
1015
|
+
useLayoutEffect2(() => {
|
|
1016
|
+
requestIdRef.current++;
|
|
1017
|
+
setLayout(null);
|
|
1018
|
+
setPageWidth(0);
|
|
1019
|
+
setPageHeight(0);
|
|
1020
|
+
setContentHeight(0);
|
|
1021
|
+
setElapsedMs(0);
|
|
1022
|
+
}, [recompute]);
|
|
1023
|
+
useEffect2(() => {
|
|
1024
|
+
void recompute();
|
|
1025
|
+
return () => {
|
|
1026
|
+
requestIdRef.current++;
|
|
1027
|
+
};
|
|
1028
|
+
}, [recompute]);
|
|
1029
|
+
useEffect2(() => {
|
|
1030
|
+
if (!enableResize) return;
|
|
1031
|
+
let timer = null;
|
|
1032
|
+
const onResize = () => {
|
|
1033
|
+
if (!(surface.current && layoutRef.current)) return;
|
|
1034
|
+
if (timer) clearTimeout(timer);
|
|
1035
|
+
timer = setTimeout(() => {
|
|
1036
|
+
if (!(surface.current && layoutRef.current)) return;
|
|
1037
|
+
const dims = book.computePageSize(surface.current);
|
|
1038
|
+
setPageWidth(dims.pageWidth);
|
|
1039
|
+
setPageHeight(dims.pageHeight);
|
|
1040
|
+
setContentHeight(dims.contentHeight);
|
|
1041
|
+
layoutRef.current.resize({
|
|
1042
|
+
pageWidth: dims.pageWidth,
|
|
1043
|
+
lineWidth: dims.contentHeight - book.getOptions().fontSize * 0.5
|
|
1044
|
+
});
|
|
1045
|
+
}, resizeDebounce);
|
|
1046
|
+
};
|
|
1047
|
+
window.addEventListener("resize", onResize);
|
|
1048
|
+
return () => {
|
|
1049
|
+
window.removeEventListener("resize", onResize);
|
|
1050
|
+
if (timer) clearTimeout(timer);
|
|
1051
|
+
};
|
|
1052
|
+
}, [book, surface, enableResize, resizeDebounce]);
|
|
1053
|
+
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/useEpub.ts
|
|
1057
|
+
import { parseEpub } from "@libraz/mejiro/epub";
|
|
1058
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef5, useState as useState3 } from "react";
|
|
1059
|
+
function useEpub(options = {}) {
|
|
1060
|
+
const [epub, setEpub] = useState3(null);
|
|
1061
|
+
const [loading, setLoading] = useState3(false);
|
|
1062
|
+
const [error, setError] = useState3(null);
|
|
1063
|
+
const requestIdRef = useRef5(0);
|
|
1064
|
+
const onLoadRef = useRef5(options.onLoad);
|
|
1065
|
+
onLoadRef.current = options.onLoad;
|
|
1066
|
+
const onErrorRef = useRef5(options.onError);
|
|
1067
|
+
onErrorRef.current = options.onError;
|
|
1068
|
+
const fetchOptionsRef = useRef5(options.fetchOptions);
|
|
1069
|
+
fetchOptionsRef.current = options.fetchOptions;
|
|
1070
|
+
const fetchEpubRef = useRef5(options.fetchEpub);
|
|
1071
|
+
fetchEpubRef.current = options.fetchEpub;
|
|
1072
|
+
const loadBuffer = useCallback2(async (buffer) => {
|
|
1073
|
+
const requestId = ++requestIdRef.current;
|
|
1074
|
+
setLoading(true);
|
|
1075
|
+
setError(null);
|
|
1076
|
+
try {
|
|
1077
|
+
const book = await parseEpub(buffer);
|
|
1078
|
+
if (requestId !== requestIdRef.current) return null;
|
|
1079
|
+
setEpub(book);
|
|
1080
|
+
onLoadRef.current?.(book);
|
|
1081
|
+
return book;
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
if (requestId === requestIdRef.current) {
|
|
1084
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1085
|
+
setError(nextError);
|
|
1086
|
+
onErrorRef.current?.(nextError);
|
|
1087
|
+
}
|
|
1088
|
+
return null;
|
|
1089
|
+
} finally {
|
|
1090
|
+
if (requestId === requestIdRef.current) setLoading(false);
|
|
1091
|
+
}
|
|
1092
|
+
}, []);
|
|
1093
|
+
const loadFile = useCallback2(async (file) => {
|
|
1094
|
+
const requestId = ++requestIdRef.current;
|
|
1095
|
+
setLoading(true);
|
|
1096
|
+
setError(null);
|
|
1097
|
+
try {
|
|
1098
|
+
const book = await parseEpub(await file.arrayBuffer());
|
|
1099
|
+
if (requestId !== requestIdRef.current) return null;
|
|
1100
|
+
setEpub(book);
|
|
1101
|
+
onLoadRef.current?.(book);
|
|
1102
|
+
return book;
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
if (requestId === requestIdRef.current) {
|
|
1105
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1106
|
+
setError(nextError);
|
|
1107
|
+
onErrorRef.current?.(nextError);
|
|
1108
|
+
}
|
|
1109
|
+
return null;
|
|
1110
|
+
} finally {
|
|
1111
|
+
if (requestId === requestIdRef.current) setLoading(false);
|
|
1112
|
+
}
|
|
1113
|
+
}, []);
|
|
1114
|
+
const loadUrl = useCallback2(async (url) => {
|
|
1115
|
+
const requestId = ++requestIdRef.current;
|
|
1116
|
+
setLoading(true);
|
|
1117
|
+
setError(null);
|
|
1118
|
+
try {
|
|
1119
|
+
let buffer;
|
|
1120
|
+
if (fetchEpubRef.current) {
|
|
1121
|
+
buffer = await fetchEpubRef.current(url);
|
|
1122
|
+
} else {
|
|
1123
|
+
const init = fetchOptionsRef.current;
|
|
1124
|
+
const res = init ? await fetch(url, init) : await fetch(url);
|
|
1125
|
+
if (!res.ok) return null;
|
|
1126
|
+
buffer = await res.arrayBuffer();
|
|
1127
|
+
}
|
|
1128
|
+
const book = await parseEpub(buffer);
|
|
1129
|
+
if (requestId !== requestIdRef.current) return null;
|
|
1130
|
+
setEpub(book);
|
|
1131
|
+
onLoadRef.current?.(book);
|
|
1132
|
+
return book;
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
if (requestId === requestIdRef.current) {
|
|
1135
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
1136
|
+
setError(nextError);
|
|
1137
|
+
onErrorRef.current?.(nextError);
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
} finally {
|
|
1141
|
+
if (requestId === requestIdRef.current) setLoading(false);
|
|
1142
|
+
}
|
|
1143
|
+
}, []);
|
|
1144
|
+
const defaultUrl = options.defaultUrl;
|
|
1145
|
+
useEffect3(() => {
|
|
1146
|
+
if (defaultUrl) void loadUrl(defaultUrl);
|
|
1147
|
+
}, [defaultUrl, loadUrl]);
|
|
1148
|
+
const replaceEpub = useCallback2((book) => {
|
|
1149
|
+
requestIdRef.current++;
|
|
1150
|
+
setLoading(false);
|
|
1151
|
+
setError(null);
|
|
1152
|
+
setEpub(book);
|
|
1153
|
+
}, []);
|
|
1154
|
+
return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/useMejiroBook.ts
|
|
1158
|
+
import { MejiroBook } from "@libraz/mejiro/book";
|
|
1159
|
+
import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
|
|
1160
|
+
function useMejiroBook(initial) {
|
|
1161
|
+
const bookRef = useRef6(null);
|
|
1162
|
+
if (!bookRef.current) bookRef.current = new MejiroBook(initial);
|
|
1163
|
+
const [options, setLocal] = useState4(initial);
|
|
1164
|
+
const setOptions = useCallback3(async (next) => {
|
|
1165
|
+
setLocal((prev) => ({ ...prev, ...next }));
|
|
1166
|
+
await bookRef.current?.setOptions(next);
|
|
1167
|
+
}, []);
|
|
1168
|
+
return { book: bookRef.current, options, setOptions };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/useMultiImageOverlay.ts
|
|
83
1172
|
import { moveImageOverlayRect, resizeImageOverlayRect } from "@libraz/mejiro";
|
|
84
|
-
import {
|
|
1173
|
+
import {
|
|
1174
|
+
useCallback as useCallback4,
|
|
1175
|
+
useEffect as useEffect4,
|
|
1176
|
+
useMemo as useMemo3,
|
|
1177
|
+
useRef as useRef7,
|
|
1178
|
+
useState as useState5
|
|
1179
|
+
} from "react";
|
|
1180
|
+
var nextId = 0;
|
|
1181
|
+
function useMultiImageOverlay(layout, spreadIdx, options = {}) {
|
|
1182
|
+
const defW = options.defaultWidth ?? 120;
|
|
1183
|
+
const defH = options.defaultHeight ?? 160;
|
|
1184
|
+
const defX = options.defaultX ?? 80;
|
|
1185
|
+
const defY = options.defaultY ?? 100;
|
|
1186
|
+
const margin = options.margin;
|
|
1187
|
+
const [images, setImages] = useState5(() => /* @__PURE__ */ new Map());
|
|
1188
|
+
const imagesRef = useRef7(images);
|
|
1189
|
+
imagesRef.current = images;
|
|
1190
|
+
const layoutRef = useRef7(null);
|
|
1191
|
+
layoutRef.current = layout;
|
|
1192
|
+
const spreadIdxRef = useRef7(spreadIdx);
|
|
1193
|
+
spreadIdxRef.current = spreadIdx;
|
|
1194
|
+
const onUpdateRef = useRef7(options.onUpdate);
|
|
1195
|
+
onUpdateRef.current = options.onUpdate;
|
|
1196
|
+
const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1197
|
+
useEffect4(
|
|
1198
|
+
() => () => {
|
|
1199
|
+
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
1200
|
+
activeDragCleanupsRef.current.clear();
|
|
1201
|
+
},
|
|
1202
|
+
[]
|
|
1203
|
+
);
|
|
1204
|
+
const syncSpread = useCallback4(
|
|
1205
|
+
(si) => {
|
|
1206
|
+
const lo = layoutRef.current;
|
|
1207
|
+
if (!lo) return;
|
|
1208
|
+
const items = imagesRef.current.get(si) ?? [];
|
|
1209
|
+
const bookImages = items.map((it) => ({
|
|
1210
|
+
x: it.rect.x,
|
|
1211
|
+
y: it.rect.y,
|
|
1212
|
+
w: it.rect.w,
|
|
1213
|
+
h: it.rect.h,
|
|
1214
|
+
margin
|
|
1215
|
+
}));
|
|
1216
|
+
lo.setImages(si, bookImages);
|
|
1217
|
+
onUpdateRef.current?.(lo.getSpread(si));
|
|
1218
|
+
},
|
|
1219
|
+
[margin]
|
|
1220
|
+
);
|
|
1221
|
+
const commit = useCallback4(
|
|
1222
|
+
(next, affectedSpread) => {
|
|
1223
|
+
imagesRef.current = next;
|
|
1224
|
+
setImages(next);
|
|
1225
|
+
syncSpread(affectedSpread);
|
|
1226
|
+
},
|
|
1227
|
+
[syncSpread]
|
|
1228
|
+
);
|
|
1229
|
+
const findById = useCallback4(
|
|
1230
|
+
(id) => {
|
|
1231
|
+
for (const [s, list] of imagesRef.current) {
|
|
1232
|
+
const i = list.findIndex((it) => it.id === id);
|
|
1233
|
+
if (i >= 0) return { spread: s, index: i, item: list[i] };
|
|
1234
|
+
}
|
|
1235
|
+
return null;
|
|
1236
|
+
},
|
|
1237
|
+
[]
|
|
1238
|
+
);
|
|
1239
|
+
const addImage = useCallback4(
|
|
1240
|
+
(overrides) => {
|
|
1241
|
+
const si = spreadIdxRef.current;
|
|
1242
|
+
const list = imagesRef.current.get(si) ?? [];
|
|
1243
|
+
const last = list[list.length - 1];
|
|
1244
|
+
const baseX = last ? last.rect.x - 100 : defX;
|
|
1245
|
+
const baseY = last ? last.rect.y + 40 : defY;
|
|
1246
|
+
nextId += 1;
|
|
1247
|
+
const item = {
|
|
1248
|
+
id: `mejiro-img-${nextId}`,
|
|
1249
|
+
rect: {
|
|
1250
|
+
x: overrides?.x ?? baseX,
|
|
1251
|
+
y: overrides?.y ?? baseY,
|
|
1252
|
+
w: overrides?.w ?? defW,
|
|
1253
|
+
h: overrides?.h ?? defH
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
const next = new Map(imagesRef.current);
|
|
1257
|
+
next.set(si, [...list, item]);
|
|
1258
|
+
commit(next, si);
|
|
1259
|
+
return item;
|
|
1260
|
+
},
|
|
1261
|
+
[defX, defY, defW, defH, commit]
|
|
1262
|
+
);
|
|
1263
|
+
const removeImage = useCallback4(
|
|
1264
|
+
(id) => {
|
|
1265
|
+
const found = findById(id);
|
|
1266
|
+
if (!found) return;
|
|
1267
|
+
const next = new Map(imagesRef.current);
|
|
1268
|
+
const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
|
|
1269
|
+
if (list.length === 0) next.delete(found.spread);
|
|
1270
|
+
else next.set(found.spread, list);
|
|
1271
|
+
commit(next, found.spread);
|
|
1272
|
+
},
|
|
1273
|
+
[findById, commit]
|
|
1274
|
+
);
|
|
1275
|
+
const updateImage = useCallback4(
|
|
1276
|
+
(id, rect) => {
|
|
1277
|
+
const found = findById(id);
|
|
1278
|
+
if (!found) return;
|
|
1279
|
+
const next = new Map(imagesRef.current);
|
|
1280
|
+
const list = (next.get(found.spread) ?? []).map(
|
|
1281
|
+
(it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
|
|
1282
|
+
);
|
|
1283
|
+
next.set(found.spread, list);
|
|
1284
|
+
commit(next, found.spread);
|
|
1285
|
+
},
|
|
1286
|
+
[findById, commit]
|
|
1287
|
+
);
|
|
1288
|
+
const clearImages = useCallback4(
|
|
1289
|
+
(target) => {
|
|
1290
|
+
if (target == null) {
|
|
1291
|
+
const old = imagesRef.current;
|
|
1292
|
+
imagesRef.current = /* @__PURE__ */ new Map();
|
|
1293
|
+
setImages(/* @__PURE__ */ new Map());
|
|
1294
|
+
layoutRef.current?.clearImages();
|
|
1295
|
+
if (layoutRef.current) {
|
|
1296
|
+
for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
const next = new Map(imagesRef.current);
|
|
1300
|
+
next.delete(target);
|
|
1301
|
+
commit(next, target);
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
[commit]
|
|
1305
|
+
);
|
|
1306
|
+
const onOverlayPointerDown = useCallback4(
|
|
1307
|
+
(id, e) => {
|
|
1308
|
+
e.preventDefault();
|
|
1309
|
+
const found = findById(id);
|
|
1310
|
+
if (!found) return;
|
|
1311
|
+
const startX = e.clientX;
|
|
1312
|
+
const startY = e.clientY;
|
|
1313
|
+
const start = { ...found.item.rect };
|
|
1314
|
+
const target = e.currentTarget;
|
|
1315
|
+
target.setPointerCapture(e.nativeEvent.pointerId);
|
|
1316
|
+
target.classList.add("is-dragging");
|
|
1317
|
+
let rafId = 0;
|
|
1318
|
+
const onMove = (me) => {
|
|
1319
|
+
const dx = me.clientX - startX;
|
|
1320
|
+
const dy = me.clientY - startY;
|
|
1321
|
+
const r = moveImageOverlayRect(start, dx, dy);
|
|
1322
|
+
cancelAnimationFrame(rafId);
|
|
1323
|
+
rafId = requestAnimationFrame(() => updateImage(id, r));
|
|
1324
|
+
};
|
|
1325
|
+
let cleanup;
|
|
1326
|
+
const onUp = () => cleanup();
|
|
1327
|
+
cleanup = () => {
|
|
1328
|
+
cancelAnimationFrame(rafId);
|
|
1329
|
+
target.classList.remove("is-dragging");
|
|
1330
|
+
document.removeEventListener("pointermove", onMove);
|
|
1331
|
+
document.removeEventListener("pointerup", onUp);
|
|
1332
|
+
activeDragCleanupsRef.current.delete(cleanup);
|
|
1333
|
+
};
|
|
1334
|
+
document.addEventListener("pointermove", onMove);
|
|
1335
|
+
document.addEventListener("pointerup", onUp);
|
|
1336
|
+
activeDragCleanupsRef.current.add(cleanup);
|
|
1337
|
+
},
|
|
1338
|
+
[findById, updateImage]
|
|
1339
|
+
);
|
|
1340
|
+
const onResizePointerDown = useCallback4(
|
|
1341
|
+
(id, e) => {
|
|
1342
|
+
e.preventDefault();
|
|
1343
|
+
e.stopPropagation();
|
|
1344
|
+
const found = findById(id);
|
|
1345
|
+
if (!found) return;
|
|
1346
|
+
const startX = e.clientX;
|
|
1347
|
+
const startY = e.clientY;
|
|
1348
|
+
const start = { ...found.item.rect };
|
|
1349
|
+
const target = e.currentTarget;
|
|
1350
|
+
target.setPointerCapture(e.nativeEvent.pointerId);
|
|
1351
|
+
target.parentElement?.classList.add("is-dragging");
|
|
1352
|
+
let rafId = 0;
|
|
1353
|
+
const onMove = (me) => {
|
|
1354
|
+
const dx = me.clientX - startX;
|
|
1355
|
+
const dy = me.clientY - startY;
|
|
1356
|
+
const r = resizeImageOverlayRect(start, dx, dy);
|
|
1357
|
+
cancelAnimationFrame(rafId);
|
|
1358
|
+
rafId = requestAnimationFrame(() => updateImage(id, r));
|
|
1359
|
+
};
|
|
1360
|
+
let cleanup;
|
|
1361
|
+
const onUp = () => cleanup();
|
|
1362
|
+
cleanup = () => {
|
|
1363
|
+
cancelAnimationFrame(rafId);
|
|
1364
|
+
target.parentElement?.classList.remove("is-dragging");
|
|
1365
|
+
document.removeEventListener("pointermove", onMove);
|
|
1366
|
+
document.removeEventListener("pointerup", onUp);
|
|
1367
|
+
activeDragCleanupsRef.current.delete(cleanup);
|
|
1368
|
+
};
|
|
1369
|
+
document.addEventListener("pointermove", onMove);
|
|
1370
|
+
document.addEventListener("pointerup", onUp);
|
|
1371
|
+
activeDragCleanupsRef.current.add(cleanup);
|
|
1372
|
+
},
|
|
1373
|
+
[findById, updateImage]
|
|
1374
|
+
);
|
|
1375
|
+
useEffect4(() => {
|
|
1376
|
+
if (!layout) return;
|
|
1377
|
+
for (const si of imagesRef.current.keys()) syncSpread(si);
|
|
1378
|
+
}, [layout, syncSpread]);
|
|
1379
|
+
const currentImages = useMemo3(() => images.get(spreadIdx) ?? [], [images, spreadIdx]);
|
|
1380
|
+
const hasImages = useMemo3(() => [...images.values()].some((list) => list.length > 0), [images]);
|
|
1381
|
+
return {
|
|
1382
|
+
imagesBySpread: images,
|
|
1383
|
+
currentImages,
|
|
1384
|
+
hasImages,
|
|
1385
|
+
addImage,
|
|
1386
|
+
removeImage,
|
|
1387
|
+
updateImage,
|
|
1388
|
+
clearImages,
|
|
1389
|
+
onOverlayPointerDown,
|
|
1390
|
+
onResizePointerDown
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/useSpread.ts
|
|
1395
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState6 } from "react";
|
|
1396
|
+
function useSpread(layout, options = {}) {
|
|
1397
|
+
const enableKeyboard = options.enableKeyboard ?? true;
|
|
1398
|
+
const turnDuration = options.turnDuration ?? 180;
|
|
1399
|
+
const onChangeRef = useRef8(options.onChange);
|
|
1400
|
+
onChangeRef.current = options.onChange;
|
|
1401
|
+
const [spreadIdx, setSpreadIdx] = useState6(0);
|
|
1402
|
+
const [spread, setSpread] = useState6(null);
|
|
1403
|
+
const [turning, setTurning] = useState6(false);
|
|
1404
|
+
const layoutRef = useRef8(null);
|
|
1405
|
+
layoutRef.current = layout;
|
|
1406
|
+
const spreadIdxRef = useRef8(0);
|
|
1407
|
+
spreadIdxRef.current = spreadIdx;
|
|
1408
|
+
const turnTimerRef = useRef8(null);
|
|
1409
|
+
const layoutGenerationRef = useRef8(0);
|
|
1410
|
+
const totalPages = layout?.totalPages ?? 0;
|
|
1411
|
+
const totalSpreads = Math.max(1, Math.ceil(totalPages / 2));
|
|
1412
|
+
const refresh = useCallback5(() => {
|
|
1413
|
+
if (!layoutRef.current) {
|
|
1414
|
+
setSpread(null);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
setSpread(layoutRef.current.getSpread(spreadIdxRef.current));
|
|
1418
|
+
}, []);
|
|
1419
|
+
useLayoutEffect3(() => {
|
|
1420
|
+
layoutGenerationRef.current++;
|
|
1421
|
+
if (turnTimerRef.current) {
|
|
1422
|
+
clearTimeout(turnTimerRef.current);
|
|
1423
|
+
turnTimerRef.current = null;
|
|
1424
|
+
}
|
|
1425
|
+
setTurning(false);
|
|
1426
|
+
setSpreadIdx(0);
|
|
1427
|
+
if (!layout) {
|
|
1428
|
+
setSpread(null);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
setSpread(layout.getSpread(0));
|
|
1432
|
+
}, [layout]);
|
|
1433
|
+
useEffect5(() => {
|
|
1434
|
+
if (!layout) return;
|
|
1435
|
+
setSpread(layout.getSpread(spreadIdx));
|
|
1436
|
+
onChangeRef.current?.(spreadIdx);
|
|
1437
|
+
}, [layout, spreadIdx]);
|
|
1438
|
+
const goTo = useCallback5(
|
|
1439
|
+
(index) => {
|
|
1440
|
+
if (!layoutRef.current) return;
|
|
1441
|
+
const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
|
|
1442
|
+
const target = Math.max(0, Math.min(max, index));
|
|
1443
|
+
if (target === spreadIdxRef.current) return;
|
|
1444
|
+
if (turnDuration > 0) {
|
|
1445
|
+
const generation = layoutGenerationRef.current;
|
|
1446
|
+
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1447
|
+
setTurning(true);
|
|
1448
|
+
turnTimerRef.current = setTimeout(() => {
|
|
1449
|
+
if (generation !== layoutGenerationRef.current) return;
|
|
1450
|
+
setSpreadIdx(target);
|
|
1451
|
+
setTurning(false);
|
|
1452
|
+
turnTimerRef.current = null;
|
|
1453
|
+
}, turnDuration);
|
|
1454
|
+
} else {
|
|
1455
|
+
setSpreadIdx(target);
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
[turnDuration]
|
|
1459
|
+
);
|
|
1460
|
+
const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
|
|
1461
|
+
const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
|
|
1462
|
+
useEffect5(() => {
|
|
1463
|
+
if (!enableKeyboard) return;
|
|
1464
|
+
const onKey = (e) => {
|
|
1465
|
+
if (!layoutRef.current) return;
|
|
1466
|
+
if (e.key === "ArrowLeft") next();
|
|
1467
|
+
else if (e.key === "ArrowRight") prev();
|
|
1468
|
+
};
|
|
1469
|
+
window.addEventListener("keydown", onKey);
|
|
1470
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1471
|
+
}, [enableKeyboard, next, prev]);
|
|
1472
|
+
useEffect5(
|
|
1473
|
+
() => () => {
|
|
1474
|
+
if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
|
|
1475
|
+
},
|
|
1476
|
+
[]
|
|
1477
|
+
);
|
|
1478
|
+
return { spreadIdx, spread, totalPages, totalSpreads, turning, next, prev, goTo, refresh };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/MejiroReader.tsx
|
|
1482
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1483
|
+
function MejiroReaderInner(props, ref) {
|
|
1484
|
+
const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
|
|
1485
|
+
const dialectProp = "dialect" in props ? props.dialect : void 0;
|
|
1486
|
+
const {
|
|
1487
|
+
options = DEFAULT_BOOK_OPTIONS,
|
|
1488
|
+
fonts,
|
|
1489
|
+
epub: epubProp,
|
|
1490
|
+
epubUrl,
|
|
1491
|
+
chapter: chapterProp,
|
|
1492
|
+
spreadIdx: spreadIdxProp,
|
|
1493
|
+
theme = "light",
|
|
1494
|
+
mode = "paginated",
|
|
1495
|
+
spreadMode = "double",
|
|
1496
|
+
enableSurfaceTap = true,
|
|
1497
|
+
fallback,
|
|
1498
|
+
fetchOptions,
|
|
1499
|
+
fetchEpub: fetchEpubFn,
|
|
1500
|
+
locale,
|
|
1501
|
+
messages,
|
|
1502
|
+
title = "mejiro",
|
|
1503
|
+
subtitle,
|
|
1504
|
+
logo,
|
|
1505
|
+
bare = false,
|
|
1506
|
+
enableHeader = !bare,
|
|
1507
|
+
enableDropZone = false,
|
|
1508
|
+
enableChapterNav = !bare,
|
|
1509
|
+
chapterNavMode = "select",
|
|
1510
|
+
enableSettings = !bare,
|
|
1511
|
+
enableImageOverlay = false,
|
|
1512
|
+
enableStats = !bare,
|
|
1513
|
+
enableKeyboard = true,
|
|
1514
|
+
enablePageIndicator = !bare,
|
|
1515
|
+
annotations,
|
|
1516
|
+
onLoad,
|
|
1517
|
+
onError,
|
|
1518
|
+
onChapterChange,
|
|
1519
|
+
onSpreadChange,
|
|
1520
|
+
onSpreadIdxChange,
|
|
1521
|
+
onPageRead,
|
|
1522
|
+
onChapterCompleted
|
|
1523
|
+
} = props;
|
|
1524
|
+
const surfaceRef = useRef9(null);
|
|
1525
|
+
const fileRef = useRef9(null);
|
|
1526
|
+
const [settingsOpen, setSettingsOpen] = useState7(false);
|
|
1527
|
+
const [chapterState, setChapterState] = useState7(chapterProp ?? 0);
|
|
1528
|
+
const chapter = chapterProp ?? chapterState;
|
|
1529
|
+
const [chromeHidden, setChromeHidden] = useState7(false);
|
|
1530
|
+
const [autoSingle, setAutoSingle] = useState7(false);
|
|
1531
|
+
useEffect6(() => {
|
|
1532
|
+
if (!settingsOpen) return;
|
|
1533
|
+
const onKey = (e2) => {
|
|
1534
|
+
if (e2.key === "Escape") setSettingsOpen(false);
|
|
1535
|
+
};
|
|
1536
|
+
window.addEventListener("keydown", onKey);
|
|
1537
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1538
|
+
}, [settingsOpen]);
|
|
1539
|
+
useEffect6(() => {
|
|
1540
|
+
if (spreadMode !== "auto") return;
|
|
1541
|
+
const surface = surfaceRef.current;
|
|
1542
|
+
if (!surface) return;
|
|
1543
|
+
const update = () => {
|
|
1544
|
+
const rect = surface.getBoundingClientRect();
|
|
1545
|
+
setAutoSingle(rect.width < rect.height);
|
|
1546
|
+
};
|
|
1547
|
+
update();
|
|
1548
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
1549
|
+
const observer = new ResizeObserver(update);
|
|
1550
|
+
observer.observe(surface);
|
|
1551
|
+
return () => observer.disconnect();
|
|
1552
|
+
}, [spreadMode]);
|
|
1553
|
+
const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
|
|
1554
|
+
const { book, options: bookOptions, setOptions } = useMejiroBook(options);
|
|
1555
|
+
const synthesizedEpub = useMemo4(() => {
|
|
1556
|
+
if (manuscriptProp === void 0) return null;
|
|
1557
|
+
return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
|
|
1558
|
+
}, [manuscriptProp, dialectProp]);
|
|
1559
|
+
const epubCtx = useEpub({
|
|
1560
|
+
// `epub` / `manuscript` take precedence: skip the URL fetch when a parsed
|
|
1561
|
+
// book (or a synthesized manuscript book) is supplied.
|
|
1562
|
+
defaultUrl: epubProp !== void 0 || manuscriptProp !== void 0 ? void 0 : epubUrl,
|
|
1563
|
+
fetchOptions,
|
|
1564
|
+
fetchEpub: fetchEpubFn,
|
|
1565
|
+
onLoad: (b) => {
|
|
1566
|
+
if (chapterProp == null) setChapterState(0);
|
|
1567
|
+
onLoad?.(b);
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
|
|
1571
|
+
const isManuscriptSource = manuscriptProp !== void 0;
|
|
1572
|
+
const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
|
|
1573
|
+
const onErrorRef = useRef9(onError);
|
|
1574
|
+
onErrorRef.current = onError;
|
|
1575
|
+
useEffect6(() => {
|
|
1576
|
+
if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
|
|
1577
|
+
}, [epubCtx.error]);
|
|
1578
|
+
const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef);
|
|
1579
|
+
const spreadChangedRef = useRef9(void 0);
|
|
1580
|
+
spreadChangedRef.current = (i) => {
|
|
1581
|
+
onSpreadChange?.(i);
|
|
1582
|
+
onSpreadIdxChange?.(i);
|
|
1583
|
+
};
|
|
1584
|
+
const spreadCtx = useSpread(layoutCtx.layout, {
|
|
1585
|
+
enableKeyboard,
|
|
1586
|
+
onChange: (i) => spreadChangedRef.current?.(i)
|
|
1587
|
+
});
|
|
1588
|
+
const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.spreadIdx, {
|
|
1589
|
+
onUpdate: () => spreadCtx.refresh()
|
|
1590
|
+
});
|
|
1591
|
+
const annotationRects = useMemo4(() => {
|
|
1592
|
+
if (!(annotations && layoutCtx.layout)) return [];
|
|
1593
|
+
const result = [];
|
|
1594
|
+
for (const annotation of annotations) {
|
|
1595
|
+
if (annotation.chapter !== chapter) continue;
|
|
1596
|
+
const rects = layoutCtx.layout.selectionRects({
|
|
1597
|
+
start: annotation.start,
|
|
1598
|
+
end: annotation.end
|
|
1599
|
+
});
|
|
1600
|
+
for (const rect of rects) result.push(rect);
|
|
1601
|
+
}
|
|
1602
|
+
return result;
|
|
1603
|
+
}, [annotations, layoutCtx.layout, chapter]);
|
|
1604
|
+
const clearImagesRef = useRef9(imageCtx.clearImages);
|
|
1605
|
+
clearImagesRef.current = imageCtx.clearImages;
|
|
1606
|
+
const onLoadRef = useRef9(onLoad);
|
|
1607
|
+
onLoadRef.current = onLoad;
|
|
1608
|
+
useEffect6(() => {
|
|
1609
|
+
if (!controlled) return;
|
|
1610
|
+
if (chapterProp == null) setChapterState(0);
|
|
1611
|
+
clearImagesRef.current();
|
|
1612
|
+
book.clearCache();
|
|
1613
|
+
if (epubProp) onLoadRef.current?.(epubProp);
|
|
1614
|
+
}, [controlled, epubProp, book, chapterProp]);
|
|
1615
|
+
const spreadGoToRef = useRef9(spreadCtx.goTo);
|
|
1616
|
+
spreadGoToRef.current = spreadCtx.goTo;
|
|
1617
|
+
useEffect6(() => {
|
|
1618
|
+
if (spreadIdxProp == null) return;
|
|
1619
|
+
if (spreadIdxProp === spreadCtx.spreadIdx) return;
|
|
1620
|
+
spreadGoToRef.current(spreadIdxProp);
|
|
1621
|
+
}, [spreadIdxProp, spreadCtx.spreadIdx]);
|
|
1622
|
+
const onChapter = useCallback6(
|
|
1623
|
+
(i) => {
|
|
1624
|
+
if (i === chapter) return;
|
|
1625
|
+
if (chapterProp == null) setChapterState(i);
|
|
1626
|
+
onChapterChange?.(i);
|
|
1627
|
+
},
|
|
1628
|
+
[chapter, chapterProp, onChapterChange]
|
|
1629
|
+
);
|
|
1630
|
+
const listenersRef = useRef9(/* @__PURE__ */ new Map());
|
|
1631
|
+
const emit = useCallback6(
|
|
1632
|
+
(event, payload) => {
|
|
1633
|
+
const set = listenersRef.current.get(event);
|
|
1634
|
+
if (!set) return;
|
|
1635
|
+
for (const cb of set) cb(payload);
|
|
1636
|
+
},
|
|
1637
|
+
[]
|
|
1638
|
+
);
|
|
1639
|
+
const pendingAnchorRef = useRef9(null);
|
|
1640
|
+
const layout = layoutCtx.layout;
|
|
1641
|
+
const tryApplyPendingAnchor = useCallback6(() => {
|
|
1642
|
+
const pending = pendingAnchorRef.current;
|
|
1643
|
+
if (!pending) return;
|
|
1644
|
+
if (pending.anchor.chapter !== chapter) return;
|
|
1645
|
+
if (!layout) return;
|
|
1646
|
+
const loc = layout.locateAnchor({
|
|
1647
|
+
paragraph: pending.anchor.paragraph,
|
|
1648
|
+
charIndex: pending.anchor.charIndex
|
|
1649
|
+
});
|
|
1650
|
+
if (!loc) return;
|
|
1651
|
+
pendingAnchorRef.current = null;
|
|
1652
|
+
spreadGoToRef.current(loc.spreadIdx);
|
|
1653
|
+
pending.resolve();
|
|
1654
|
+
}, [chapter, layout]);
|
|
1655
|
+
useEffect6(() => {
|
|
1656
|
+
tryApplyPendingAnchor();
|
|
1657
|
+
}, [tryApplyPendingAnchor]);
|
|
1658
|
+
useEffect6(
|
|
1659
|
+
() => () => {
|
|
1660
|
+
pendingAnchorRef.current?.resolve();
|
|
1661
|
+
pendingAnchorRef.current = null;
|
|
1662
|
+
},
|
|
1663
|
+
[]
|
|
1664
|
+
);
|
|
1665
|
+
const didMountRef = useRef9(false);
|
|
1666
|
+
const dwellRef = useRef9(null);
|
|
1667
|
+
const onPageReadRef = useRef9(onPageRead);
|
|
1668
|
+
onPageReadRef.current = onPageRead;
|
|
1669
|
+
const onChapterCompletedRef = useRef9(onChapterCompleted);
|
|
1670
|
+
onChapterCompletedRef.current = onChapterCompleted;
|
|
1671
|
+
useEffect6(() => {
|
|
1672
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1673
|
+
if (didMountRef.current && dwellRef.current) {
|
|
1674
|
+
onPageReadRef.current?.(dwellRef.current.anchor, now - dwellRef.current.ts);
|
|
1675
|
+
}
|
|
1676
|
+
if (layout) {
|
|
1677
|
+
const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
|
|
1678
|
+
dwellRef.current = inCh ? { anchor: { chapter, ...inCh }, ts: now } : null;
|
|
1679
|
+
} else {
|
|
1680
|
+
dwellRef.current = null;
|
|
1681
|
+
}
|
|
1682
|
+
if (!didMountRef.current) {
|
|
1683
|
+
didMountRef.current = true;
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
emit("spreadChanged", { chapter, spreadIdx: spreadCtx.spreadIdx });
|
|
1687
|
+
if (spreadCtx.totalSpreads > 0 && spreadCtx.spreadIdx === spreadCtx.totalSpreads - 1) {
|
|
1688
|
+
emit("chapterFinished", { chapter });
|
|
1689
|
+
onChapterCompletedRef.current?.(chapter);
|
|
1690
|
+
}
|
|
1691
|
+
}, [chapter, spreadCtx.spreadIdx, spreadCtx.totalSpreads, layout, emit]);
|
|
1692
|
+
const prevTurningRef = useRef9(false);
|
|
1693
|
+
useEffect6(() => {
|
|
1694
|
+
if (spreadCtx.turning && !prevTurningRef.current) {
|
|
1695
|
+
emit("turnStart", { from: spreadCtx.spreadIdx });
|
|
1696
|
+
} else if (!spreadCtx.turning && prevTurningRef.current) {
|
|
1697
|
+
emit("turnEnd", { to: spreadCtx.spreadIdx });
|
|
1698
|
+
}
|
|
1699
|
+
prevTurningRef.current = spreadCtx.turning;
|
|
1700
|
+
}, [spreadCtx.turning, spreadCtx.spreadIdx, emit]);
|
|
1701
|
+
useImperativeHandle(
|
|
1702
|
+
ref,
|
|
1703
|
+
() => ({
|
|
1704
|
+
goToSpread: (i) => spreadGoToRef.current(i),
|
|
1705
|
+
next: () => spreadCtx.next(),
|
|
1706
|
+
prev: () => spreadCtx.prev(),
|
|
1707
|
+
goToChapter: (i) => onChapter(i),
|
|
1708
|
+
getReadingPosition: () => ({
|
|
1709
|
+
chapter,
|
|
1710
|
+
spreadIdx: spreadCtx.spreadIdx,
|
|
1711
|
+
totalPages: spreadCtx.totalPages,
|
|
1712
|
+
totalSpreads: spreadCtx.totalSpreads
|
|
1713
|
+
}),
|
|
1714
|
+
goToAnchor: (anchor) => new Promise((resolve) => {
|
|
1715
|
+
pendingAnchorRef.current?.resolve();
|
|
1716
|
+
pendingAnchorRef.current = { anchor, resolve };
|
|
1717
|
+
if (anchor.chapter !== chapter) onChapter(anchor.chapter);
|
|
1718
|
+
tryApplyPendingAnchor();
|
|
1719
|
+
}),
|
|
1720
|
+
getAnchor: () => {
|
|
1721
|
+
if (!layout) return null;
|
|
1722
|
+
const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
|
|
1723
|
+
return inCh ? { chapter, ...inCh } : null;
|
|
1724
|
+
},
|
|
1725
|
+
getVisibleRange: () => {
|
|
1726
|
+
if (!layout) return null;
|
|
1727
|
+
const start = layout.anchorAt(spreadCtx.spreadIdx, "right");
|
|
1728
|
+
if (!start) return null;
|
|
1729
|
+
const next = layout.anchorAt(spreadCtx.spreadIdx + 1, "right");
|
|
1730
|
+
let end;
|
|
1731
|
+
if (next) {
|
|
1732
|
+
end = next;
|
|
1733
|
+
} else {
|
|
1734
|
+
const ch = e?.chapters[chapter];
|
|
1735
|
+
const lastP = (ch?.paragraphs.length ?? 1) - 1;
|
|
1736
|
+
const lastText = ch?.paragraphs[lastP]?.text ?? "";
|
|
1737
|
+
end = { paragraph: Math.max(0, lastP), charIndex: [...lastText].length };
|
|
1738
|
+
}
|
|
1739
|
+
return {
|
|
1740
|
+
start: { chapter, ...start },
|
|
1741
|
+
end: { chapter, ...end }
|
|
1742
|
+
};
|
|
1743
|
+
},
|
|
1744
|
+
setOptions: (partial) => setOptions(partial),
|
|
1745
|
+
subscribe: (event, listener) => {
|
|
1746
|
+
let set = listenersRef.current.get(event);
|
|
1747
|
+
if (!set) {
|
|
1748
|
+
set = /* @__PURE__ */ new Set();
|
|
1749
|
+
listenersRef.current.set(event, set);
|
|
1750
|
+
}
|
|
1751
|
+
set.add(listener);
|
|
1752
|
+
return () => {
|
|
1753
|
+
listenersRef.current.get(event)?.delete(listener);
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
}),
|
|
1757
|
+
[
|
|
1758
|
+
spreadCtx.next,
|
|
1759
|
+
spreadCtx.prev,
|
|
1760
|
+
spreadCtx.spreadIdx,
|
|
1761
|
+
spreadCtx.totalPages,
|
|
1762
|
+
spreadCtx.totalSpreads,
|
|
1763
|
+
onChapter,
|
|
1764
|
+
chapter,
|
|
1765
|
+
layout,
|
|
1766
|
+
e,
|
|
1767
|
+
setOptions,
|
|
1768
|
+
tryApplyPendingAnchor
|
|
1769
|
+
]
|
|
1770
|
+
);
|
|
1771
|
+
const editable = {
|
|
1772
|
+
fontFamily: bookOptions.fontFamily,
|
|
1773
|
+
fontSize: bookOptions.fontSize,
|
|
1774
|
+
lineSpacing: bookOptions.lineSpacing ?? 1.8,
|
|
1775
|
+
mode: bookOptions.mode ?? "strict",
|
|
1776
|
+
enableHanging: bookOptions.enableHanging ?? true
|
|
1777
|
+
};
|
|
1778
|
+
const fontLabel = (() => {
|
|
1779
|
+
const css = normalizeFontFamily5(bookOptions.fontFamily);
|
|
1780
|
+
const f = fonts?.find((x) => x.value === css);
|
|
1781
|
+
const name = f?.label ?? css;
|
|
1782
|
+
return `${name} ${bookOptions.fontSize}px`;
|
|
1783
|
+
})();
|
|
1784
|
+
const showChapterSelect = e && enableChapterNav && (chapterNavMode === "select" || chapterNavMode === "both");
|
|
1785
|
+
const showChapterPanel = e && enableChapterNav && (chapterNavMode === "panel" || chapterNavMode === "both");
|
|
1786
|
+
const runningTitleRight = e ? e.author ? `${e.author} ${e.title}` : e.title : "";
|
|
1787
|
+
const runningTitleLeft = e?.chapters[chapter]?.title ?? "";
|
|
1788
|
+
const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
|
|
1789
|
+
const themeName = typeof theme === "string" ? theme : theme.name;
|
|
1790
|
+
const themeOverride = typeof theme === "string" ? void 0 : theme.override;
|
|
1791
|
+
const themeStyle = themeOverride;
|
|
1792
|
+
const resolvedMessages = useI18n({ locale, messages });
|
|
1793
|
+
const effectiveSubtitle = subtitle ?? resolvedMessages.logoSubtitle;
|
|
1794
|
+
const defaultLogo = /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-logo", children: [
|
|
1795
|
+
/* @__PURE__ */ jsx12("span", { className: "mejiro-reader-logo-mark", children: title }),
|
|
1796
|
+
effectiveSubtitle && /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-logo-sub", children: effectiveSubtitle })
|
|
1797
|
+
] });
|
|
1798
|
+
const header = enableHeader ? /* @__PURE__ */ jsxs10("header", { className: "mejiro-reader-header", children: [
|
|
1799
|
+
/* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-left", children: [
|
|
1800
|
+
logo === void 0 ? defaultLogo : logo,
|
|
1801
|
+
showChapterSelect && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter })
|
|
1802
|
+
] }),
|
|
1803
|
+
/* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-actions", children: [
|
|
1804
|
+
enableStats && /* @__PURE__ */ jsx12(
|
|
1805
|
+
MejiroStats,
|
|
1806
|
+
{
|
|
1807
|
+
chapter: e?.chapters[chapter] ?? null,
|
|
1808
|
+
totalPages: layoutCtx.layout?.totalPages ?? 0,
|
|
1809
|
+
elapsedMs: layoutCtx.elapsedMs,
|
|
1810
|
+
fontLabel
|
|
1811
|
+
}
|
|
1812
|
+
),
|
|
1813
|
+
enableDropZone && /* @__PURE__ */ jsx12(
|
|
1814
|
+
"button",
|
|
1815
|
+
{
|
|
1816
|
+
type: "button",
|
|
1817
|
+
className: "mejiro-reader-btn",
|
|
1818
|
+
onClick: () => fileRef.current?.click(),
|
|
1819
|
+
children: resolvedMessages.openButton
|
|
1820
|
+
}
|
|
1821
|
+
),
|
|
1822
|
+
enableImageOverlay && e && /* @__PURE__ */ jsx12(
|
|
1823
|
+
"button",
|
|
1824
|
+
{
|
|
1825
|
+
type: "button",
|
|
1826
|
+
className: `mejiro-reader-btn${imageCtx.hasImages ? " is-active" : ""}`,
|
|
1827
|
+
onClick: () => imageCtx.addImage(),
|
|
1828
|
+
children: resolvedMessages.imageButton
|
|
1829
|
+
}
|
|
1830
|
+
),
|
|
1831
|
+
enableSettings && /* @__PURE__ */ jsxs10(
|
|
1832
|
+
"button",
|
|
1833
|
+
{
|
|
1834
|
+
type: "button",
|
|
1835
|
+
className: `mejiro-reader-btn${settingsOpen ? " is-active" : ""}`,
|
|
1836
|
+
onClick: () => setSettingsOpen((v) => !v),
|
|
1837
|
+
children: [
|
|
1838
|
+
resolvedMessages.settingsButton,
|
|
1839
|
+
/* @__PURE__ */ jsx12("span", { className: "mejiro-reader-btn-arrow", children: "\u25BE" })
|
|
1840
|
+
]
|
|
1841
|
+
}
|
|
1842
|
+
)
|
|
1843
|
+
] })
|
|
1844
|
+
] }) : null;
|
|
1845
|
+
const currentSpread = spreadCtx.spreadIdx;
|
|
1846
|
+
const rightPage = currentSpread * 2 + 1;
|
|
1847
|
+
const leftPage = currentSpread * 2 + 2;
|
|
1848
|
+
const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
|
|
1849
|
+
return /* @__PURE__ */ jsx12(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs10(
|
|
1850
|
+
"div",
|
|
1851
|
+
{
|
|
1852
|
+
className: `mejiro-reader${chromeHidden ? " mejiro-reader--chrome-hidden" : ""}`,
|
|
1853
|
+
"data-mejiro-theme": themeName,
|
|
1854
|
+
style: themeStyle,
|
|
1855
|
+
children: [
|
|
1856
|
+
header,
|
|
1857
|
+
enableSettings && /* @__PURE__ */ jsx12(
|
|
1858
|
+
MejiroSettingsPanel,
|
|
1859
|
+
{
|
|
1860
|
+
open: settingsOpen,
|
|
1861
|
+
settings: editable,
|
|
1862
|
+
fonts,
|
|
1863
|
+
onChange: setOptions
|
|
1864
|
+
}
|
|
1865
|
+
),
|
|
1866
|
+
/* @__PURE__ */ jsxs10("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
|
|
1867
|
+
showChapterPanel && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
|
|
1868
|
+
/* @__PURE__ */ jsxs10("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
|
|
1869
|
+
!(e || epubCtx.loading) && enableDropZone && /* @__PURE__ */ jsx12(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
|
|
1870
|
+
epubCtx.loading && /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
|
|
1871
|
+
!layoutReady && fallback && /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-fallback", children: fallback }),
|
|
1872
|
+
layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx12(
|
|
1873
|
+
MejiroScrollView,
|
|
1874
|
+
{
|
|
1875
|
+
layout: layoutCtx.layout,
|
|
1876
|
+
pageWidth: layoutCtx.pageWidth,
|
|
1877
|
+
pageHeight: layoutCtx.pageHeight,
|
|
1878
|
+
contentHeight: layoutCtx.contentHeight,
|
|
1879
|
+
fontFamily: bookOptions.fontFamily,
|
|
1880
|
+
fontSize: bookOptions.fontSize,
|
|
1881
|
+
lineSpacing: bookOptions.lineSpacing,
|
|
1882
|
+
scrollToPage: spreadCtx.spreadIdx * 2,
|
|
1883
|
+
onVisiblePageChange: (pageIdx) => {
|
|
1884
|
+
const target = Math.floor(pageIdx / 2);
|
|
1885
|
+
if (target !== spreadCtx.spreadIdx) spreadGoToRef.current(target);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
),
|
|
1889
|
+
layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx12(
|
|
1890
|
+
MejiroSpread,
|
|
1891
|
+
{
|
|
1892
|
+
singlePage: effectiveSingle,
|
|
1893
|
+
onSwipe: (dir) => dir === "next" ? spreadCtx.next() : spreadCtx.prev(),
|
|
1894
|
+
onSurfaceTap: enableSurfaceTap ? () => setChromeHidden((v) => !v) : void 0,
|
|
1895
|
+
spread: spreadCtx.spread,
|
|
1896
|
+
pageWidth: layoutCtx.pageWidth,
|
|
1897
|
+
pageHeight: layoutCtx.pageHeight,
|
|
1898
|
+
contentHeight: layoutCtx.contentHeight,
|
|
1899
|
+
fontFamily: bookOptions.fontFamily,
|
|
1900
|
+
fontSize: bookOptions.fontSize,
|
|
1901
|
+
lineSpacing: bookOptions.lineSpacing,
|
|
1902
|
+
turning: spreadCtx.turning,
|
|
1903
|
+
rightHeader: { title: runningTitleRight, pageNumber: rightPage },
|
|
1904
|
+
leftHeader: {
|
|
1905
|
+
title: runningTitleLeft,
|
|
1906
|
+
pageNumber: showLeft ? leftPage : null
|
|
1907
|
+
},
|
|
1908
|
+
images: imageCtx.currentImages,
|
|
1909
|
+
indicator: enablePageIndicator ? /* @__PURE__ */ jsx12(
|
|
1910
|
+
MejiroPageIndicator,
|
|
1911
|
+
{
|
|
1912
|
+
current: spreadCtx.spreadIdx + 1,
|
|
1913
|
+
total: spreadCtx.totalSpreads
|
|
1914
|
+
}
|
|
1915
|
+
) : null,
|
|
1916
|
+
onPrev: spreadCtx.prev,
|
|
1917
|
+
onNext: spreadCtx.next,
|
|
1918
|
+
onImagePointerDown: imageCtx.onOverlayPointerDown,
|
|
1919
|
+
onImageResizePointerDown: imageCtx.onResizePointerDown,
|
|
1920
|
+
onImageClose: imageCtx.removeImage,
|
|
1921
|
+
selectionRects: annotationRects.length ? annotationRects : void 0
|
|
1922
|
+
},
|
|
1923
|
+
`${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
|
|
1924
|
+
)
|
|
1925
|
+
] })
|
|
1926
|
+
] }),
|
|
1927
|
+
/* @__PURE__ */ jsx12(
|
|
1928
|
+
"input",
|
|
1929
|
+
{
|
|
1930
|
+
ref: fileRef,
|
|
1931
|
+
type: "file",
|
|
1932
|
+
accept: ".epub",
|
|
1933
|
+
hidden: true,
|
|
1934
|
+
onChange: (ev) => {
|
|
1935
|
+
const file = ev.target.files?.[0];
|
|
1936
|
+
if (file) void epubCtx.loadFile(file);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
),
|
|
1940
|
+
/* @__PURE__ */ jsx12("div", { className: "mejiro-reader-sr-only", role: "status", "aria-live": "polite", children: spreadCtx.totalSpreads > 0 ? format(resolvedMessages.spreadAnnouncement, {
|
|
1941
|
+
spread: spreadCtx.spreadIdx + 1,
|
|
1942
|
+
total: spreadCtx.totalSpreads
|
|
1943
|
+
}) : "" })
|
|
1944
|
+
]
|
|
1945
|
+
}
|
|
1946
|
+
) });
|
|
1947
|
+
}
|
|
1948
|
+
var MejiroReader = forwardRef(MejiroReaderInner);
|
|
1949
|
+
MejiroReader.displayName = "MejiroReader";
|
|
1950
|
+
|
|
1951
|
+
// src/MejiroEditor.tsx
|
|
1952
|
+
import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1953
|
+
function cloneBook(book) {
|
|
1954
|
+
return {
|
|
1955
|
+
...book,
|
|
1956
|
+
chapters: book.chapters.map((chapter) => ({
|
|
1957
|
+
...chapter,
|
|
1958
|
+
blocks: chapter.blocks.map(
|
|
1959
|
+
(block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
|
|
1960
|
+
),
|
|
1961
|
+
imageAssets: new Map(chapter.imageAssets),
|
|
1962
|
+
originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
|
|
1963
|
+
paragraphs: chapter.paragraphs.map((paragraph) => ({
|
|
1964
|
+
...paragraph,
|
|
1965
|
+
inlineAnnotations: [...paragraph.inlineAnnotations]
|
|
1966
|
+
})),
|
|
1967
|
+
paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
|
|
1968
|
+
})),
|
|
1969
|
+
packageData: {
|
|
1970
|
+
...book.packageData,
|
|
1971
|
+
files: new Map(book.packageData.files)
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function utf16ToCodepoint(text, utf16Offset) {
|
|
1976
|
+
let cp = 0;
|
|
1977
|
+
let i = 0;
|
|
1978
|
+
while (i < utf16Offset && i < text.length) {
|
|
1979
|
+
const ch = text.codePointAt(i);
|
|
1980
|
+
if (ch === void 0) break;
|
|
1981
|
+
i += ch > 65535 ? 2 : 1;
|
|
1982
|
+
cp++;
|
|
1983
|
+
}
|
|
1984
|
+
return cp;
|
|
1985
|
+
}
|
|
1986
|
+
function applyWatermark(editor, watermark) {
|
|
1987
|
+
editor.transaction(() => {
|
|
1988
|
+
for (let i = 0; i < editor.chapters.length; i++) {
|
|
1989
|
+
editor.insertParagraph(i, 0, {
|
|
1990
|
+
text: `[mejiro-watermark] ${watermark.text}`,
|
|
1991
|
+
inlineAnnotations: []
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
function MejiroEditor({
|
|
1997
|
+
epubUrl,
|
|
1998
|
+
fonts,
|
|
1999
|
+
enableProofread = true,
|
|
2000
|
+
enableRuby = true,
|
|
2001
|
+
enableImages = true,
|
|
2002
|
+
enableExport = true,
|
|
2003
|
+
onBeforeExport,
|
|
2004
|
+
exportPolicy,
|
|
2005
|
+
assetResolver,
|
|
2006
|
+
onLoad,
|
|
2007
|
+
onExport,
|
|
2008
|
+
onError
|
|
2009
|
+
}) {
|
|
2010
|
+
const messages = useI18n();
|
|
2011
|
+
const imageInputRef = useRef10(null);
|
|
2012
|
+
const textareaRef = useRef10(null);
|
|
2013
|
+
const [editor, setEditor] = useState8(null);
|
|
2014
|
+
const [loading, setLoading] = useState8(false);
|
|
2015
|
+
const [error, setError] = useState8(null);
|
|
2016
|
+
const [revision, setRevision] = useState8(0);
|
|
2017
|
+
const [selection, setSelection] = useState8({ chapter: 0, paragraph: 0 });
|
|
2018
|
+
const [text, setText] = useState8("");
|
|
2019
|
+
const [rubyStart, setRubyStart] = useState8(0);
|
|
2020
|
+
const [rubyEnd, setRubyEnd] = useState8(1);
|
|
2021
|
+
const [rubyText, setRubyText] = useState8("");
|
|
2022
|
+
const loadRequestIdRef = useRef10(0);
|
|
2023
|
+
const onLoadRef = useRef10(onLoad);
|
|
2024
|
+
const onErrorRef = useRef10(onError);
|
|
2025
|
+
const book = editor?.book ?? null;
|
|
2026
|
+
const chapter = book?.chapters[selection.chapter] ?? null;
|
|
2027
|
+
const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
|
|
2028
|
+
const previewBook = book ? cloneBook(book) : null;
|
|
2029
|
+
void revision;
|
|
2030
|
+
useEffect7(() => {
|
|
2031
|
+
onLoadRef.current = onLoad;
|
|
2032
|
+
}, [onLoad]);
|
|
2033
|
+
useEffect7(() => {
|
|
2034
|
+
onErrorRef.current = onError;
|
|
2035
|
+
}, [onError]);
|
|
2036
|
+
const loadBufferForRequest = useCallback7(async (buffer, requestId) => {
|
|
2037
|
+
setLoading(true);
|
|
2038
|
+
setError(null);
|
|
2039
|
+
try {
|
|
2040
|
+
const next = await EditableEpub.load(buffer);
|
|
2041
|
+
if (requestId !== loadRequestIdRef.current) return;
|
|
2042
|
+
setEditor(next);
|
|
2043
|
+
setSelection({ chapter: 0, paragraph: 0 });
|
|
2044
|
+
setRevision((value) => value + 1);
|
|
2045
|
+
onLoadRef.current?.(next);
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
if (requestId === loadRequestIdRef.current) {
|
|
2048
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2049
|
+
setError(nextError);
|
|
2050
|
+
onErrorRef.current?.(nextError);
|
|
2051
|
+
}
|
|
2052
|
+
} finally {
|
|
2053
|
+
if (requestId === loadRequestIdRef.current) setLoading(false);
|
|
2054
|
+
}
|
|
2055
|
+
}, []);
|
|
2056
|
+
async function loadFile(file) {
|
|
2057
|
+
const requestId = ++loadRequestIdRef.current;
|
|
2058
|
+
setLoading(true);
|
|
2059
|
+
setError(null);
|
|
2060
|
+
try {
|
|
2061
|
+
await loadBufferForRequest(await file.arrayBuffer(), requestId);
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
if (requestId === loadRequestIdRef.current) {
|
|
2064
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2065
|
+
setError(nextError);
|
|
2066
|
+
onErrorRef.current?.(nextError);
|
|
2067
|
+
setLoading(false);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
useEffect7(() => {
|
|
2072
|
+
if (!epubUrl) return;
|
|
2073
|
+
let cancelled = false;
|
|
2074
|
+
const requestId = ++loadRequestIdRef.current;
|
|
2075
|
+
void (async () => {
|
|
2076
|
+
setLoading(true);
|
|
2077
|
+
setError(null);
|
|
2078
|
+
try {
|
|
2079
|
+
const res = await fetch(epubUrl);
|
|
2080
|
+
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
2081
|
+
const buffer = await res.arrayBuffer();
|
|
2082
|
+
if (cancelled || requestId !== loadRequestIdRef.current) return;
|
|
2083
|
+
await loadBufferForRequest(buffer, requestId);
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
if (!cancelled && requestId === loadRequestIdRef.current) {
|
|
2086
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
2087
|
+
setError(nextError);
|
|
2088
|
+
onErrorRef.current?.(nextError);
|
|
2089
|
+
}
|
|
2090
|
+
} finally {
|
|
2091
|
+
if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
|
|
2092
|
+
}
|
|
2093
|
+
})();
|
|
2094
|
+
return () => {
|
|
2095
|
+
cancelled = true;
|
|
2096
|
+
};
|
|
2097
|
+
}, [epubUrl, loadBufferForRequest]);
|
|
2098
|
+
useEffect7(() => {
|
|
2099
|
+
setText(paragraph?.text ?? "");
|
|
2100
|
+
setRubyStart(0);
|
|
2101
|
+
setRubyEnd(Math.min(1, [...paragraph?.text ?? ""].length));
|
|
2102
|
+
setRubyText("");
|
|
2103
|
+
}, [paragraph]);
|
|
2104
|
+
function selectParagraph(chapterIndex, paragraphIndex) {
|
|
2105
|
+
setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
|
|
2106
|
+
}
|
|
2107
|
+
function captureRubyRange() {
|
|
2108
|
+
const el = textareaRef.current;
|
|
2109
|
+
if (!el) return;
|
|
2110
|
+
const utf16Start = el.selectionStart ?? 0;
|
|
2111
|
+
const utf16End = el.selectionEnd ?? utf16Start;
|
|
2112
|
+
const start = utf16ToCodepoint(text, utf16Start);
|
|
2113
|
+
const end = utf16ToCodepoint(text, Math.max(utf16End, utf16Start + 1));
|
|
2114
|
+
setRubyStart(start);
|
|
2115
|
+
setRubyEnd(Math.max(start + 1, end));
|
|
2116
|
+
}
|
|
2117
|
+
function applyText() {
|
|
2118
|
+
if (!editor) return;
|
|
2119
|
+
editor.updateParagraph(selection.chapter, selection.paragraph, { text });
|
|
2120
|
+
setRevision((value) => value + 1);
|
|
2121
|
+
}
|
|
2122
|
+
function applyRuby() {
|
|
2123
|
+
if (!(editor && paragraph && rubyText.trim())) return;
|
|
2124
|
+
const len = [...text].length;
|
|
2125
|
+
const start = Math.max(0, Math.min(rubyStart, len));
|
|
2126
|
+
const end = Math.max(start + 1, Math.min(rubyEnd, len));
|
|
2127
|
+
const newRuby = {
|
|
2128
|
+
kind: "ruby",
|
|
2129
|
+
startIndex: start,
|
|
2130
|
+
endIndex: end,
|
|
2131
|
+
rubyText: rubyText.trim(),
|
|
2132
|
+
type: end - start === 1 ? "mono" : "group"
|
|
2133
|
+
};
|
|
2134
|
+
const nextInline = [
|
|
2135
|
+
...paragraph.inlineAnnotations.filter(
|
|
2136
|
+
(ann) => ann.endIndex <= start || ann.startIndex >= end
|
|
2137
|
+
),
|
|
2138
|
+
newRuby
|
|
2139
|
+
].sort((a, b) => a.startIndex - b.startIndex);
|
|
2140
|
+
editor.updateParagraph(selection.chapter, selection.paragraph, {
|
|
2141
|
+
text,
|
|
2142
|
+
inlineAnnotations: nextInline
|
|
2143
|
+
});
|
|
2144
|
+
setRubyText("");
|
|
2145
|
+
setRevision((value) => value + 1);
|
|
2146
|
+
}
|
|
2147
|
+
async function addImage(file) {
|
|
2148
|
+
if (!(editor && chapter)) return;
|
|
2149
|
+
editor.addImage(selection.chapter, {
|
|
2150
|
+
filename: file.name,
|
|
2151
|
+
mediaType: file.type || "application/octet-stream",
|
|
2152
|
+
data: await file.arrayBuffer(),
|
|
2153
|
+
alt: file.name,
|
|
2154
|
+
afterBlockId: paragraphBlockId(chapter, selection.paragraph)
|
|
2155
|
+
});
|
|
2156
|
+
setRevision((value) => value + 1);
|
|
2157
|
+
}
|
|
2158
|
+
async function exportEpub() {
|
|
2159
|
+
if (!editor) return;
|
|
2160
|
+
if (exportPolicy?.watermark) applyWatermark(editor, exportPolicy.watermark);
|
|
2161
|
+
let buffer = await editor.export(assetResolver ? { assetResolver } : void 0);
|
|
2162
|
+
if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
|
|
2163
|
+
const decision = await onBeforeExport?.(buffer);
|
|
2164
|
+
onExport?.(buffer);
|
|
2165
|
+
if (decision === false) return;
|
|
2166
|
+
if (exportPolicy?.allowDownload === false) return;
|
|
2167
|
+
const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
|
|
2168
|
+
const a = document.createElement("a");
|
|
2169
|
+
a.href = url;
|
|
2170
|
+
a.download = `${editor.title || "edited"}.epub`;
|
|
2171
|
+
a.click();
|
|
2172
|
+
URL.revokeObjectURL(url);
|
|
2173
|
+
}
|
|
2174
|
+
return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor", children: [
|
|
2175
|
+
/* @__PURE__ */ jsxs11("main", { className: "mejiro-editor-preview", children: [
|
|
2176
|
+
previewBook ? /* @__PURE__ */ jsx13(
|
|
2177
|
+
MejiroReader,
|
|
2178
|
+
{
|
|
2179
|
+
epub: previewBook,
|
|
2180
|
+
fonts,
|
|
2181
|
+
subtitle: messages.editorPreviewSubtitle,
|
|
2182
|
+
chapterNavMode: "panel",
|
|
2183
|
+
enableImageOverlay: false,
|
|
2184
|
+
enableSurfaceTap: false
|
|
2185
|
+
}
|
|
2186
|
+
) : /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (file) => void loadFile(file) }),
|
|
2187
|
+
loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-loading", children: messages.loading }),
|
|
2188
|
+
error && /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-error", children: error.message })
|
|
2189
|
+
] }),
|
|
2190
|
+
/* @__PURE__ */ jsxs11("aside", { className: "mejiro-editor-panel", children: [
|
|
2191
|
+
/* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-head", children: [
|
|
2192
|
+
/* @__PURE__ */ jsx13("span", { children: messages.editorTitle }),
|
|
2193
|
+
/* @__PURE__ */ jsx13("strong", { children: editor?.title ?? messages.editorNoBookLoaded }),
|
|
2194
|
+
editor?.author && /* @__PURE__ */ jsx13("small", { children: editor.author })
|
|
2195
|
+
] }),
|
|
2196
|
+
book && /* @__PURE__ */ jsxs11(Fragment4, { children: [
|
|
2197
|
+
/* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
|
|
2198
|
+
/* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorParagraphs }),
|
|
2199
|
+
/* @__PURE__ */ jsx13("div", { className: "mejiro-editor-paragraphs", children: book.chapters.map(
|
|
2200
|
+
(ch, ci) => ch.blocks.filter((b) => b.kind === "paragraph").map((block, pi) => /* @__PURE__ */ jsxs11(
|
|
2201
|
+
"button",
|
|
2202
|
+
{
|
|
2203
|
+
type: "button",
|
|
2204
|
+
className: selection.chapter === ci && selection.paragraph === pi ? "is-active" : "",
|
|
2205
|
+
onClick: () => selectParagraph(ci, pi),
|
|
2206
|
+
children: [
|
|
2207
|
+
/* @__PURE__ */ jsx13("span", { children: ch.title ?? format(messages.chapterN, { n: ci + 1 }) }),
|
|
2208
|
+
/* @__PURE__ */ jsx13("strong", { children: block.text.slice(0, 42) })
|
|
2209
|
+
]
|
|
2210
|
+
},
|
|
2211
|
+
`${ch.href}-${block.id}`
|
|
2212
|
+
))
|
|
2213
|
+
) })
|
|
2214
|
+
] }),
|
|
2215
|
+
enableProofread && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
|
|
2216
|
+
/* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
|
|
2217
|
+
/* @__PURE__ */ jsx13(
|
|
2218
|
+
"textarea",
|
|
2219
|
+
{
|
|
2220
|
+
ref: textareaRef,
|
|
2221
|
+
value: text,
|
|
2222
|
+
onChange: (event) => setText(event.target.value),
|
|
2223
|
+
onSelect: captureRubyRange
|
|
2224
|
+
}
|
|
2225
|
+
),
|
|
2226
|
+
/* @__PURE__ */ jsx13("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
|
|
2227
|
+
] }),
|
|
2228
|
+
enableRuby && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
|
|
2229
|
+
/* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
|
|
2230
|
+
/* @__PURE__ */ jsx13("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
|
|
2231
|
+
/* @__PURE__ */ jsx13("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
|
|
2232
|
+
start: rubyStart,
|
|
2233
|
+
end: rubyEnd,
|
|
2234
|
+
count: Math.max(0, rubyEnd - rubyStart)
|
|
2235
|
+
}) }),
|
|
2236
|
+
/* @__PURE__ */ jsx13(
|
|
2237
|
+
"input",
|
|
2238
|
+
{
|
|
2239
|
+
value: rubyText,
|
|
2240
|
+
placeholder: messages.editorRubyPlaceholder,
|
|
2241
|
+
onChange: (event) => setRubyText(event.target.value)
|
|
2242
|
+
}
|
|
2243
|
+
),
|
|
2244
|
+
/* @__PURE__ */ jsx13("button", { type: "button", className: "mejiro-editor-primary", onClick: applyRuby, children: messages.editorApplyRuby })
|
|
2245
|
+
] }),
|
|
2246
|
+
enableImages && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
|
|
2247
|
+
/* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorImages }),
|
|
2248
|
+
/* @__PURE__ */ jsx13("button", { type: "button", onClick: () => imageInputRef.current?.click(), children: messages.editorInsertImageAfterParagraph }),
|
|
2249
|
+
/* @__PURE__ */ jsx13(
|
|
2250
|
+
"input",
|
|
2251
|
+
{
|
|
2252
|
+
ref: imageInputRef,
|
|
2253
|
+
type: "file",
|
|
2254
|
+
accept: "image/*",
|
|
2255
|
+
hidden: true,
|
|
2256
|
+
onChange: (event) => {
|
|
2257
|
+
const file = event.target.files?.[0];
|
|
2258
|
+
if (file) void addImage(file);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
)
|
|
2262
|
+
] }),
|
|
2263
|
+
enableExport && /* @__PURE__ */ jsx13(
|
|
2264
|
+
"button",
|
|
2265
|
+
{
|
|
2266
|
+
type: "button",
|
|
2267
|
+
className: "mejiro-editor-export",
|
|
2268
|
+
onClick: () => void exportEpub(),
|
|
2269
|
+
children: messages.editorExportEpub
|
|
2270
|
+
}
|
|
2271
|
+
)
|
|
2272
|
+
] })
|
|
2273
|
+
] })
|
|
2274
|
+
] });
|
|
2275
|
+
}
|
|
2276
|
+
function paragraphBlockId(chapter, paragraphIndex) {
|
|
2277
|
+
let current = 0;
|
|
2278
|
+
for (const block of chapter.blocks) {
|
|
2279
|
+
if (block.kind !== "paragraph") continue;
|
|
2280
|
+
if (current === paragraphIndex) return block.id;
|
|
2281
|
+
current++;
|
|
2282
|
+
}
|
|
2283
|
+
return void 0;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// src/MejiroManuscriptEditor.tsx
|
|
2287
|
+
import { EpubProject } from "@libraz/mejiro/epub";
|
|
2288
|
+
import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef12, useState as useState10 } from "react";
|
|
2289
|
+
|
|
2290
|
+
// src/useManuscriptDraft.ts
|
|
2291
|
+
import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef11, useState as useState9 } from "react";
|
|
2292
|
+
var DEFAULT_DELAY = 800;
|
|
2293
|
+
function defaultChapter(index) {
|
|
2294
|
+
return { id: `chapter-${Date.now()}-${index}`, title: `\u7B2C${index + 1}\u8A71`, body: "" };
|
|
2295
|
+
}
|
|
2296
|
+
function useManuscriptDraft(options = {}) {
|
|
2297
|
+
const { onAutosave, autosaveDelay = DEFAULT_DELAY } = options;
|
|
2298
|
+
const [chapters, setChaptersState] = useState9(
|
|
2299
|
+
() => options.initialChapters?.length ? options.initialChapters : [defaultChapter(0)]
|
|
2300
|
+
);
|
|
2301
|
+
const [selected, setSelectedState] = useState9(0);
|
|
2302
|
+
const saveRef = useRef11(onAutosave);
|
|
2303
|
+
saveRef.current = onAutosave;
|
|
2304
|
+
const didMountRef = useRef11(false);
|
|
2305
|
+
useEffect8(() => {
|
|
2306
|
+
if (!didMountRef.current) {
|
|
2307
|
+
didMountRef.current = true;
|
|
2308
|
+
return void 0;
|
|
2309
|
+
}
|
|
2310
|
+
const callback = saveRef.current;
|
|
2311
|
+
if (!callback) return void 0;
|
|
2312
|
+
const timer = setTimeout(() => {
|
|
2313
|
+
void callback(chapters);
|
|
2314
|
+
}, autosaveDelay);
|
|
2315
|
+
return () => clearTimeout(timer);
|
|
2316
|
+
}, [chapters, autosaveDelay]);
|
|
2317
|
+
const setSelected = useCallback8(
|
|
2318
|
+
(index) => {
|
|
2319
|
+
setSelectedState(() => Math.max(0, Math.min(index, chapters.length - 1)));
|
|
2320
|
+
},
|
|
2321
|
+
[chapters.length]
|
|
2322
|
+
);
|
|
2323
|
+
const setChapters = useCallback8(
|
|
2324
|
+
(next) => {
|
|
2325
|
+
const normalized = next.length ? next : [defaultChapter(0)];
|
|
2326
|
+
setChaptersState(normalized);
|
|
2327
|
+
setSelectedState((current) => {
|
|
2328
|
+
const selectedId = chapters[current]?.id;
|
|
2329
|
+
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
2330
|
+
return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
|
|
2331
|
+
});
|
|
2332
|
+
},
|
|
2333
|
+
[chapters]
|
|
2334
|
+
);
|
|
2335
|
+
const patchChapter = useCallback8((index, patch) => {
|
|
2336
|
+
setChaptersState(
|
|
2337
|
+
(current) => current.map((chapter, i) => i === index ? { ...chapter, ...patch } : chapter)
|
|
2338
|
+
);
|
|
2339
|
+
}, []);
|
|
2340
|
+
const addChapter = useCallback8((chapter = {}) => {
|
|
2341
|
+
setChaptersState((current) => {
|
|
2342
|
+
const generated = defaultChapter(current.length);
|
|
2343
|
+
const next = [
|
|
2344
|
+
...current,
|
|
2345
|
+
{
|
|
2346
|
+
id: chapter.id ?? generated.id,
|
|
2347
|
+
title: chapter.title ?? generated.title,
|
|
2348
|
+
body: chapter.body ?? generated.body
|
|
2349
|
+
}
|
|
2350
|
+
];
|
|
2351
|
+
setSelectedState(next.length - 1);
|
|
2352
|
+
return next;
|
|
2353
|
+
});
|
|
2354
|
+
}, []);
|
|
2355
|
+
const removeChapter = useCallback8((index) => {
|
|
2356
|
+
setChaptersState((current) => {
|
|
2357
|
+
if (current.length <= 1) return current;
|
|
2358
|
+
const next = current.filter((_, i) => i !== index);
|
|
2359
|
+
setSelectedState((prev) => {
|
|
2360
|
+
if (prev === index) return Math.max(0, Math.min(index, next.length - 1));
|
|
2361
|
+
if (index < prev) return prev - 1;
|
|
2362
|
+
return Math.max(0, Math.min(prev, next.length - 1));
|
|
2363
|
+
});
|
|
2364
|
+
return next;
|
|
2365
|
+
});
|
|
2366
|
+
}, []);
|
|
2367
|
+
const reorderChapters = useCallback8((from, to) => {
|
|
2368
|
+
setChaptersState((current) => {
|
|
2369
|
+
if (from < 0 || from >= current.length) return current;
|
|
2370
|
+
const next = [...current];
|
|
2371
|
+
const [moved] = next.splice(from, 1);
|
|
2372
|
+
const target = Math.max(0, Math.min(next.length, to));
|
|
2373
|
+
next.splice(target, 0, moved);
|
|
2374
|
+
setSelectedState((prev) => {
|
|
2375
|
+
if (prev === from) return target;
|
|
2376
|
+
if (from < prev && target >= prev) return prev - 1;
|
|
2377
|
+
if (from > prev && target <= prev) return prev + 1;
|
|
2378
|
+
return prev;
|
|
2379
|
+
});
|
|
2380
|
+
return next;
|
|
2381
|
+
});
|
|
2382
|
+
}, []);
|
|
2383
|
+
return {
|
|
2384
|
+
chapters,
|
|
2385
|
+
selected,
|
|
2386
|
+
setSelected,
|
|
2387
|
+
setChapters,
|
|
2388
|
+
patchChapter,
|
|
2389
|
+
addChapter,
|
|
2390
|
+
removeChapter,
|
|
2391
|
+
reorderChapters
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// src/MejiroManuscriptEditor.tsx
|
|
2396
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2397
|
+
function downloadEpub(buffer, title) {
|
|
2398
|
+
const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
|
|
2399
|
+
const a = document.createElement("a");
|
|
2400
|
+
a.href = url;
|
|
2401
|
+
a.download = `${title || "book"}.epub`;
|
|
2402
|
+
a.click();
|
|
2403
|
+
URL.revokeObjectURL(url);
|
|
2404
|
+
}
|
|
2405
|
+
function coverExtension(mediaType) {
|
|
2406
|
+
switch (mediaType) {
|
|
2407
|
+
case "image/gif":
|
|
2408
|
+
return ".gif";
|
|
2409
|
+
case "image/jpeg":
|
|
2410
|
+
return ".jpg";
|
|
2411
|
+
case "image/png":
|
|
2412
|
+
return ".png";
|
|
2413
|
+
case "image/svg+xml":
|
|
2414
|
+
return ".svg";
|
|
2415
|
+
case "image/webp":
|
|
2416
|
+
return ".webp";
|
|
2417
|
+
default:
|
|
2418
|
+
return ".bin";
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function coverAssetHref(file) {
|
|
2422
|
+
const filename = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2423
|
+
return `OPS/Images/${filename && !/^\.+$/u.test(filename) ? filename : `cover${coverExtension(file.type)}`}`;
|
|
2424
|
+
}
|
|
2425
|
+
function defaultChapter2(messages) {
|
|
2426
|
+
return {
|
|
2427
|
+
id: `chapter-${Date.now()}-0`,
|
|
2428
|
+
title: format(messages.manuscriptDefaultChapterTitle, { n: 1 }),
|
|
2429
|
+
body: messages.manuscriptDefaultBody
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
function MejiroManuscriptEditor({
|
|
2433
|
+
fonts,
|
|
2434
|
+
title: titleProp,
|
|
2435
|
+
onTitleChange,
|
|
2436
|
+
author: authorProp,
|
|
2437
|
+
onAuthorChange,
|
|
2438
|
+
cover: coverProp,
|
|
2439
|
+
onCoverChange,
|
|
2440
|
+
chapters: initialChapters,
|
|
2441
|
+
onAutosave,
|
|
2442
|
+
autosaveDelay,
|
|
2443
|
+
previewProps,
|
|
2444
|
+
assetResolver,
|
|
2445
|
+
onExport
|
|
2446
|
+
}) {
|
|
2447
|
+
const messages = useI18n();
|
|
2448
|
+
const coverInputRef = useRef12(null);
|
|
2449
|
+
const bodyTextareaRef = useRef12(null);
|
|
2450
|
+
const titleControlled = onTitleChange !== void 0;
|
|
2451
|
+
const authorControlled = onAuthorChange !== void 0;
|
|
2452
|
+
const coverControlled = onCoverChange !== void 0;
|
|
2453
|
+
const [titleState, setTitleState] = useState10(titleProp ?? messages.manuscriptDefaultTitle);
|
|
2454
|
+
const [authorState, setAuthorState] = useState10(authorProp ?? "");
|
|
2455
|
+
const [coverState, setCoverState] = useState10(coverProp ?? null);
|
|
2456
|
+
const title = titleControlled ? titleProp ?? "" : titleState;
|
|
2457
|
+
const author = authorControlled ? authorProp ?? "" : authorState;
|
|
2458
|
+
const cover = coverControlled ? coverProp ?? null : coverState;
|
|
2459
|
+
useEffect9(() => {
|
|
2460
|
+
if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
|
|
2461
|
+
}, [titleControlled, titleProp]);
|
|
2462
|
+
useEffect9(() => {
|
|
2463
|
+
if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
|
|
2464
|
+
}, [authorControlled, authorProp]);
|
|
2465
|
+
useEffect9(() => {
|
|
2466
|
+
if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
|
|
2467
|
+
}, [coverControlled, coverProp]);
|
|
2468
|
+
const setTitle = useCallback9(
|
|
2469
|
+
(next) => {
|
|
2470
|
+
if (!titleControlled) setTitleState(next);
|
|
2471
|
+
onTitleChange?.(next);
|
|
2472
|
+
},
|
|
2473
|
+
[titleControlled, onTitleChange]
|
|
2474
|
+
);
|
|
2475
|
+
const setAuthor = useCallback9(
|
|
2476
|
+
(next) => {
|
|
2477
|
+
if (!authorControlled) setAuthorState(next);
|
|
2478
|
+
onAuthorChange?.(next);
|
|
2479
|
+
},
|
|
2480
|
+
[authorControlled, onAuthorChange]
|
|
2481
|
+
);
|
|
2482
|
+
const setCover = useCallback9(
|
|
2483
|
+
(next) => {
|
|
2484
|
+
if (!coverControlled) setCoverState(next);
|
|
2485
|
+
onCoverChange?.(next);
|
|
2486
|
+
},
|
|
2487
|
+
[coverControlled, onCoverChange]
|
|
2488
|
+
);
|
|
2489
|
+
const draft = useManuscriptDraft({
|
|
2490
|
+
initialChapters: initialChapters?.length ? initialChapters : [defaultChapter2(messages)],
|
|
2491
|
+
onAutosave,
|
|
2492
|
+
autosaveDelay
|
|
2493
|
+
});
|
|
2494
|
+
const {
|
|
2495
|
+
chapters,
|
|
2496
|
+
selected,
|
|
2497
|
+
setSelected,
|
|
2498
|
+
patchChapter,
|
|
2499
|
+
addChapter,
|
|
2500
|
+
removeChapter,
|
|
2501
|
+
reorderChapters
|
|
2502
|
+
} = draft;
|
|
2503
|
+
const current = chapters[selected] ?? chapters[0];
|
|
2504
|
+
const [draggingIndex, setDraggingIndex] = useState10(null);
|
|
2505
|
+
function wrapSelection(open, close) {
|
|
2506
|
+
const el = bodyTextareaRef.current;
|
|
2507
|
+
if (!el) return;
|
|
2508
|
+
const start = el.selectionStart ?? el.value.length;
|
|
2509
|
+
const end = el.selectionEnd ?? start;
|
|
2510
|
+
const before = el.value.slice(0, start);
|
|
2511
|
+
const middle = el.value.slice(start, end);
|
|
2512
|
+
const after = el.value.slice(end);
|
|
2513
|
+
const next = `${before}${open}${middle}${close}${after}`;
|
|
2514
|
+
patchChapter(selected, { body: next });
|
|
2515
|
+
requestAnimationFrame(() => {
|
|
2516
|
+
if (!bodyTextareaRef.current) return;
|
|
2517
|
+
const caret = start + open.length + middle.length;
|
|
2518
|
+
bodyTextareaRef.current.focus();
|
|
2519
|
+
bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
const exportEpub = useCallback9(async () => {
|
|
2523
|
+
const project = EpubProject.fromManuscript({
|
|
2524
|
+
metadata: { title, author: author || void 0 },
|
|
2525
|
+
chapters: chapters.map((chapter) => ({
|
|
2526
|
+
id: chapter.id,
|
|
2527
|
+
title: chapter.title || messages.untitled,
|
|
2528
|
+
body: chapter.body
|
|
2529
|
+
}))
|
|
2530
|
+
});
|
|
2531
|
+
if (cover) {
|
|
2532
|
+
project.setCover({
|
|
2533
|
+
href: coverAssetHref(cover),
|
|
2534
|
+
mediaType: cover.type || void 0,
|
|
2535
|
+
data: await cover.arrayBuffer()
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
|
|
2539
|
+
onExport?.(buffer);
|
|
2540
|
+
downloadEpub(buffer, title);
|
|
2541
|
+
}, [assetResolver, author, chapters, cover, messages.untitled, onExport, title]);
|
|
2542
|
+
return /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor mejiro-manuscript-editor", children: [
|
|
2543
|
+
/* @__PURE__ */ jsx14("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx14(
|
|
2544
|
+
MejiroReader,
|
|
2545
|
+
{
|
|
2546
|
+
subtitle: messages.manuscriptPreviewSubtitle,
|
|
2547
|
+
chapterNavMode: "panel",
|
|
2548
|
+
enableSurfaceTap: false,
|
|
2549
|
+
...previewProps,
|
|
2550
|
+
manuscript: chapters.map((chapter) => ({
|
|
2551
|
+
id: chapter.id,
|
|
2552
|
+
title: chapter.title || messages.untitled,
|
|
2553
|
+
body: chapter.body
|
|
2554
|
+
})),
|
|
2555
|
+
fonts,
|
|
2556
|
+
chapter: selected,
|
|
2557
|
+
onChapterChange: setSelected,
|
|
2558
|
+
enableImageOverlay: false
|
|
2559
|
+
}
|
|
2560
|
+
) }),
|
|
2561
|
+
/* @__PURE__ */ jsxs12("aside", { className: "mejiro-editor-panel", children: [
|
|
2562
|
+
/* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-head", children: [
|
|
2563
|
+
/* @__PURE__ */ jsx14("span", { children: messages.manuscriptTitle }),
|
|
2564
|
+
/* @__PURE__ */ jsx14("strong", { children: title }),
|
|
2565
|
+
/* @__PURE__ */ jsx14("small", { children: messages.manuscriptRubyHint })
|
|
2566
|
+
] }),
|
|
2567
|
+
/* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
|
|
2568
|
+
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
|
|
2569
|
+
/* @__PURE__ */ jsx14("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
|
|
2570
|
+
/* @__PURE__ */ jsx14("input", { value: author, onChange: (event) => setAuthor(event.target.value) }),
|
|
2571
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => coverInputRef.current?.click(), children: cover ? cover.name : messages.manuscriptChooseCoverImage }),
|
|
2572
|
+
/* @__PURE__ */ jsx14(
|
|
2573
|
+
"input",
|
|
2574
|
+
{
|
|
2575
|
+
ref: coverInputRef,
|
|
2576
|
+
type: "file",
|
|
2577
|
+
accept: "image/*",
|
|
2578
|
+
hidden: true,
|
|
2579
|
+
onChange: (event) => setCover(event.target.files?.[0] ?? null)
|
|
2580
|
+
}
|
|
2581
|
+
)
|
|
2582
|
+
] }),
|
|
2583
|
+
/* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
|
|
2584
|
+
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptChapters }),
|
|
2585
|
+
/* @__PURE__ */ jsx14("div", { className: "mejiro-editor-paragraphs", children: chapters.map((chapter, index) => /* @__PURE__ */ jsxs12(
|
|
2586
|
+
"button",
|
|
2587
|
+
{
|
|
2588
|
+
type: "button",
|
|
2589
|
+
className: selected === index ? "is-active" : "",
|
|
2590
|
+
draggable: true,
|
|
2591
|
+
"aria-label": format(messages.manuscriptReorderHandle, {
|
|
2592
|
+
title: chapter.title || messages.untitled
|
|
2593
|
+
}),
|
|
2594
|
+
onClick: () => setSelected(index),
|
|
2595
|
+
onDragStart: (event) => {
|
|
2596
|
+
setDraggingIndex(index);
|
|
2597
|
+
event.dataTransfer.effectAllowed = "move";
|
|
2598
|
+
},
|
|
2599
|
+
onDragEnd: () => setDraggingIndex(null),
|
|
2600
|
+
onDragOver: (event) => {
|
|
2601
|
+
if (draggingIndex === null || draggingIndex === index) return;
|
|
2602
|
+
event.preventDefault();
|
|
2603
|
+
event.dataTransfer.dropEffect = "move";
|
|
2604
|
+
},
|
|
2605
|
+
onDrop: (event) => {
|
|
2606
|
+
event.preventDefault();
|
|
2607
|
+
if (draggingIndex !== null && draggingIndex !== index) {
|
|
2608
|
+
reorderChapters(draggingIndex, index);
|
|
2609
|
+
}
|
|
2610
|
+
setDraggingIndex(null);
|
|
2611
|
+
},
|
|
2612
|
+
"data-dragging": draggingIndex === index ? "" : void 0,
|
|
2613
|
+
children: [
|
|
2614
|
+
/* @__PURE__ */ jsx14("span", { children: format(messages.chapterN, { n: index + 1 }) }),
|
|
2615
|
+
/* @__PURE__ */ jsx14("strong", { children: chapter.title || messages.untitled })
|
|
2616
|
+
]
|
|
2617
|
+
},
|
|
2618
|
+
chapter.id
|
|
2619
|
+
)) }),
|
|
2620
|
+
/* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid", children: [
|
|
2621
|
+
/* @__PURE__ */ jsx14(
|
|
2622
|
+
"button",
|
|
2623
|
+
{
|
|
2624
|
+
type: "button",
|
|
2625
|
+
onClick: () => addChapter({
|
|
2626
|
+
title: format(messages.manuscriptDefaultChapterTitle, {
|
|
2627
|
+
n: chapters.length + 1
|
|
2628
|
+
})
|
|
2629
|
+
}),
|
|
2630
|
+
children: messages.manuscriptAddChapter
|
|
2631
|
+
}
|
|
2632
|
+
),
|
|
2633
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => removeChapter(selected), children: messages.manuscriptRemove })
|
|
2634
|
+
] })
|
|
2635
|
+
] }),
|
|
2636
|
+
current && /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
|
|
2637
|
+
/* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptDraft }),
|
|
2638
|
+
/* @__PURE__ */ jsx14(
|
|
2639
|
+
"input",
|
|
2640
|
+
{
|
|
2641
|
+
value: current.title,
|
|
2642
|
+
onChange: (event) => patchChapter(selected, { title: event.target.value })
|
|
2643
|
+
}
|
|
2644
|
+
),
|
|
2645
|
+
/* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
|
|
2646
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
|
|
2647
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
|
|
2648
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
|
|
2649
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("**", "**"), children: messages.manuscriptStrong })
|
|
2650
|
+
] }),
|
|
2651
|
+
/* @__PURE__ */ jsx14(
|
|
2652
|
+
"textarea",
|
|
2653
|
+
{
|
|
2654
|
+
ref: bodyTextareaRef,
|
|
2655
|
+
className: "mejiro-editor-manuscript",
|
|
2656
|
+
value: current.body,
|
|
2657
|
+
onChange: (event) => patchChapter(selected, { body: event.target.value })
|
|
2658
|
+
}
|
|
2659
|
+
)
|
|
2660
|
+
] }),
|
|
2661
|
+
/* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-export", onClick: () => void exportEpub(), children: messages.editorExportEpub })
|
|
2662
|
+
] })
|
|
2663
|
+
] });
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// src/MejiroNotationHighlighter.tsx
|
|
2667
|
+
import { tokenizeManuscriptSource } from "@libraz/mejiro";
|
|
2668
|
+
import {
|
|
2669
|
+
forwardRef as forwardRef2,
|
|
2670
|
+
useImperativeHandle as useImperativeHandle2,
|
|
2671
|
+
useMemo as useMemo5,
|
|
2672
|
+
useRef as useRef13
|
|
2673
|
+
} from "react";
|
|
2674
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2675
|
+
var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
|
|
2676
|
+
value,
|
|
2677
|
+
onChange,
|
|
2678
|
+
dialect = "mejiro",
|
|
2679
|
+
wrapperClassName,
|
|
2680
|
+
wrapperStyle,
|
|
2681
|
+
className,
|
|
2682
|
+
style,
|
|
2683
|
+
onScroll,
|
|
2684
|
+
...rest
|
|
2685
|
+
}, ref) {
|
|
2686
|
+
const textareaRef = useRef13(null);
|
|
2687
|
+
const overlayRef = useRef13(null);
|
|
2688
|
+
useImperativeHandle2(ref, () => textareaRef.current, []);
|
|
2689
|
+
const segments = useMemo5(() => buildSegments(value, dialect), [value, dialect]);
|
|
2690
|
+
return /* @__PURE__ */ jsxs13(
|
|
2691
|
+
"div",
|
|
2692
|
+
{
|
|
2693
|
+
className: `mejiro-notation-highlighter${wrapperClassName ? ` ${wrapperClassName}` : ""}`,
|
|
2694
|
+
style: wrapperStyle,
|
|
2695
|
+
children: [
|
|
2696
|
+
/* @__PURE__ */ jsxs13(
|
|
2697
|
+
"div",
|
|
2698
|
+
{
|
|
2699
|
+
ref: overlayRef,
|
|
2700
|
+
className: "mejiro-notation-overlay",
|
|
2701
|
+
"aria-hidden": "true",
|
|
2702
|
+
children: [
|
|
2703
|
+
segments.map((segment) => /* @__PURE__ */ jsx15(
|
|
2704
|
+
"span",
|
|
2705
|
+
{
|
|
2706
|
+
className: segment.kind ? `mejiro-notation-token mejiro-notation-${segment.kind}` : void 0,
|
|
2707
|
+
"data-token": segment.kind,
|
|
2708
|
+
children: segment.text
|
|
2709
|
+
},
|
|
2710
|
+
segment.key
|
|
2711
|
+
)),
|
|
2712
|
+
"\u200B"
|
|
2713
|
+
]
|
|
2714
|
+
}
|
|
2715
|
+
),
|
|
2716
|
+
/* @__PURE__ */ jsx15(
|
|
2717
|
+
"textarea",
|
|
2718
|
+
{
|
|
2719
|
+
...rest,
|
|
2720
|
+
ref: textareaRef,
|
|
2721
|
+
className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
|
|
2722
|
+
style,
|
|
2723
|
+
value,
|
|
2724
|
+
onChange: (event) => onChange(event.target.value),
|
|
2725
|
+
onScroll: (event) => {
|
|
2726
|
+
if (overlayRef.current) {
|
|
2727
|
+
overlayRef.current.scrollTop = event.currentTarget.scrollTop;
|
|
2728
|
+
overlayRef.current.scrollLeft = event.currentTarget.scrollLeft;
|
|
2729
|
+
}
|
|
2730
|
+
onScroll?.(event);
|
|
2731
|
+
},
|
|
2732
|
+
spellCheck: false
|
|
2733
|
+
}
|
|
2734
|
+
)
|
|
2735
|
+
]
|
|
2736
|
+
}
|
|
2737
|
+
);
|
|
2738
|
+
});
|
|
2739
|
+
function buildSegments(text, dialect) {
|
|
2740
|
+
const tokens = tokenizeManuscriptSource(text, dialect);
|
|
2741
|
+
const segments = [];
|
|
2742
|
+
let cursor = 0;
|
|
2743
|
+
for (const [i, token] of tokens.entries()) {
|
|
2744
|
+
if (token.start > cursor) {
|
|
2745
|
+
segments.push({ key: `t-${i}-pre`, text: text.slice(cursor, token.start) });
|
|
2746
|
+
}
|
|
2747
|
+
segments.push({
|
|
2748
|
+
key: `t-${i}`,
|
|
2749
|
+
text: text.slice(token.start, token.end),
|
|
2750
|
+
kind: token.kind
|
|
2751
|
+
});
|
|
2752
|
+
cursor = token.end;
|
|
2753
|
+
}
|
|
2754
|
+
if (cursor < text.length) {
|
|
2755
|
+
segments.push({ key: "tail", text: text.slice(cursor) });
|
|
2756
|
+
}
|
|
2757
|
+
if (segments.length === 0) {
|
|
2758
|
+
segments.push({ key: "empty", text });
|
|
2759
|
+
}
|
|
2760
|
+
return segments;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// src/MejiroShelf.tsx
|
|
2764
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2765
|
+
function MejiroShelf({
|
|
2766
|
+
volumes,
|
|
2767
|
+
currentId,
|
|
2768
|
+
onSelect,
|
|
2769
|
+
title
|
|
2770
|
+
}) {
|
|
2771
|
+
const messages = useI18n();
|
|
2772
|
+
const resolvedTitle = title ?? messages.shelfTitle;
|
|
2773
|
+
return /* @__PURE__ */ jsxs14("section", { className: "mejiro-shelf", "aria-label": resolvedTitle, children: [
|
|
2774
|
+
/* @__PURE__ */ jsx16("header", { className: "mejiro-shelf-header", children: /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-title", children: resolvedTitle }) }),
|
|
2775
|
+
/* @__PURE__ */ jsx16("ul", { className: "mejiro-shelf-grid", children: volumes.map((v) => /* @__PURE__ */ jsx16("li", { className: "mejiro-shelf-item", children: /* @__PURE__ */ jsxs14(
|
|
2776
|
+
"button",
|
|
2777
|
+
{
|
|
2778
|
+
type: "button",
|
|
2779
|
+
className: `mejiro-shelf-card${v.id === currentId ? " is-active" : ""}`,
|
|
2780
|
+
onClick: () => onSelect?.(v),
|
|
2781
|
+
children: [
|
|
2782
|
+
v.cover ? /* @__PURE__ */ jsx16(
|
|
2783
|
+
"span",
|
|
2784
|
+
{
|
|
2785
|
+
className: "mejiro-shelf-cover",
|
|
2786
|
+
style: { backgroundImage: `url(${JSON.stringify(v.cover)})` },
|
|
2787
|
+
"aria-hidden": "true"
|
|
2788
|
+
}
|
|
2789
|
+
) : /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-cover mejiro-shelf-cover--blank", "aria-hidden": "true" }),
|
|
2790
|
+
/* @__PURE__ */ jsxs14("span", { className: "mejiro-shelf-meta", children: [
|
|
2791
|
+
/* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-label", children: v.label }),
|
|
2792
|
+
v.author && /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-author", children: v.author })
|
|
2793
|
+
] })
|
|
2794
|
+
]
|
|
2795
|
+
}
|
|
2796
|
+
) }, v.id)) })
|
|
2797
|
+
] });
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// src/MejiroToc.tsx
|
|
2801
|
+
import { useMemo as useMemo6, useState as useState11 } from "react";
|
|
2802
|
+
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2803
|
+
function buildEntries(epub, messages) {
|
|
2804
|
+
return epub.chapters.map((ch, i) => {
|
|
2805
|
+
const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
|
|
2806
|
+
const headings = ch.paragraphs.filter((p) => p.headingLevel && p.text.trim() && p.text.trim() !== title).map((p) => p.text.trim());
|
|
2807
|
+
return { index: i, title, headings };
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
function MejiroToc({
|
|
2811
|
+
epub,
|
|
2812
|
+
currentAnchor,
|
|
2813
|
+
searchable = false,
|
|
2814
|
+
title,
|
|
2815
|
+
searchPlaceholder,
|
|
2816
|
+
onSelect
|
|
2817
|
+
}) {
|
|
2818
|
+
const messages = useI18n();
|
|
2819
|
+
const resolvedTitle = title ?? messages.tocTitle;
|
|
2820
|
+
const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
|
|
2821
|
+
const [query, setQuery] = useState11("");
|
|
2822
|
+
const entries = useMemo6(() => buildEntries(epub, messages), [epub, messages]);
|
|
2823
|
+
const filtered = useMemo6(() => {
|
|
2824
|
+
if (!query) return entries;
|
|
2825
|
+
const needle = query.toLowerCase();
|
|
2826
|
+
return entries.filter(
|
|
2827
|
+
(e) => e.title.toLowerCase().includes(needle) || e.headings.some((h) => h.toLowerCase().includes(needle))
|
|
2828
|
+
);
|
|
2829
|
+
}, [entries, query]);
|
|
2830
|
+
const activeIndex = currentAnchor?.chapter ?? -1;
|
|
2831
|
+
return /* @__PURE__ */ jsxs15("nav", { className: "mejiro-toc", "aria-label": resolvedTitle, children: [
|
|
2832
|
+
/* @__PURE__ */ jsxs15("header", { className: "mejiro-toc-header", children: [
|
|
2833
|
+
/* @__PURE__ */ jsx17("span", { className: "mejiro-toc-title", children: resolvedTitle }),
|
|
2834
|
+
epub.author && /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-subtitle", children: epub.author })
|
|
2835
|
+
] }),
|
|
2836
|
+
searchable && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-search", children: /* @__PURE__ */ jsx17(
|
|
2837
|
+
"input",
|
|
2838
|
+
{
|
|
2839
|
+
type: "search",
|
|
2840
|
+
value: query,
|
|
2841
|
+
placeholder: resolvedSearchPlaceholder,
|
|
2842
|
+
onChange: (e) => setQuery(e.target.value),
|
|
2843
|
+
"aria-label": resolvedSearchPlaceholder
|
|
2844
|
+
}
|
|
2845
|
+
) }),
|
|
2846
|
+
/* @__PURE__ */ jsx17("ol", { className: "mejiro-toc-list", children: filtered.map((entry) => /* @__PURE__ */ jsxs15("li", { className: "mejiro-toc-item", children: [
|
|
2847
|
+
/* @__PURE__ */ jsxs15(
|
|
2848
|
+
"button",
|
|
2849
|
+
{
|
|
2850
|
+
type: "button",
|
|
2851
|
+
className: `mejiro-toc-link${entry.index === activeIndex ? " is-active" : ""}`,
|
|
2852
|
+
"aria-current": entry.index === activeIndex ? "true" : void 0,
|
|
2853
|
+
onClick: () => onSelect?.(entry.index),
|
|
2854
|
+
children: [
|
|
2855
|
+
/* @__PURE__ */ jsx17("span", { className: "mejiro-toc-num", children: String(entry.index + 1).padStart(2, "0") }),
|
|
2856
|
+
/* @__PURE__ */ jsx17("span", { className: "mejiro-toc-label", children: entry.title })
|
|
2857
|
+
]
|
|
2858
|
+
}
|
|
2859
|
+
),
|
|
2860
|
+
entry.headings.length > 0 && /* @__PURE__ */ jsx17("ul", { className: "mejiro-toc-subheads", children: entry.headings.slice(0, 5).map((h) => /* @__PURE__ */ jsx17("li", { className: "mejiro-toc-subhead", children: h }, h)) })
|
|
2861
|
+
] }, entry.index)) }),
|
|
2862
|
+
filtered.length === 0 && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
|
|
2863
|
+
] });
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// src/useAnnotations.ts
|
|
2867
|
+
import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState12 } from "react";
|
|
2868
|
+
var STORAGE_VERSION = 1;
|
|
2869
|
+
function resolveDefaultStorage() {
|
|
2870
|
+
if (typeof globalThis === "undefined") return null;
|
|
2871
|
+
const w = globalThis.localStorage;
|
|
2872
|
+
return w ?? null;
|
|
2873
|
+
}
|
|
2874
|
+
function parse(raw) {
|
|
2875
|
+
if (!raw) return [];
|
|
2876
|
+
try {
|
|
2877
|
+
const parsed = JSON.parse(raw);
|
|
2878
|
+
if (parsed && Array.isArray(parsed.annotations)) return parsed.annotations;
|
|
2879
|
+
} catch {
|
|
2880
|
+
}
|
|
2881
|
+
return [];
|
|
2882
|
+
}
|
|
2883
|
+
function serialize(annotations) {
|
|
2884
|
+
const payload = { version: STORAGE_VERSION, annotations: [...annotations] };
|
|
2885
|
+
return JSON.stringify(payload);
|
|
2886
|
+
}
|
|
2887
|
+
function sort(annotations) {
|
|
2888
|
+
return [...annotations].sort((a, b) => {
|
|
2889
|
+
if (a.chapter !== b.chapter) return a.chapter - b.chapter;
|
|
2890
|
+
if (a.start.paragraph !== b.start.paragraph) return a.start.paragraph - b.start.paragraph;
|
|
2891
|
+
return a.start.charIndex - b.start.charIndex;
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
function makeId() {
|
|
2895
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2896
|
+
return crypto.randomUUID();
|
|
2897
|
+
}
|
|
2898
|
+
return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2899
|
+
}
|
|
2900
|
+
function useAnnotations(options) {
|
|
2901
|
+
const { key, throttleMs = 250 } = options;
|
|
2902
|
+
const storage = options.storage ?? resolveDefaultStorage();
|
|
2903
|
+
const [annotations, setAnnotations] = useState12(() => {
|
|
2904
|
+
if (!storage) return [];
|
|
2905
|
+
try {
|
|
2906
|
+
return sort(parse(storage.getItem(key)));
|
|
2907
|
+
} catch {
|
|
2908
|
+
return [];
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
useEffect10(() => {
|
|
2912
|
+
if (!storage) {
|
|
2913
|
+
setAnnotations([]);
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
try {
|
|
2917
|
+
setAnnotations(sort(parse(storage.getItem(key))));
|
|
2918
|
+
} catch {
|
|
2919
|
+
setAnnotations([]);
|
|
2920
|
+
}
|
|
2921
|
+
}, [storage, key]);
|
|
2922
|
+
const timerRef = useRef14(null);
|
|
2923
|
+
useEffect10(
|
|
2924
|
+
() => () => {
|
|
2925
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
2926
|
+
},
|
|
2927
|
+
[]
|
|
2928
|
+
);
|
|
2929
|
+
const onChangeRef = useRef14(options.onChange);
|
|
2930
|
+
onChangeRef.current = options.onChange;
|
|
2931
|
+
const commit = useCallback10(
|
|
2932
|
+
(next) => {
|
|
2933
|
+
if (storage) {
|
|
2934
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
2935
|
+
timerRef.current = setTimeout(() => {
|
|
2936
|
+
try {
|
|
2937
|
+
storage.setItem(key, serialize(next));
|
|
2938
|
+
} catch {
|
|
2939
|
+
}
|
|
2940
|
+
}, throttleMs);
|
|
2941
|
+
}
|
|
2942
|
+
onChangeRef.current?.(next);
|
|
2943
|
+
},
|
|
2944
|
+
[storage, key, throttleMs]
|
|
2945
|
+
);
|
|
2946
|
+
const add = useCallback10(
|
|
2947
|
+
(input) => {
|
|
2948
|
+
const annotation = {
|
|
2949
|
+
...input,
|
|
2950
|
+
id: input.id ?? makeId(),
|
|
2951
|
+
createdAt: input.createdAt ?? Date.now()
|
|
2952
|
+
};
|
|
2953
|
+
setAnnotations((current) => {
|
|
2954
|
+
const next = sort([...current, annotation]);
|
|
2955
|
+
commit(next);
|
|
2956
|
+
return next;
|
|
2957
|
+
});
|
|
2958
|
+
return annotation;
|
|
2959
|
+
},
|
|
2960
|
+
[commit]
|
|
2961
|
+
);
|
|
2962
|
+
const remove = useCallback10(
|
|
2963
|
+
(id) => {
|
|
2964
|
+
setAnnotations((current) => {
|
|
2965
|
+
const next = current.filter((annotation) => annotation.id !== id);
|
|
2966
|
+
if (next.length !== current.length) commit(next);
|
|
2967
|
+
return next;
|
|
2968
|
+
});
|
|
2969
|
+
},
|
|
2970
|
+
[commit]
|
|
2971
|
+
);
|
|
2972
|
+
const update = useCallback10(
|
|
2973
|
+
(id, patch) => {
|
|
2974
|
+
setAnnotations((current) => {
|
|
2975
|
+
let changed = false;
|
|
2976
|
+
const next = current.map((annotation) => {
|
|
2977
|
+
if (annotation.id !== id) return annotation;
|
|
2978
|
+
changed = true;
|
|
2979
|
+
return { ...annotation, ...patch, id };
|
|
2980
|
+
});
|
|
2981
|
+
if (!changed) return current;
|
|
2982
|
+
const sorted = sort(next);
|
|
2983
|
+
commit(sorted);
|
|
2984
|
+
return sorted;
|
|
2985
|
+
});
|
|
2986
|
+
},
|
|
2987
|
+
[commit]
|
|
2988
|
+
);
|
|
2989
|
+
const clear = useCallback10(() => {
|
|
2990
|
+
setAnnotations([]);
|
|
2991
|
+
if (storage) {
|
|
2992
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
2993
|
+
try {
|
|
2994
|
+
storage.removeItem(key);
|
|
2995
|
+
} catch {
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
onChangeRef.current?.([]);
|
|
2999
|
+
}, [storage, key]);
|
|
3000
|
+
return { annotations, add, remove, update, clear };
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// src/useEditableEpub.ts
|
|
3004
|
+
import {
|
|
3005
|
+
EditableEpub as EditableEpub2
|
|
3006
|
+
} from "@libraz/mejiro/epub";
|
|
3007
|
+
import { useCallback as useCallback11, useEffect as useEffect11, useRef as useRef15, useState as useState13 } from "react";
|
|
3008
|
+
function useEditableEpub(options = {}) {
|
|
3009
|
+
const [editor, setEditor] = useState13(null);
|
|
3010
|
+
const [loading, setLoading] = useState13(false);
|
|
3011
|
+
const [exporting, setExporting] = useState13(false);
|
|
3012
|
+
const [error, setError] = useState13(null);
|
|
3013
|
+
const [revision, setRevision] = useState13(0);
|
|
3014
|
+
const [selection, setSelectionState] = useState13({
|
|
3015
|
+
chapter: 0,
|
|
3016
|
+
paragraph: 0
|
|
3017
|
+
});
|
|
3018
|
+
const requestIdRef = useRef15(0);
|
|
3019
|
+
const onLoadRef = useRef15(options.onLoad);
|
|
3020
|
+
const onErrorRef = useRef15(options.onError);
|
|
3021
|
+
const onExportRef = useRef15(options.onExport);
|
|
3022
|
+
onLoadRef.current = options.onLoad;
|
|
3023
|
+
onErrorRef.current = options.onError;
|
|
3024
|
+
onExportRef.current = options.onExport;
|
|
3025
|
+
const loadBufferWithRequest = useCallback11(
|
|
3026
|
+
async (buffer, requestId) => {
|
|
3027
|
+
setLoading(true);
|
|
3028
|
+
setError(null);
|
|
3029
|
+
try {
|
|
3030
|
+
const next = await EditableEpub2.load(buffer);
|
|
3031
|
+
if (requestId !== requestIdRef.current) return null;
|
|
3032
|
+
setEditor(next);
|
|
3033
|
+
setSelectionState({ chapter: 0, paragraph: 0 });
|
|
3034
|
+
setRevision((value) => value + 1);
|
|
3035
|
+
onLoadRef.current?.(next);
|
|
3036
|
+
return next;
|
|
3037
|
+
} catch (err) {
|
|
3038
|
+
if (requestId === requestIdRef.current) {
|
|
3039
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
3040
|
+
setError(nextError);
|
|
3041
|
+
onErrorRef.current?.(nextError);
|
|
3042
|
+
}
|
|
3043
|
+
return null;
|
|
3044
|
+
} finally {
|
|
3045
|
+
if (requestId === requestIdRef.current) setLoading(false);
|
|
3046
|
+
}
|
|
3047
|
+
},
|
|
3048
|
+
[]
|
|
3049
|
+
);
|
|
3050
|
+
const loadBuffer = useCallback11(
|
|
3051
|
+
async (buffer) => {
|
|
3052
|
+
const requestId = ++requestIdRef.current;
|
|
3053
|
+
return loadBufferWithRequest(buffer, requestId);
|
|
3054
|
+
},
|
|
3055
|
+
[loadBufferWithRequest]
|
|
3056
|
+
);
|
|
3057
|
+
const loadFile = useCallback11(
|
|
3058
|
+
async (file) => {
|
|
3059
|
+
const requestId = ++requestIdRef.current;
|
|
3060
|
+
setLoading(true);
|
|
3061
|
+
setError(null);
|
|
3062
|
+
try {
|
|
3063
|
+
return await loadBufferWithRequest(await file.arrayBuffer(), requestId);
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
if (requestId === requestIdRef.current) {
|
|
3066
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
3067
|
+
setError(nextError);
|
|
3068
|
+
onErrorRef.current?.(nextError);
|
|
3069
|
+
setLoading(false);
|
|
3070
|
+
}
|
|
3071
|
+
return null;
|
|
3072
|
+
}
|
|
3073
|
+
},
|
|
3074
|
+
[loadBufferWithRequest]
|
|
3075
|
+
);
|
|
3076
|
+
const loadUrl = useCallback11(
|
|
3077
|
+
async (url) => {
|
|
3078
|
+
const requestId = ++requestIdRef.current;
|
|
3079
|
+
setLoading(true);
|
|
3080
|
+
setError(null);
|
|
3081
|
+
try {
|
|
3082
|
+
const res = await fetch(url);
|
|
3083
|
+
if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
|
|
3084
|
+
return await loadBufferWithRequest(await res.arrayBuffer(), requestId);
|
|
3085
|
+
} catch (err) {
|
|
3086
|
+
if (requestId === requestIdRef.current) {
|
|
3087
|
+
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
3088
|
+
setError(nextError);
|
|
3089
|
+
onErrorRef.current?.(nextError);
|
|
3090
|
+
setLoading(false);
|
|
3091
|
+
}
|
|
3092
|
+
return null;
|
|
3093
|
+
}
|
|
3094
|
+
},
|
|
3095
|
+
[loadBufferWithRequest]
|
|
3096
|
+
);
|
|
3097
|
+
useEffect11(() => {
|
|
3098
|
+
if (options.defaultUrl) void loadUrl(options.defaultUrl);
|
|
3099
|
+
}, [options.defaultUrl, loadUrl]);
|
|
3100
|
+
const book = editor?.book ?? null;
|
|
3101
|
+
const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
|
|
3102
|
+
const previewBook = book ? cloneBook2(book) : null;
|
|
3103
|
+
const history = editor?.history ?? null;
|
|
3104
|
+
const setSelection = useCallback11(
|
|
3105
|
+
(nextSelection) => {
|
|
3106
|
+
setSelectionState(clampSelection(book, nextSelection));
|
|
3107
|
+
},
|
|
3108
|
+
[book]
|
|
3109
|
+
);
|
|
3110
|
+
const updateParagraph = useCallback11(
|
|
3111
|
+
(text, inlineAnnotations) => {
|
|
3112
|
+
if (!editor) return;
|
|
3113
|
+
editor.updateParagraph(selection.chapter, selection.paragraph, {
|
|
3114
|
+
text,
|
|
3115
|
+
inlineAnnotations
|
|
3116
|
+
});
|
|
3117
|
+
setRevision((value) => value + 1);
|
|
3118
|
+
},
|
|
3119
|
+
[editor, selection.chapter, selection.paragraph]
|
|
3120
|
+
);
|
|
3121
|
+
const setInlineAnnotations = useCallback11(
|
|
3122
|
+
(inlineAnnotations) => {
|
|
3123
|
+
if (!editor) return;
|
|
3124
|
+
editor.setInlineAnnotations(selection.chapter, selection.paragraph, inlineAnnotations);
|
|
3125
|
+
setRevision((value) => value + 1);
|
|
3126
|
+
},
|
|
3127
|
+
[editor, selection.chapter, selection.paragraph]
|
|
3128
|
+
);
|
|
3129
|
+
const addImage = useCallback11(
|
|
3130
|
+
(image) => {
|
|
3131
|
+
if (!editor) return;
|
|
3132
|
+
editor.addImage(selection.chapter, image);
|
|
3133
|
+
setRevision((value) => value + 1);
|
|
3134
|
+
},
|
|
3135
|
+
[editor, selection.chapter]
|
|
3136
|
+
);
|
|
3137
|
+
const undo = useCallback11(() => {
|
|
3138
|
+
if (!editor) return false;
|
|
3139
|
+
const changed = editor.undo();
|
|
3140
|
+
if (changed) setRevision((value) => value + 1);
|
|
3141
|
+
return changed;
|
|
3142
|
+
}, [editor]);
|
|
3143
|
+
const redo = useCallback11(() => {
|
|
3144
|
+
if (!editor) return false;
|
|
3145
|
+
const changed = editor.redo();
|
|
3146
|
+
if (changed) setRevision((value) => value + 1);
|
|
3147
|
+
return changed;
|
|
3148
|
+
}, [editor]);
|
|
3149
|
+
const exportEpub = useCallback11(
|
|
3150
|
+
async (options2) => {
|
|
3151
|
+
if (!editor) return null;
|
|
3152
|
+
setExporting(true);
|
|
3153
|
+
try {
|
|
3154
|
+
const buffer = await editor.export(options2);
|
|
3155
|
+
onExportRef.current?.(buffer);
|
|
3156
|
+
return buffer;
|
|
3157
|
+
} finally {
|
|
3158
|
+
setExporting(false);
|
|
3159
|
+
}
|
|
3160
|
+
},
|
|
3161
|
+
[editor]
|
|
3162
|
+
);
|
|
3163
|
+
return {
|
|
3164
|
+
editor,
|
|
3165
|
+
book,
|
|
3166
|
+
previewBook,
|
|
3167
|
+
loading,
|
|
3168
|
+
exporting,
|
|
3169
|
+
error,
|
|
3170
|
+
revision,
|
|
3171
|
+
history,
|
|
3172
|
+
selection,
|
|
3173
|
+
selectedParagraph,
|
|
3174
|
+
setSelection,
|
|
3175
|
+
loadBuffer,
|
|
3176
|
+
loadFile,
|
|
3177
|
+
loadUrl,
|
|
3178
|
+
updateParagraph,
|
|
3179
|
+
setInlineAnnotations,
|
|
3180
|
+
addImage,
|
|
3181
|
+
undo,
|
|
3182
|
+
redo,
|
|
3183
|
+
exportEpub
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
function cloneBook2(book) {
|
|
3187
|
+
return {
|
|
3188
|
+
...book,
|
|
3189
|
+
chapters: book.chapters.map((chapter) => ({
|
|
3190
|
+
...chapter,
|
|
3191
|
+
blocks: chapter.blocks.map(
|
|
3192
|
+
(block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
|
|
3193
|
+
),
|
|
3194
|
+
imageAssets: new Map(chapter.imageAssets),
|
|
3195
|
+
originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
|
|
3196
|
+
paragraphs: chapter.paragraphs.map((paragraph) => ({
|
|
3197
|
+
...paragraph,
|
|
3198
|
+
inlineAnnotations: [...paragraph.inlineAnnotations]
|
|
3199
|
+
})),
|
|
3200
|
+
paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
|
|
3201
|
+
})),
|
|
3202
|
+
packageData: {
|
|
3203
|
+
...book.packageData,
|
|
3204
|
+
files: new Map(book.packageData.files)
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
function clampSelection(book, selection) {
|
|
3209
|
+
if (!book?.chapters.length) return { chapter: 0, paragraph: 0 };
|
|
3210
|
+
const chapter = clampInteger(selection.chapter, 0, book.chapters.length - 1);
|
|
3211
|
+
const paragraphCount = book.chapters[chapter]?.paragraphs.length ?? 0;
|
|
3212
|
+
const paragraph = paragraphCount ? clampInteger(selection.paragraph, 0, paragraphCount - 1) : 0;
|
|
3213
|
+
return { chapter, paragraph };
|
|
3214
|
+
}
|
|
3215
|
+
function clampInteger(value, min, max) {
|
|
3216
|
+
if (!Number.isFinite(value)) return min;
|
|
3217
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// src/useEpubProject.ts
|
|
3221
|
+
import {
|
|
3222
|
+
EpubProject as EpubProject2,
|
|
3223
|
+
parseEpub as parseEpub2
|
|
3224
|
+
} from "@libraz/mejiro/epub";
|
|
3225
|
+
import { useCallback as useCallback12, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef16, useState as useState14 } from "react";
|
|
3226
|
+
function useEpubProject(options = {}) {
|
|
3227
|
+
const [metadata, setMetadataState] = useState14({
|
|
3228
|
+
title: "\u65B0\u3057\u3044\u4F5C\u54C1",
|
|
3229
|
+
language: "ja",
|
|
3230
|
+
...options.metadata
|
|
3231
|
+
});
|
|
3232
|
+
const [chapters, setChaptersState] = useState14(
|
|
3233
|
+
options.chapters?.length ? options.chapters : [defaultChapter3(0)]
|
|
3234
|
+
);
|
|
3235
|
+
const [selectedChapter, setSelectedChapterState] = useState14(0);
|
|
3236
|
+
const [previewBook, setPreviewBook] = useState14(null);
|
|
3237
|
+
const [previewError, setPreviewError] = useState14(null);
|
|
3238
|
+
const [previewing, setPreviewing] = useState14(false);
|
|
3239
|
+
const previewRequestIdRef = useRef16(0);
|
|
3240
|
+
const onPreviewRef = useRef16(options.onPreview);
|
|
3241
|
+
const onExportRef = useRef16(options.onExport);
|
|
3242
|
+
onPreviewRef.current = options.onPreview;
|
|
3243
|
+
onExportRef.current = options.onExport;
|
|
3244
|
+
const buildProject = useCallback12(
|
|
3245
|
+
() => EpubProject2.fromManuscript({
|
|
3246
|
+
metadata,
|
|
3247
|
+
includeTitlePage: false,
|
|
3248
|
+
includeTitleInFirstChapter: true,
|
|
3249
|
+
chapters: chapters.map((chapter) => ({
|
|
3250
|
+
id: chapter.id,
|
|
3251
|
+
title: chapter.title || "Untitled",
|
|
3252
|
+
body: chapter.body
|
|
3253
|
+
}))
|
|
3254
|
+
}),
|
|
3255
|
+
[chapters, metadata]
|
|
3256
|
+
);
|
|
3257
|
+
const assetResolverRef = useRef16(options.assetResolver);
|
|
3258
|
+
assetResolverRef.current = options.assetResolver;
|
|
3259
|
+
useEffect12(() => {
|
|
3260
|
+
const requestId = ++previewRequestIdRef.current;
|
|
3261
|
+
let cancelled = false;
|
|
3262
|
+
setPreviewing(true);
|
|
3263
|
+
const timer = setTimeout(() => {
|
|
3264
|
+
void (async () => {
|
|
3265
|
+
try {
|
|
3266
|
+
const resolver = assetResolverRef.current;
|
|
3267
|
+
const book = await parseEpub2(
|
|
3268
|
+
await buildProject().export(resolver ? { assetResolver: resolver } : void 0)
|
|
3269
|
+
);
|
|
3270
|
+
if (cancelled || requestId !== previewRequestIdRef.current) return;
|
|
3271
|
+
setPreviewBook(book);
|
|
3272
|
+
setPreviewError(null);
|
|
3273
|
+
onPreviewRef.current?.(book);
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
if (!cancelled && requestId === previewRequestIdRef.current) {
|
|
3276
|
+
setPreviewError(err instanceof Error ? err : new Error(String(err)));
|
|
3277
|
+
}
|
|
3278
|
+
} finally {
|
|
3279
|
+
if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
|
|
3280
|
+
}
|
|
3281
|
+
})();
|
|
3282
|
+
}, options.debounceMs ?? 250);
|
|
3283
|
+
return () => {
|
|
3284
|
+
cancelled = true;
|
|
3285
|
+
previewRequestIdRef.current++;
|
|
3286
|
+
clearTimeout(timer);
|
|
3287
|
+
};
|
|
3288
|
+
}, [buildProject, options.debounceMs]);
|
|
3289
|
+
const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
|
|
3290
|
+
const setMetadata = useCallback12((patch) => {
|
|
3291
|
+
setMetadataState((current) => ({ ...current, ...patch }));
|
|
3292
|
+
}, []);
|
|
3293
|
+
const setChapters = useCallback12(
|
|
3294
|
+
(next) => {
|
|
3295
|
+
const normalized = next.length ? next : [defaultChapter3(0)];
|
|
3296
|
+
setChaptersState(normalized);
|
|
3297
|
+
setSelectedChapterState((current) => {
|
|
3298
|
+
const selectedId = chapters[current]?.id;
|
|
3299
|
+
const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
|
|
3300
|
+
return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
|
|
3301
|
+
});
|
|
3302
|
+
},
|
|
3303
|
+
[chapters]
|
|
3304
|
+
);
|
|
3305
|
+
const setSelectedChapter = useCallback12(
|
|
3306
|
+
(index) => {
|
|
3307
|
+
setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
|
|
3308
|
+
},
|
|
3309
|
+
[chapters.length]
|
|
3310
|
+
);
|
|
3311
|
+
const patchChapter = useCallback12((index, patch) => {
|
|
3312
|
+
setChaptersState(
|
|
3313
|
+
(current) => current.map(
|
|
3314
|
+
(chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
|
|
3315
|
+
)
|
|
3316
|
+
);
|
|
3317
|
+
}, []);
|
|
3318
|
+
const addChapter = useCallback12((chapter = {}) => {
|
|
3319
|
+
setChaptersState((current) => {
|
|
3320
|
+
const generated = defaultChapter3(current.length);
|
|
3321
|
+
const next = [
|
|
3322
|
+
...current,
|
|
3323
|
+
{
|
|
3324
|
+
id: chapter.id ?? generated.id,
|
|
3325
|
+
title: chapter.title ?? generated.title,
|
|
3326
|
+
body: chapter.body ?? generated.body
|
|
3327
|
+
}
|
|
3328
|
+
];
|
|
3329
|
+
setSelectedChapterState(next.length - 1);
|
|
3330
|
+
return next;
|
|
3331
|
+
});
|
|
3332
|
+
}, []);
|
|
3333
|
+
const removeChapter = useCallback12(
|
|
3334
|
+
(index = selectedChapter) => {
|
|
3335
|
+
setChaptersState((current) => {
|
|
3336
|
+
if (current.length <= 1) return current;
|
|
3337
|
+
const next = current.filter((_, chapterIndex) => chapterIndex !== index);
|
|
3338
|
+
setSelectedChapterState((currentSelected) => {
|
|
3339
|
+
if (currentSelected === index) return Math.max(0, Math.min(index, next.length - 1));
|
|
3340
|
+
if (index < currentSelected) return currentSelected - 1;
|
|
3341
|
+
return Math.max(0, Math.min(currentSelected, next.length - 1));
|
|
3342
|
+
});
|
|
3343
|
+
return next;
|
|
3344
|
+
});
|
|
3345
|
+
},
|
|
3346
|
+
[selectedChapter]
|
|
3347
|
+
);
|
|
3348
|
+
const reorderChapters = useCallback12((from, to) => {
|
|
3349
|
+
setChaptersState((current) => {
|
|
3350
|
+
if (from < 0 || from >= current.length) return current;
|
|
3351
|
+
const next = [...current];
|
|
3352
|
+
const [moved] = next.splice(from, 1);
|
|
3353
|
+
const target = Math.max(0, Math.min(next.length, to));
|
|
3354
|
+
next.splice(target, 0, moved);
|
|
3355
|
+
setSelectedChapterState((currentSelected) => {
|
|
3356
|
+
if (currentSelected === from) return target;
|
|
3357
|
+
if (from < currentSelected && target >= currentSelected) return currentSelected - 1;
|
|
3358
|
+
if (from > currentSelected && target <= currentSelected) return currentSelected + 1;
|
|
3359
|
+
return currentSelected;
|
|
3360
|
+
});
|
|
3361
|
+
return next;
|
|
3362
|
+
});
|
|
3363
|
+
}, []);
|
|
3364
|
+
const exportEpub = useCallback12(async () => {
|
|
3365
|
+
const resolver = assetResolverRef.current;
|
|
3366
|
+
const buffer = await buildProject().export(resolver ? { assetResolver: resolver } : void 0);
|
|
3367
|
+
onExportRef.current?.(buffer);
|
|
3368
|
+
return buffer;
|
|
3369
|
+
}, [buildProject]);
|
|
3370
|
+
return useMemo7(
|
|
3371
|
+
() => ({
|
|
3372
|
+
metadata,
|
|
3373
|
+
chapters,
|
|
3374
|
+
selectedChapter,
|
|
3375
|
+
currentChapter,
|
|
3376
|
+
previewBook,
|
|
3377
|
+
previewError,
|
|
3378
|
+
previewing,
|
|
3379
|
+
setMetadata,
|
|
3380
|
+
setChapters,
|
|
3381
|
+
setSelectedChapter,
|
|
3382
|
+
patchChapter,
|
|
3383
|
+
addChapter,
|
|
3384
|
+
removeChapter,
|
|
3385
|
+
reorderChapters,
|
|
3386
|
+
buildProject,
|
|
3387
|
+
exportEpub
|
|
3388
|
+
}),
|
|
3389
|
+
[
|
|
3390
|
+
addChapter,
|
|
3391
|
+
buildProject,
|
|
3392
|
+
chapters,
|
|
3393
|
+
currentChapter,
|
|
3394
|
+
exportEpub,
|
|
3395
|
+
metadata,
|
|
3396
|
+
patchChapter,
|
|
3397
|
+
previewBook,
|
|
3398
|
+
previewError,
|
|
3399
|
+
previewing,
|
|
3400
|
+
reorderChapters,
|
|
3401
|
+
removeChapter,
|
|
3402
|
+
selectedChapter,
|
|
3403
|
+
setChapters,
|
|
3404
|
+
setMetadata,
|
|
3405
|
+
setSelectedChapter
|
|
3406
|
+
]
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
function defaultChapter3(index) {
|
|
3410
|
+
return {
|
|
3411
|
+
id: `chapter-${Date.now()}-${index}`,
|
|
3412
|
+
title: index === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${index + 1}\u8A71`,
|
|
3413
|
+
body: index === 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" : ""
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// src/useImageOverlay.ts
|
|
3418
|
+
import { moveImageOverlayRect as moveImageOverlayRect2, resizeImageOverlayRect as resizeImageOverlayRect2 } from "@libraz/mejiro";
|
|
3419
|
+
import {
|
|
3420
|
+
useCallback as useCallback13,
|
|
3421
|
+
useEffect as useEffect13,
|
|
3422
|
+
useRef as useRef17,
|
|
3423
|
+
useState as useState15
|
|
3424
|
+
} from "react";
|
|
85
3425
|
function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
86
3426
|
const defW = options?.defaultWidth ?? 120;
|
|
87
3427
|
const defH = options?.defaultHeight ?? 160;
|
|
88
3428
|
const defX = options?.defaultX ?? 80;
|
|
89
3429
|
const defY = options?.defaultY ?? 100;
|
|
90
3430
|
const margin = options?.margin;
|
|
91
|
-
const [imageRect, setImageRect] =
|
|
92
|
-
const rectRef =
|
|
3431
|
+
const [imageRect, setImageRect] = useState15(null);
|
|
3432
|
+
const rectRef = useRef17(imageRect);
|
|
93
3433
|
rectRef.current = imageRect;
|
|
94
|
-
const layoutRef =
|
|
3434
|
+
const layoutRef = useRef17(layout);
|
|
95
3435
|
layoutRef.current = layout;
|
|
96
|
-
const spreadRef =
|
|
3436
|
+
const spreadRef = useRef17(spreadIdx);
|
|
97
3437
|
spreadRef.current = spreadIdx;
|
|
98
|
-
const onUpdateRef =
|
|
3438
|
+
const onUpdateRef = useRef17(onUpdate);
|
|
99
3439
|
onUpdateRef.current = onUpdate;
|
|
100
|
-
const
|
|
3440
|
+
const activeDragCleanupsRef = useRef17(/* @__PURE__ */ new Set());
|
|
3441
|
+
useEffect13(
|
|
3442
|
+
() => () => {
|
|
3443
|
+
for (const cleanup of activeDragCleanupsRef.current) cleanup();
|
|
3444
|
+
activeDragCleanupsRef.current.clear();
|
|
3445
|
+
},
|
|
3446
|
+
[]
|
|
3447
|
+
);
|
|
3448
|
+
const syncToLayout = useCallback13(
|
|
101
3449
|
(rect) => {
|
|
102
3450
|
const lo = layoutRef.current;
|
|
103
3451
|
if (!lo) return;
|
|
@@ -107,7 +3455,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
107
3455
|
},
|
|
108
3456
|
[margin]
|
|
109
3457
|
);
|
|
110
|
-
const toggleImage =
|
|
3458
|
+
const toggleImage = useCallback13(() => {
|
|
111
3459
|
if (rectRef.current) {
|
|
112
3460
|
setImageRect(null);
|
|
113
3461
|
syncToLayout(null);
|
|
@@ -117,7 +3465,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
117
3465
|
syncToLayout(r);
|
|
118
3466
|
}
|
|
119
3467
|
}, [defX, defY, defW, defH, syncToLayout]);
|
|
120
|
-
const onOverlayPointerDown =
|
|
3468
|
+
const onOverlayPointerDown = useCallback13(
|
|
121
3469
|
(e) => {
|
|
122
3470
|
e.preventDefault();
|
|
123
3471
|
const current = rectRef.current;
|
|
@@ -132,24 +3480,29 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
132
3480
|
const onMove = (me) => {
|
|
133
3481
|
const dx = me.clientX - startX;
|
|
134
3482
|
const dy = me.clientY - startY;
|
|
135
|
-
const r =
|
|
3483
|
+
const r = moveImageOverlayRect2(start, dx, dy);
|
|
136
3484
|
cancelAnimationFrame(rafId);
|
|
137
3485
|
rafId = requestAnimationFrame(() => {
|
|
138
3486
|
setImageRect(r);
|
|
139
3487
|
syncToLayout(r);
|
|
140
3488
|
});
|
|
141
3489
|
};
|
|
142
|
-
|
|
3490
|
+
let cleanup;
|
|
3491
|
+
const onUp = () => cleanup();
|
|
3492
|
+
cleanup = () => {
|
|
3493
|
+
cancelAnimationFrame(rafId);
|
|
143
3494
|
target.classList.remove("dragging");
|
|
144
3495
|
document.removeEventListener("pointermove", onMove);
|
|
145
3496
|
document.removeEventListener("pointerup", onUp);
|
|
3497
|
+
activeDragCleanupsRef.current.delete(cleanup);
|
|
146
3498
|
};
|
|
147
3499
|
document.addEventListener("pointermove", onMove);
|
|
148
3500
|
document.addEventListener("pointerup", onUp);
|
|
3501
|
+
activeDragCleanupsRef.current.add(cleanup);
|
|
149
3502
|
},
|
|
150
3503
|
[syncToLayout]
|
|
151
3504
|
);
|
|
152
|
-
const onResizePointerDown =
|
|
3505
|
+
const onResizePointerDown = useCallback13(
|
|
153
3506
|
(e) => {
|
|
154
3507
|
e.preventDefault();
|
|
155
3508
|
e.stopPropagation();
|
|
@@ -165,20 +3518,25 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
165
3518
|
const onMove = (me) => {
|
|
166
3519
|
const dx = me.clientX - startX;
|
|
167
3520
|
const dy = me.clientY - startY;
|
|
168
|
-
const r =
|
|
3521
|
+
const r = resizeImageOverlayRect2(start, dx, dy);
|
|
169
3522
|
cancelAnimationFrame(rafId);
|
|
170
3523
|
rafId = requestAnimationFrame(() => {
|
|
171
3524
|
setImageRect(r);
|
|
172
3525
|
syncToLayout(r);
|
|
173
3526
|
});
|
|
174
3527
|
};
|
|
175
|
-
|
|
3528
|
+
let cleanup;
|
|
3529
|
+
const onUp = () => cleanup();
|
|
3530
|
+
cleanup = () => {
|
|
3531
|
+
cancelAnimationFrame(rafId);
|
|
176
3532
|
target.parentElement?.classList.remove("dragging");
|
|
177
3533
|
document.removeEventListener("pointermove", onMove);
|
|
178
3534
|
document.removeEventListener("pointerup", onUp);
|
|
3535
|
+
activeDragCleanupsRef.current.delete(cleanup);
|
|
179
3536
|
};
|
|
180
3537
|
document.addEventListener("pointermove", onMove);
|
|
181
3538
|
document.addEventListener("pointerup", onUp);
|
|
3539
|
+
activeDragCleanupsRef.current.add(cleanup);
|
|
182
3540
|
},
|
|
183
3541
|
[syncToLayout]
|
|
184
3542
|
);
|
|
@@ -190,8 +3548,298 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
|
|
|
190
3548
|
onResizePointerDown
|
|
191
3549
|
};
|
|
192
3550
|
}
|
|
3551
|
+
|
|
3552
|
+
// src/useLibrary.ts
|
|
3553
|
+
import { useCallback as useCallback14, useEffect as useEffect14, useMemo as useMemo8, useState as useState16 } from "react";
|
|
3554
|
+
function useLibrary(options) {
|
|
3555
|
+
const { volumes, initialVolumeId, onChange } = options;
|
|
3556
|
+
const initialIndex = initialVolumeId != null ? Math.max(
|
|
3557
|
+
0,
|
|
3558
|
+
volumes.findIndex((v) => v.id === initialVolumeId)
|
|
3559
|
+
) : 0;
|
|
3560
|
+
const [currentId, setCurrentId] = useState16(
|
|
3561
|
+
volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
|
|
3562
|
+
);
|
|
3563
|
+
useEffect14(() => {
|
|
3564
|
+
if (volumes.length === 0) {
|
|
3565
|
+
if (currentId !== null) setCurrentId(null);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
if (currentId != null && volumes.some((v) => v.id === currentId)) return;
|
|
3569
|
+
const fallback = volumes[0] ?? null;
|
|
3570
|
+
setCurrentId(fallback?.id ?? null);
|
|
3571
|
+
if (fallback) onChange?.(fallback);
|
|
3572
|
+
}, [currentId, volumes, onChange]);
|
|
3573
|
+
const index = useMemo8(() => {
|
|
3574
|
+
if (volumes.length === 0) return -1;
|
|
3575
|
+
if (currentId == null) return 0;
|
|
3576
|
+
const found = volumes.findIndex((v) => v.id === currentId);
|
|
3577
|
+
return found >= 0 ? found : 0;
|
|
3578
|
+
}, [currentId, volumes]);
|
|
3579
|
+
const current = useMemo8(
|
|
3580
|
+
() => index >= 0 && index < volumes.length ? volumes[index] : null,
|
|
3581
|
+
[index, volumes]
|
|
3582
|
+
);
|
|
3583
|
+
const fire = useCallback14(
|
|
3584
|
+
(i) => {
|
|
3585
|
+
const v = volumes[i];
|
|
3586
|
+
if (v) onChange?.(v);
|
|
3587
|
+
},
|
|
3588
|
+
[volumes, onChange]
|
|
3589
|
+
);
|
|
3590
|
+
const next = useCallback14(() => {
|
|
3591
|
+
if (index < 0) return;
|
|
3592
|
+
const ni = Math.min(volumes.length - 1, index + 1);
|
|
3593
|
+
if (ni !== index) {
|
|
3594
|
+
setCurrentId(volumes[ni]?.id ?? null);
|
|
3595
|
+
fire(ni);
|
|
3596
|
+
}
|
|
3597
|
+
}, [index, volumes, fire]);
|
|
3598
|
+
const prev = useCallback14(() => {
|
|
3599
|
+
if (index < 0) return;
|
|
3600
|
+
const ni = Math.max(0, index - 1);
|
|
3601
|
+
if (ni !== index) {
|
|
3602
|
+
setCurrentId(volumes[ni]?.id ?? null);
|
|
3603
|
+
fire(ni);
|
|
3604
|
+
}
|
|
3605
|
+
}, [index, volumes, fire]);
|
|
3606
|
+
const goTo = useCallback14(
|
|
3607
|
+
(id) => {
|
|
3608
|
+
const ni = volumes.findIndex((v) => v.id === id);
|
|
3609
|
+
if (ni < 0) return;
|
|
3610
|
+
if (ni !== index) {
|
|
3611
|
+
setCurrentId(id);
|
|
3612
|
+
fire(ni);
|
|
3613
|
+
}
|
|
3614
|
+
},
|
|
3615
|
+
[index, volumes, fire]
|
|
3616
|
+
);
|
|
3617
|
+
return { list: volumes, current, currentIndex: index, next, prev, goTo };
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// src/useManuscriptLayout.ts
|
|
3621
|
+
import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect4, useRef as useRef18, useState as useState17 } from "react";
|
|
3622
|
+
function useManuscriptLayout(book, chapter, surface, options = {}) {
|
|
3623
|
+
const dialect = options.dialect ?? "mejiro";
|
|
3624
|
+
const enableResize = options.enableResize ?? true;
|
|
3625
|
+
const resizeDebounce = options.resizeDebounce ?? 120;
|
|
3626
|
+
const [layout, setLayout] = useState17(null);
|
|
3627
|
+
const [pageWidth, setPageWidth] = useState17(0);
|
|
3628
|
+
const [pageHeight, setPageHeight] = useState17(0);
|
|
3629
|
+
const [contentHeight, setContentHeight] = useState17(0);
|
|
3630
|
+
const [elapsedMs, setElapsedMs] = useState17(0);
|
|
3631
|
+
const layoutRef = useRef18(null);
|
|
3632
|
+
const requestIdRef = useRef18(0);
|
|
3633
|
+
layoutRef.current = layout;
|
|
3634
|
+
const recompute = useCallback15(async () => {
|
|
3635
|
+
const requestId = ++requestIdRef.current;
|
|
3636
|
+
if (!(chapter && surface.current)) {
|
|
3637
|
+
setLayout(null);
|
|
3638
|
+
setPageWidth(0);
|
|
3639
|
+
setPageHeight(0);
|
|
3640
|
+
setContentHeight(0);
|
|
3641
|
+
setElapsedMs(0);
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
setLayout(null);
|
|
3645
|
+
const dims = book.computePageSize(surface.current);
|
|
3646
|
+
setPageWidth(dims.pageWidth);
|
|
3647
|
+
setPageHeight(dims.pageHeight);
|
|
3648
|
+
setContentHeight(dims.contentHeight);
|
|
3649
|
+
const t0 = performance.now();
|
|
3650
|
+
const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
|
|
3651
|
+
if (requestId !== requestIdRef.current) return;
|
|
3652
|
+
const next = layouts.values().next().value ?? null;
|
|
3653
|
+
setLayout(next);
|
|
3654
|
+
setElapsedMs(performance.now() - t0);
|
|
3655
|
+
}, [book, chapter, dialect, surface]);
|
|
3656
|
+
useLayoutEffect4(() => {
|
|
3657
|
+
requestIdRef.current++;
|
|
3658
|
+
setLayout(null);
|
|
3659
|
+
setPageWidth(0);
|
|
3660
|
+
setPageHeight(0);
|
|
3661
|
+
setContentHeight(0);
|
|
3662
|
+
setElapsedMs(0);
|
|
3663
|
+
}, [recompute]);
|
|
3664
|
+
useEffect15(() => {
|
|
3665
|
+
void recompute();
|
|
3666
|
+
return () => {
|
|
3667
|
+
requestIdRef.current++;
|
|
3668
|
+
};
|
|
3669
|
+
}, [recompute]);
|
|
3670
|
+
useEffect15(() => {
|
|
3671
|
+
if (!enableResize) return;
|
|
3672
|
+
let timer = null;
|
|
3673
|
+
const onResize = () => {
|
|
3674
|
+
if (!(surface.current && layoutRef.current)) return;
|
|
3675
|
+
if (timer) clearTimeout(timer);
|
|
3676
|
+
timer = setTimeout(() => {
|
|
3677
|
+
if (!(surface.current && layoutRef.current)) return;
|
|
3678
|
+
const dims = book.computePageSize(surface.current);
|
|
3679
|
+
setPageWidth(dims.pageWidth);
|
|
3680
|
+
setPageHeight(dims.pageHeight);
|
|
3681
|
+
setContentHeight(dims.contentHeight);
|
|
3682
|
+
layoutRef.current.resize({
|
|
3683
|
+
pageWidth: dims.pageWidth,
|
|
3684
|
+
lineWidth: dims.contentHeight - book.getOptions().fontSize * 0.5
|
|
3685
|
+
});
|
|
3686
|
+
}, resizeDebounce);
|
|
3687
|
+
};
|
|
3688
|
+
window.addEventListener("resize", onResize);
|
|
3689
|
+
return () => {
|
|
3690
|
+
window.removeEventListener("resize", onResize);
|
|
3691
|
+
if (timer) clearTimeout(timer);
|
|
3692
|
+
};
|
|
3693
|
+
}, [book, surface, enableResize, resizeDebounce]);
|
|
3694
|
+
return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// src/useReadingPosition.ts
|
|
3698
|
+
import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState18 } from "react";
|
|
3699
|
+
var STORAGE_VERSION2 = 2;
|
|
3700
|
+
function resolveDefaultStorage2() {
|
|
3701
|
+
if (typeof globalThis === "undefined") return null;
|
|
3702
|
+
const w = globalThis.localStorage;
|
|
3703
|
+
return w ?? null;
|
|
3704
|
+
}
|
|
3705
|
+
function parsePosition(raw) {
|
|
3706
|
+
if (!raw) return null;
|
|
3707
|
+
let parsed;
|
|
3708
|
+
try {
|
|
3709
|
+
parsed = JSON.parse(raw);
|
|
3710
|
+
} catch {
|
|
3711
|
+
return null;
|
|
3712
|
+
}
|
|
3713
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3714
|
+
const p = parsed;
|
|
3715
|
+
if (typeof p.chapter !== "number") return null;
|
|
3716
|
+
if (p.version === STORAGE_VERSION2) {
|
|
3717
|
+
if (typeof p.paragraph !== "number" || typeof p.charIndex !== "number") return null;
|
|
3718
|
+
return { chapter: p.chapter, paragraph: p.paragraph, charIndex: p.charIndex };
|
|
3719
|
+
}
|
|
3720
|
+
if (typeof p.spreadIdx === "number") {
|
|
3721
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
3722
|
+
console.warn(
|
|
3723
|
+
"[mejiro] useReadingPosition: migrating legacy {chapter, spreadIdx} format. spreadIdx has been dropped because it does not survive reflow; the position now resumes at the start of the chapter. Persist new positions via save() to upgrade."
|
|
3724
|
+
);
|
|
3725
|
+
}
|
|
3726
|
+
return { chapter: p.chapter, paragraph: 0, charIndex: 0 };
|
|
3727
|
+
}
|
|
3728
|
+
return null;
|
|
3729
|
+
}
|
|
3730
|
+
function serializePosition(value) {
|
|
3731
|
+
const payload = {
|
|
3732
|
+
version: STORAGE_VERSION2,
|
|
3733
|
+
chapter: value.chapter,
|
|
3734
|
+
paragraph: value.paragraph,
|
|
3735
|
+
charIndex: value.charIndex
|
|
3736
|
+
};
|
|
3737
|
+
return JSON.stringify(payload);
|
|
3738
|
+
}
|
|
3739
|
+
function useReadingPosition(options) {
|
|
3740
|
+
const { key, throttleMs = 250 } = options;
|
|
3741
|
+
const storage = options.storage ?? resolveDefaultStorage2();
|
|
3742
|
+
const [position, setPosition] = useState18(() => {
|
|
3743
|
+
if (!storage) return null;
|
|
3744
|
+
try {
|
|
3745
|
+
return parsePosition(storage.getItem(key));
|
|
3746
|
+
} catch {
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
useEffect16(() => {
|
|
3751
|
+
if (!storage) {
|
|
3752
|
+
setPosition(null);
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
try {
|
|
3756
|
+
setPosition(parsePosition(storage.getItem(key)));
|
|
3757
|
+
} catch {
|
|
3758
|
+
setPosition(null);
|
|
3759
|
+
}
|
|
3760
|
+
}, [storage, key]);
|
|
3761
|
+
const timerRef = useRef19(null);
|
|
3762
|
+
useEffect16(
|
|
3763
|
+
() => () => {
|
|
3764
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3765
|
+
},
|
|
3766
|
+
[]
|
|
3767
|
+
);
|
|
3768
|
+
const onChangeRef = useRef19(options.onChange);
|
|
3769
|
+
onChangeRef.current = options.onChange;
|
|
3770
|
+
const save = useCallback16(
|
|
3771
|
+
(next) => {
|
|
3772
|
+
setPosition(next);
|
|
3773
|
+
if (storage) {
|
|
3774
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3775
|
+
timerRef.current = setTimeout(() => {
|
|
3776
|
+
try {
|
|
3777
|
+
storage.setItem(key, serializePosition(next));
|
|
3778
|
+
} catch {
|
|
3779
|
+
}
|
|
3780
|
+
}, throttleMs);
|
|
3781
|
+
}
|
|
3782
|
+
onChangeRef.current?.(next);
|
|
3783
|
+
},
|
|
3784
|
+
[storage, key, throttleMs]
|
|
3785
|
+
);
|
|
3786
|
+
const clear = useCallback16(() => {
|
|
3787
|
+
setPosition(null);
|
|
3788
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3789
|
+
if (storage) {
|
|
3790
|
+
try {
|
|
3791
|
+
storage.removeItem(key);
|
|
3792
|
+
} catch {
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
onChangeRef.current?.(null);
|
|
3796
|
+
}, [storage, key]);
|
|
3797
|
+
return { position, save, clear };
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
// src/index.ts
|
|
3801
|
+
function loadMejiroStyles() {
|
|
3802
|
+
if (typeof document === "undefined") return;
|
|
3803
|
+
void import("@libraz/mejiro/render/mejiro.css");
|
|
3804
|
+
void import("@libraz/mejiro/render/mejiro-reader.css");
|
|
3805
|
+
void import("@libraz/mejiro/render/mejiro-editor.css");
|
|
3806
|
+
}
|
|
3807
|
+
loadMejiroStyles();
|
|
193
3808
|
export {
|
|
3809
|
+
MejiroChapterNav,
|
|
3810
|
+
MejiroDropZone,
|
|
3811
|
+
MejiroEditor,
|
|
3812
|
+
MejiroI18nProvider,
|
|
3813
|
+
MejiroImageOverlay,
|
|
3814
|
+
MejiroManuscriptEditor,
|
|
3815
|
+
MejiroNotationHighlighter,
|
|
194
3816
|
MejiroPage,
|
|
3817
|
+
MejiroPageIndicator,
|
|
195
3818
|
MejiroPageView,
|
|
196
|
-
|
|
3819
|
+
MejiroReader,
|
|
3820
|
+
MejiroScrollView,
|
|
3821
|
+
MejiroSelectionLayer,
|
|
3822
|
+
MejiroSettingsPanel,
|
|
3823
|
+
MejiroShelf,
|
|
3824
|
+
MejiroSpread,
|
|
3825
|
+
MejiroStats,
|
|
3826
|
+
MejiroToc,
|
|
3827
|
+
enMessages,
|
|
3828
|
+
format,
|
|
3829
|
+
jaMessages,
|
|
3830
|
+
resolveMessages,
|
|
3831
|
+
useAnnotations,
|
|
3832
|
+
useChapterLayout,
|
|
3833
|
+
useEditableEpub,
|
|
3834
|
+
useEpub,
|
|
3835
|
+
useEpubProject,
|
|
3836
|
+
useI18n,
|
|
3837
|
+
useImageOverlay,
|
|
3838
|
+
useLibrary,
|
|
3839
|
+
useManuscriptDraft,
|
|
3840
|
+
useManuscriptLayout,
|
|
3841
|
+
useMejiroBook,
|
|
3842
|
+
useMultiImageOverlay,
|
|
3843
|
+
useReadingPosition,
|
|
3844
|
+
useSpread
|
|
197
3845
|
};
|