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