@papyrus-sdk/ui-react-native 0.2.35-beta.1 → 0.2.35-beta.11
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 +8 -2
- package/dist/index.d.mts +31 -3
- package/dist/index.d.ts +31 -3
- package/dist/index.js +2881 -1734
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2639 -1484
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -7
- package/runtime/epubAnnotations.js +118 -0
- package/runtime/epubAnnotations.test.ts +31 -0
- package/runtime/epubSelectionMenu.test.ts +39 -0
- package/runtime/index.html +310 -15
- package/runtime/runtime.js +310 -15
- package/runtime/syncComicRuntime.mjs +2 -1
package/runtime/runtime.js
CHANGED
|
@@ -109,6 +109,127 @@
|
|
|
109
109
|
/* @papyrus-comic-runtime:end */
|
|
110
110
|
|
|
111
111
|
(function () {
|
|
112
|
+
/* @papyrus-epub-annotations:start */
|
|
113
|
+
(function (root, factory) {
|
|
114
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
115
|
+
else root.PapyrusEpubAnnotations = factory();
|
|
116
|
+
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
|
117
|
+
function validAnchor(anchor) {
|
|
118
|
+
return anchor && anchor.version === 1 && anchor.kind === 'epub-cfi' &&
|
|
119
|
+
typeof anchor.cfiRange === 'string' && anchor.cfiRange.length <= 4096 && /^epubcfi\([^\n<>]+\)$/.test(anchor.cfiRange) &&
|
|
120
|
+
typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
|
|
121
|
+
typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
|
|
122
|
+
}
|
|
123
|
+
function textNodes(document) {
|
|
124
|
+
var walker = document.createTreeWalker(document.body, 4);
|
|
125
|
+
var nodes = [], text = '', node;
|
|
126
|
+
while ((node = walker.nextNode())) {
|
|
127
|
+
if (node.parentElement.closest('script,style,[data-papyrus-overlay]')) continue;
|
|
128
|
+
nodes.push({node:node, start:text.length}); text += node.textContent;
|
|
129
|
+
}
|
|
130
|
+
return {nodes:nodes,text:text};
|
|
131
|
+
}
|
|
132
|
+
function recoverRange(document, anchor) {
|
|
133
|
+
var source = textNodes(document), match = null, start = source.text.indexOf(anchor.quote);
|
|
134
|
+
while (start >= 0) {
|
|
135
|
+
var end = start + anchor.quote.length;
|
|
136
|
+
if ((!anchor.prefix || source.text.slice(Math.max(0,start-anchor.prefix.length),start) === anchor.prefix) &&
|
|
137
|
+
(!anchor.suffix || source.text.slice(end,end+anchor.suffix.length) === anchor.suffix)) {
|
|
138
|
+
if (match) return null; match = {start:start,end:end};
|
|
139
|
+
}
|
|
140
|
+
start = source.text.indexOf(anchor.quote,start+1);
|
|
141
|
+
}
|
|
142
|
+
if (!match) return null;
|
|
143
|
+
var range = document.createRange();
|
|
144
|
+
var first = source.nodes.find(function(n){return n.start+n.node.length > match.start;});
|
|
145
|
+
var last = source.nodes.find(function(n){return n.start+n.node.length >= match.end;});
|
|
146
|
+
if (!first || !last) return null;
|
|
147
|
+
range.setStart(first.node,match.start-first.start); range.setEnd(last.node,match.end-last.start);
|
|
148
|
+
return range;
|
|
149
|
+
}
|
|
150
|
+
function resolveRange(contents, anchor) {
|
|
151
|
+
if (!validAnchor(anchor)) return null;
|
|
152
|
+
try { var range = contents.range(anchor.cfiRange); if (range && range.toString() === anchor.quote) return range; } catch (_) {}
|
|
153
|
+
return recoverRange(contents.document,anchor);
|
|
154
|
+
}
|
|
155
|
+
function selection(contents, cfiRange, href, documentId) {
|
|
156
|
+
var range;
|
|
157
|
+
try { range = contents.range(cfiRange); } catch (_) { return null; }
|
|
158
|
+
if (!range || !range.toString()) return null;
|
|
159
|
+
var before = contents.document.createRange(); before.selectNodeContents(contents.document.body); before.setEnd(range.startContainer,range.startOffset);
|
|
160
|
+
var after = contents.document.createRange(); after.selectNodeContents(contents.document.body); after.setStart(range.endContainer,range.endOffset);
|
|
161
|
+
var anchor = {version:1,kind:'epub-cfi',cfiRange:cfiRange,href:href,spineIndex:contents.sectionIndex,documentId:documentId,quote:range.toString(),prefix:before.toString().slice(-48),suffix:after.toString().slice(0,48)};
|
|
162
|
+
return validAnchor(anchor) ? anchor : null;
|
|
163
|
+
}
|
|
164
|
+
function install(contents, options) {
|
|
165
|
+
var doc = contents.document, win = contents.window, alive = true, frame = 0;
|
|
166
|
+
var overlay = doc.createElement('div'); overlay.dataset.papyrusOverlay = 'annotations';
|
|
167
|
+
overlay.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;z-index:3';
|
|
168
|
+
var originalPosition=doc.body.style.position;
|
|
169
|
+
if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
|
|
170
|
+
doc.body.appendChild(overlay);
|
|
171
|
+
var registry = new Map(), hitRegions = [];
|
|
172
|
+
function render() {
|
|
173
|
+
frame = 0; if (!alive || !options.isCurrent()) return;
|
|
174
|
+
var keep = new Set(), noteGroups = new Map();
|
|
175
|
+
hitRegions = [];
|
|
176
|
+
options.annotations().forEach(function(annotation) {
|
|
177
|
+
var anchor = annotation.anchor;
|
|
178
|
+
if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
|
|
179
|
+
var range = resolveRange(contents,anchor); if (!range) return;
|
|
180
|
+
var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
|
|
181
|
+
if (!rects.length) return;
|
|
182
|
+
keep.add(annotation.id);
|
|
183
|
+
var group = registry.get(annotation.id);
|
|
184
|
+
if (!group) { group = doc.createElement('div'); group.dataset.annotationId = annotation.id; overlay.appendChild(group); registry.set(annotation.id,group); if (!doc.defaultView.matchMedia || !doc.defaultView.matchMedia('(prefers-reduced-motion: reduce)').matches) { if (typeof group.animate === 'function') group.animate([{opacity:0},{opacity:1}],{duration:180,easing:'ease-out'}); } }
|
|
185
|
+
group.replaceChildren();
|
|
186
|
+
hitRegions.push({id:annotation.id,rects:rects});
|
|
187
|
+
var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
|
|
188
|
+
var style = annotation.markupStyle || annotation.type;
|
|
189
|
+
var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
|
|
190
|
+
rects.forEach(function(rect) {
|
|
191
|
+
var mark = doc.createElement('div');
|
|
192
|
+
var bodyRect=doc.body.getBoundingClientRect();
|
|
193
|
+
var height = style === 'highlight' ? rect.height : 2;
|
|
194
|
+
var y = style === 'strikeout' ? rect.top+rect.height/2 : style === 'underline' ? rect.bottom-2 : rect.top;
|
|
195
|
+
mark.style.cssText = 'position:absolute;pointer-events:none;left:'+(rect.left-bodyRect.left)+'px;top:'+(y-bodyRect.top)+'px;width:'+rect.width+'px;height:'+height+'px;background:'+color+';opacity:'+opacity;
|
|
196
|
+
if (['highlight','underline','strikeout'].includes(style)) group.appendChild(mark);
|
|
197
|
+
});
|
|
198
|
+
var hasNote = annotation.noteContent !== undefined || annotation.type === 'comment' || annotation.type === 'text';
|
|
199
|
+
if (hasNote) {
|
|
200
|
+
var last = rects[rects.length-1], button = doc.createElement('button');
|
|
201
|
+
button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
|
|
202
|
+
button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+Math.max(0,Math.min(doc.documentElement.clientWidth-44,last.right-doc.body.getBoundingClientRect().left))+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
|
|
203
|
+
var key = Math.floor(last.top/44);
|
|
204
|
+
var neighbors=noteGroups.get(key);
|
|
205
|
+
if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
|
|
206
|
+
neighbors.ids.push(annotation.id);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
noteGroups.forEach(function(group){
|
|
210
|
+
group.button.textContent=group.ids.length>1?String(group.ids.length):'●';
|
|
211
|
+
group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
|
|
212
|
+
});
|
|
213
|
+
registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
|
|
214
|
+
}
|
|
215
|
+
function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
|
|
216
|
+
function tapMarkup(event) {
|
|
217
|
+
if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
|
|
218
|
+
var hit=hitRegions.find(function(item){return item.rects.some(function(r){return event.clientX>=r.left && event.clientX<=r.right && event.clientY>=r.top && event.clientY<=r.bottom;});});
|
|
219
|
+
if(hit) {event.stopPropagation();options.onTap(hit.id);}
|
|
220
|
+
}
|
|
221
|
+
doc.addEventListener('click',tapMarkup);
|
|
222
|
+
var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
|
|
223
|
+
if (observer) observer.observe(doc.body);
|
|
224
|
+
win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
|
|
225
|
+
if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
|
|
226
|
+
schedule();
|
|
227
|
+
return {schedule:schedule, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
|
|
228
|
+
}
|
|
229
|
+
return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/* @papyrus-epub-annotations:end */
|
|
112
233
|
const viewer = document.getElementById('viewer');
|
|
113
234
|
const DEFAULT_FONT_SIZE = 16;
|
|
114
235
|
const TEXT_PAGE_CHUNK = 1600;
|
|
@@ -116,6 +237,11 @@
|
|
|
116
237
|
let currentType = null;
|
|
117
238
|
let book = null;
|
|
118
239
|
let rendition = null;
|
|
240
|
+
let documentSessionId = null;
|
|
241
|
+
let annotationDocumentId;
|
|
242
|
+
let annotationLabels = {};
|
|
243
|
+
let epubAnnotations = [];
|
|
244
|
+
const epubAnnotationRenderers = new Map();
|
|
119
245
|
let spineItems = [];
|
|
120
246
|
let textPages = [];
|
|
121
247
|
let textContainer = null;
|
|
@@ -523,6 +649,8 @@
|
|
|
523
649
|
};
|
|
524
650
|
|
|
525
651
|
const clearViewer = () => {
|
|
652
|
+
epubAnnotationRenderers.forEach(renderer => renderer.destroy());
|
|
653
|
+
epubAnnotationRenderers.clear();
|
|
526
654
|
if (epubInteractionCleanup) {
|
|
527
655
|
epubInteractionCleanup();
|
|
528
656
|
epubInteractionCleanup = null;
|
|
@@ -1180,6 +1308,7 @@
|
|
|
1180
1308
|
clearViewer();
|
|
1181
1309
|
rendition = book.renderTo(viewer, {
|
|
1182
1310
|
manager: 'continuous',
|
|
1311
|
+
allowScriptedContent: false,
|
|
1183
1312
|
width: '100%',
|
|
1184
1313
|
height: '100%',
|
|
1185
1314
|
flow: 'scrolled-continuous',
|
|
@@ -1188,10 +1317,22 @@
|
|
|
1188
1317
|
sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
|
|
1189
1318
|
|
|
1190
1319
|
if (rendition && rendition.hooks && rendition.hooks.content) {
|
|
1320
|
+
const session = documentSessionId;
|
|
1321
|
+
const activeRendition = rendition;
|
|
1191
1322
|
rendition.hooks.content.register((contents) => {
|
|
1323
|
+
const href = spineItems[contents.sectionIndex]?.href;
|
|
1324
|
+
if (href && window.PapyrusEpubAnnotations) {
|
|
1325
|
+
const renderer = window.PapyrusEpubAnnotations.install(contents, {
|
|
1326
|
+
href, get documentId() { return annotationDocumentId; },
|
|
1327
|
+
isCurrent: () => session === documentSessionId && activeRendition === rendition,
|
|
1328
|
+
annotations: () => epubAnnotations, labels: () => annotationLabels,
|
|
1329
|
+
onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
|
|
1330
|
+
});
|
|
1331
|
+
epubAnnotationRenderers.set(contents,renderer);
|
|
1332
|
+
}
|
|
1192
1333
|
const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
|
|
1193
1334
|
if (frame) {
|
|
1194
|
-
frame.setAttribute('sandbox', 'allow-
|
|
1335
|
+
frame.setAttribute('sandbox', 'allow-same-origin');
|
|
1195
1336
|
}
|
|
1196
1337
|
const selectionDocument = contents && contents.document;
|
|
1197
1338
|
const selectionWindow = contents && contents.window;
|
|
@@ -1201,13 +1342,46 @@
|
|
|
1201
1342
|
selectionWindow &&
|
|
1202
1343
|
typeof selectionWindow.getSelection === 'function'
|
|
1203
1344
|
) {
|
|
1345
|
+
let selectionTimer = null;
|
|
1346
|
+
let lastSelectionKey = '';
|
|
1347
|
+
let lastSelectionAt = 0;
|
|
1348
|
+
const reportSelection = () => {
|
|
1349
|
+
selectionTimer = null;
|
|
1350
|
+
if (session !== documentSessionId || activeRendition !== rendition) return;
|
|
1351
|
+
const selected = selectionWindow.getSelection();
|
|
1352
|
+
if (!selected || selected.isCollapsed || selected.rangeCount === 0 ||
|
|
1353
|
+
typeof contents.cfiFromRange !== 'function') return;
|
|
1354
|
+
const range = selected.getRangeAt(0).cloneRange();
|
|
1355
|
+
if (!range.toString().trim()) return;
|
|
1356
|
+
const href = spineItems[contents.sectionIndex]?.href;
|
|
1357
|
+
if (!href) return;
|
|
1358
|
+
let cfi;
|
|
1359
|
+
try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
|
|
1360
|
+
const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
|
|
1361
|
+
if (!anchor) return;
|
|
1362
|
+
const key = anchor.cfiRange + ':' + anchor.quote;
|
|
1363
|
+
const now = Date.now();
|
|
1364
|
+
if (key === lastSelectionKey && now - lastSelectionAt < 750) return;
|
|
1365
|
+
lastSelectionKey = key;
|
|
1366
|
+
lastSelectionAt = now;
|
|
1367
|
+
const payload = {documentSessionId: session, text: anchor.quote,
|
|
1368
|
+
pageIndex: contents.sectionIndex + 1, anchor};
|
|
1369
|
+
nativeMenuSelection = {contents, payload};
|
|
1370
|
+
sendEvent('EPUB_TEXT_SELECTED', payload);
|
|
1371
|
+
};
|
|
1204
1372
|
const handleSelectionChange = () => {
|
|
1205
1373
|
const selection = selectionWindow.getSelection();
|
|
1206
1374
|
const text = selection ? selection.toString().trim() : '';
|
|
1207
1375
|
epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
|
|
1376
|
+
if (selectionTimer !== null) clearTimeout(selectionTimer);
|
|
1377
|
+
// epub.js "selected" is not reliable on every iOS WKWebView path.
|
|
1378
|
+
// The content document still emits selectionchange, so resolve its
|
|
1379
|
+
// live Range to a CFI before the native menu action can fire.
|
|
1380
|
+
if (text) reportSelection();
|
|
1208
1381
|
};
|
|
1209
1382
|
selectionDocument.addEventListener('selectionchange', handleSelectionChange);
|
|
1210
1383
|
const selectionCleanup = () => {
|
|
1384
|
+
if (selectionTimer !== null) clearTimeout(selectionTimer);
|
|
1211
1385
|
selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
|
|
1212
1386
|
epubSelectionCleanups.delete(selectionCleanup);
|
|
1213
1387
|
};
|
|
@@ -1228,22 +1402,30 @@
|
|
|
1228
1402
|
rendition.hooks.unloaded.register((view) => {
|
|
1229
1403
|
const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
|
|
1230
1404
|
if (typeof cleanup === 'function') cleanup();
|
|
1405
|
+
const renderer = epubAnnotationRenderers.get(view?.contents);
|
|
1406
|
+
if (renderer) renderer.destroy();
|
|
1407
|
+
epubAnnotationRenderers.delete(view?.contents);
|
|
1231
1408
|
});
|
|
1232
1409
|
}
|
|
1233
1410
|
|
|
1234
1411
|
if (rendition && typeof rendition.on === 'function') {
|
|
1412
|
+
const selectionSession = documentSessionId;
|
|
1413
|
+
const selectionRendition = rendition;
|
|
1235
1414
|
rendition.on('selected', (cfiRange, contents) => {
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
if (
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1415
|
+
if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
|
|
1416
|
+
const href = spineItems[contents.sectionIndex]?.href;
|
|
1417
|
+
const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
|
|
1418
|
+
if (!anchor) return;
|
|
1419
|
+
epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
|
|
1420
|
+
const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
|
|
1421
|
+
nativeMenuSelection = {contents, payload};
|
|
1422
|
+
sendEvent('EPUB_TEXT_SELECTED', payload);
|
|
1245
1423
|
});
|
|
1424
|
+
['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
|
|
1425
|
+
if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
|
|
1426
|
+
}));
|
|
1246
1427
|
rendition.on('relocated', (location) => {
|
|
1428
|
+
if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
|
|
1247
1429
|
const start = location && location.start;
|
|
1248
1430
|
let index = start && Number.isInteger(start.index) ? start.index : -1;
|
|
1249
1431
|
if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
|
|
@@ -1362,17 +1544,27 @@
|
|
|
1362
1544
|
sendEvent('VIEWER_SCROLL', { offsetY });
|
|
1363
1545
|
};
|
|
1364
1546
|
const handleTouchStart = (event) => {
|
|
1365
|
-
touchStart = getTouchPoint(event);
|
|
1547
|
+
touchStart = event?.touches?.length > 1 ? null : getTouchPoint(event);
|
|
1366
1548
|
epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
|
|
1367
1549
|
};
|
|
1368
1550
|
const handleTouchMove = (event) => {
|
|
1551
|
+
if (event?.touches?.length > 1) touchStart = null;
|
|
1369
1552
|
epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
|
|
1370
1553
|
};
|
|
1371
1554
|
const handleTouchEnd = (event) => {
|
|
1372
1555
|
const end = getTouchPoint(event);
|
|
1373
1556
|
const start = touchStart;
|
|
1374
1557
|
touchStart = null;
|
|
1375
|
-
|
|
1558
|
+
const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
|
|
1559
|
+
if (!selectedText && start && end && epubViewMode === 'single' && Math.abs(end.x-start.x) >= 48 && Math.abs(end.x-start.x) > Math.abs(end.y-start.y)*1.5) {
|
|
1560
|
+
const active = rendition;
|
|
1561
|
+
const interactionSession = documentSessionId;
|
|
1562
|
+
epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
|
|
1563
|
+
if (active !== rendition || interactionSession !== documentSessionId) return;
|
|
1564
|
+
return end.x < start.x ? active.next() : active.prev();
|
|
1565
|
+
});
|
|
1566
|
+
epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR', {message:String(error)}));
|
|
1567
|
+
} else if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
|
|
1376
1568
|
sendEvent('VIEWER_TAP', {});
|
|
1377
1569
|
}
|
|
1378
1570
|
epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
|
|
@@ -1417,6 +1609,8 @@
|
|
|
1417
1609
|
return { pageCount, outline };
|
|
1418
1610
|
};
|
|
1419
1611
|
|
|
1612
|
+
let epubNavigationQueue = Promise.resolve();
|
|
1613
|
+
let epubNavigationRequest = 0;
|
|
1420
1614
|
const displayEpubPage = async (pageIndex) => {
|
|
1421
1615
|
if (!rendition) return;
|
|
1422
1616
|
const item = spineItems[pageIndex];
|
|
@@ -1426,9 +1620,18 @@
|
|
|
1426
1620
|
target,
|
|
1427
1621
|
pageIndex,
|
|
1428
1622
|
});
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1623
|
+
const activeRendition = rendition;
|
|
1624
|
+
const session = documentSessionId;
|
|
1625
|
+
const request = ++epubNavigationRequest;
|
|
1626
|
+
const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
|
|
1627
|
+
if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
|
|
1628
|
+
await activeRendition.display(target);
|
|
1629
|
+
if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
|
|
1630
|
+
currentPage = pageIndex + 1;
|
|
1631
|
+
sendState();
|
|
1632
|
+
});
|
|
1633
|
+
epubNavigationQueue = navigation;
|
|
1634
|
+
await navigation;
|
|
1432
1635
|
};
|
|
1433
1636
|
|
|
1434
1637
|
const getTextContent = async (pageIndex) => {
|
|
@@ -1529,6 +1732,9 @@
|
|
|
1529
1732
|
}
|
|
1530
1733
|
};
|
|
1531
1734
|
|
|
1735
|
+
let nativeMenuSelection = null;
|
|
1736
|
+
let epubViewMode = "continuous";
|
|
1737
|
+
|
|
1532
1738
|
const handleCommand = async (message) => {
|
|
1533
1739
|
const { id, kind, payload } = message;
|
|
1534
1740
|
|
|
@@ -1536,7 +1742,13 @@
|
|
|
1536
1742
|
if (kind === 'load') {
|
|
1537
1743
|
const generation = (epubLoadGeneration += 1);
|
|
1538
1744
|
currentType = payload.type;
|
|
1745
|
+
documentSessionId = payload.documentSessionId || null;
|
|
1746
|
+
epubAnnotations = [];
|
|
1747
|
+
annotationDocumentId = undefined;
|
|
1539
1748
|
zoom = 1.0;
|
|
1749
|
+
epubViewMode = "continuous";
|
|
1750
|
+
epubNavigationRequest += 1;
|
|
1751
|
+
epubNavigationQueue = Promise.resolve();
|
|
1540
1752
|
|
|
1541
1753
|
if (currentType === 'text') {
|
|
1542
1754
|
const result = await loadText(
|
|
@@ -1576,6 +1788,42 @@
|
|
|
1576
1788
|
throw new Error('Unsupported document type');
|
|
1577
1789
|
}
|
|
1578
1790
|
|
|
1791
|
+
if (kind === 'epub-annotations' || kind === 'epub-annotation-navigate') {
|
|
1792
|
+
if (currentType !== 'epub' || !documentSessionId || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB annotation session');
|
|
1793
|
+
if (kind === 'epub-annotations') {
|
|
1794
|
+
const incoming = payload.annotations;
|
|
1795
|
+
if (!Array.isArray(incoming) || incoming.length > 10000 || incoming.some(a => !a || typeof a.id !== 'string' || a.id.length > 256)) throw new Error('Invalid EPUB annotations');
|
|
1796
|
+
const ids = new Set();
|
|
1797
|
+
epubAnnotations = incoming.filter(a => {
|
|
1798
|
+
if (ids.has(a.id) || !window.PapyrusEpubAnnotations.validAnchor(a.anchor)) return false;
|
|
1799
|
+
ids.add(a.id); return true;
|
|
1800
|
+
});
|
|
1801
|
+
annotationDocumentId = typeof payload.documentId === 'string' ? payload.documentId : undefined;
|
|
1802
|
+
annotationLabels = {comment: typeof payload.labels?.comment === 'string' ? payload.labels.comment.slice(0,128) : ''};
|
|
1803
|
+
epubAnnotationRenderers.forEach(renderer => renderer.schedule());
|
|
1804
|
+
} else {
|
|
1805
|
+
const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
|
|
1806
|
+
if (!annotation || (annotation.anchor.documentId && annotation.anchor.documentId !== annotationDocumentId)) throw new Error('Annotation not available for this document');
|
|
1807
|
+
// Navigation uses the CFI, never a synthetic percentage/page rectangle.
|
|
1808
|
+
const navigationSession = documentSessionId;
|
|
1809
|
+
const activeRendition = rendition;
|
|
1810
|
+
// Open the confirmed chapter first, resolve its quote, then navigate its actual range.
|
|
1811
|
+
epubNavigationRequest += 1;
|
|
1812
|
+
const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
|
|
1813
|
+
if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
|
|
1814
|
+
await activeRendition.display(annotation.anchor.href);
|
|
1815
|
+
if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
|
|
1816
|
+
const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
|
|
1817
|
+
const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
|
|
1818
|
+
if(!range)throw new Error('EPUB anchor unresolved');
|
|
1819
|
+
await activeRendition.display(contents.cfiFromRange(range));
|
|
1820
|
+
});
|
|
1821
|
+
epubNavigationQueue = navigation;
|
|
1822
|
+
await navigation;
|
|
1823
|
+
}
|
|
1824
|
+
sendResponse(id,true,null); return;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1579
1827
|
if (kind === 'go-to-page') {
|
|
1580
1828
|
const page = Math.max(1, payload.page || 1);
|
|
1581
1829
|
if (currentType === 'text') {
|
|
@@ -1598,6 +1846,33 @@
|
|
|
1598
1846
|
return;
|
|
1599
1847
|
}
|
|
1600
1848
|
|
|
1849
|
+
if (kind === 'epub-turn-page') {
|
|
1850
|
+
if (epubViewMode !== 'single') {sendResponse(id,true,null); return;}
|
|
1851
|
+
if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB navigation');
|
|
1852
|
+
const active = rendition;
|
|
1853
|
+
epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
|
|
1854
|
+
if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
|
|
1855
|
+
return payload.direction < 0 ? active.prev() : active.next();
|
|
1856
|
+
});
|
|
1857
|
+
await epubNavigationQueue;
|
|
1858
|
+
sendResponse(id,true,null); return;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (kind === 'set-epub-view-mode') {
|
|
1862
|
+
if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
|
|
1863
|
+
epubViewMode = payload.viewMode === 'single' ? 'single' : 'continuous';
|
|
1864
|
+
const active = rendition;
|
|
1865
|
+
epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
|
|
1866
|
+
if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
|
|
1867
|
+
const flow = epubViewMode === 'single' ? 'paginated' : 'scrolled-continuous';
|
|
1868
|
+
if (active.settings.flow === flow) return;
|
|
1869
|
+
active.flow(flow);
|
|
1870
|
+
return active.q.enqueue(() => Promise.resolve());
|
|
1871
|
+
});
|
|
1872
|
+
await epubNavigationQueue;
|
|
1873
|
+
sendResponse(id,true,null); return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1601
1876
|
if (kind === 'set-zoom') {
|
|
1602
1877
|
zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
|
|
1603
1878
|
applyZoom();
|
|
@@ -1745,6 +2020,26 @@
|
|
|
1745
2020
|
return;
|
|
1746
2021
|
}
|
|
1747
2022
|
|
|
2023
|
+
if (message.type === 'epub-selection-action') {
|
|
2024
|
+
const selected = nativeMenuSelection;
|
|
2025
|
+
if (!selected || message.documentSessionId !== documentSessionId ||
|
|
2026
|
+
selected.payload.documentSessionId !== documentSessionId) return;
|
|
2027
|
+
if (message.key === 'selectAll') {
|
|
2028
|
+
const doc = selected.contents.document;
|
|
2029
|
+
const range = doc.createRange();
|
|
2030
|
+
range.selectNodeContents(doc.body);
|
|
2031
|
+
const selection = selected.contents.window.getSelection();
|
|
2032
|
+
selection.removeAllRanges();
|
|
2033
|
+
selection.addRange(range);
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (!['copy','define','highlight','underline','strikeout','comment'].includes(message.key)) return;
|
|
2037
|
+
sendEvent('EPUB_SELECTION_ACTION', {...selected.payload, action: message.key});
|
|
2038
|
+
selected.contents.window.getSelection()?.removeAllRanges();
|
|
2039
|
+
nativeMenuSelection = null;
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1748
2043
|
if (!message.kind) return;
|
|
1749
2044
|
handleCommand(message);
|
|
1750
2045
|
};
|
|
@@ -17,7 +17,8 @@ const replaceMarkedSource = (content, start, end, replacement) => {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const runtimePath = resolve(runtimeDirectory, "runtime.js");
|
|
20
|
-
const
|
|
20
|
+
const epubSource = await readFile(resolve(runtimeDirectory,"epubAnnotations.js"),"utf8");
|
|
21
|
+
const runtime = replaceMarkedSource(await readFile(runtimePath, "utf8"), "/* @papyrus-epub-annotations:start */", "/* @papyrus-epub-annotations:end */", `\n${epubSource}\n`);
|
|
21
22
|
const comicRuntimeEnd = "/* @papyrus-comic-runtime:end */";
|
|
22
23
|
const runtimeBodyStart = runtime.indexOf("\n(function () {", runtime.indexOf(comicRuntimeEnd));
|
|
23
24
|
if (runtimeBodyStart < 0) {
|