@papyrus-sdk/ui-react-native 0.2.35-beta.10 → 0.2.35-beta.11.epub.1

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.
@@ -109,6 +109,42 @@
109
109
  /* @papyrus-comic-runtime:end */
110
110
 
111
111
  (function () {
112
+ /* @papyrus-epub-reader:start */
113
+ (function(root){
114
+ function mode(value){return value==='chapter-scroll'?'chapter-scroll':'paged';}
115
+ function validCfi(value){return typeof value==='string' && value.length<=4096 && /^epubcfi\([^\n<>]+\)$/.test(value);}
116
+ function options(value){return {manager:'default',flow:mode(value)==='paged'?'paginated':'scrolled-doc',spread:'none',width:'100%',height:'100%',allowScriptedContent:false};}
117
+ function gesture(start,end,state){
118
+ if(!start || !end || state.selected || state.interactive || state.multitouch)return null;
119
+ var dx=end.x-start.x,dy=end.y-start.y,elapsed=end.time-start.time;
120
+ if(state.mode==='paged' && Math.abs(dx)>=48 && Math.abs(dx)>Math.abs(dy)*1.5 && elapsed>=0 && elapsed<1500)return (dx<0)!==Boolean(state.rtl)?'next':'prev';
121
+ if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
122
+ return null;
123
+ }
124
+ function location(raw,count,readingMode){
125
+ var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
126
+ var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
127
+ var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
128
+ var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
129
+ var fraction=total?Math.max(0,(visualPage-1)/total):0;
130
+ return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:Boolean(raw.atStart),atEnd:Boolean(raw.atEnd)};
131
+ }
132
+ function install(contents,config){
133
+ var doc=contents.document,win=contents.window,start=null,blocked=false;
134
+ function point(e){var t=e.changedTouches&&e.changedTouches[0]||e.touches&&e.touches[0];return t?{x:t.clientX,y:t.clientY,time:Date.now()}:null;}
135
+ function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
136
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
137
+ function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
138
+ function move(e){if(e.touches.length!==1)blocked=true;}
139
+ function end(e){var action=gesture(start,point(e),{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
140
+ function cancel(){start=null;blocked=true;}
141
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});
142
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);};
143
+ }
144
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,install:install};
145
+ })(typeof window==='undefined'?globalThis:window);
146
+
147
+ /* @papyrus-epub-reader:end */
112
148
  /* @papyrus-epub-annotations:start */
113
149
  (function (root, factory) {
114
150
  if (typeof module === 'object' && module.exports) module.exports = factory();
@@ -187,6 +223,7 @@
187
223
  var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
188
224
  var style = annotation.markupStyle || annotation.type;
189
225
  var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
226
+ if(style === 'highlight')opacity=Math.min(0.35,opacity);
190
227
  rects.forEach(function(rect) {
191
228
  var mark = doc.createElement('div');
192
229
  var bodyRect=doc.body.getBoundingClientRect();
@@ -199,8 +236,10 @@
199
236
  if (hasNote) {
200
237
  var last = rects[rects.length-1], button = doc.createElement('button');
201
238
  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);
239
+ var pageWidth=Math.max(44, options.pageWidth ? options.pageWidth() : win.innerWidth);
240
+ var bodyRect=doc.body.getBoundingClientRect(), column=Math.max(0,Math.floor((last.left-bodyRect.left)/pageWidth));
241
+ button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+((column+1)*pageWidth-44)+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
242
+ var key = column+":"+Math.floor(last.top/44);
204
243
  var neighbors=noteGroups.get(key);
205
244
  if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
206
245
  neighbors.ids.push(annotation.id);
@@ -221,10 +260,10 @@
221
260
  doc.addEventListener('click',tapMarkup);
222
261
  var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
223
262
  if (observer) observer.observe(doc.body);
224
- win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
263
+ win.addEventListener('resize',schedule);win.addEventListener('scroll',schedule); doc.addEventListener('load',schedule,true);
225
264
  if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
226
265
  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();}};
266
+ return {schedule:schedule, hit:function(x,y){return hitRegions.some(function(item){return item.rects.some(function(r){return x>=r.left && x<=r.right && y>=r.top && y<=r.bottom;});});}, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);win.removeEventListener('scroll',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
228
267
  }
229
268
  return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
230
269
  });
@@ -1305,26 +1344,55 @@
1305
1344
  pageCount,
1306
1345
  });
1307
1346
 
1347
+ if (book.locations?.generate) {
1348
+ const activeBook=book;
1349
+ activeBook.locations.generate(150).then(() => {
1350
+ if (isCurrentLoad() && activeBook===book && epubLocation) {
1351
+ epubLocation.progress=activeBook.locations.percentageFromCfi(epubLocation.cfi);
1352
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
1353
+ }
1354
+ }).catch(() => {});
1355
+ }
1356
+ return renderEpubRendition(loadId,generation,null);
1357
+ };
1358
+
1359
+ const renderEpubRendition = async (loadId,generation,target) => {
1360
+ const diagnosticPayload={loadId};
1361
+ const isCurrentLoad=()=>generation===epubLoadGeneration;
1362
+ if(epubInteractionCleanup)epubInteractionCleanup();
1363
+ if(rendition)rendition.destroy();
1308
1364
  clearViewer();
1309
- rendition = book.renderTo(viewer, {
1310
- manager: 'continuous',
1311
- allowScriptedContent: false,
1312
- width: '100%',
1313
- height: '100%',
1314
- flow: 'scrolled-continuous',
1315
- spread: 'none',
1316
- });
1365
+ rendition = book.renderTo(viewer, window.PapyrusEpubReader.options(epubViewMode === 'single' ? 'paged' : 'chapter-scroll'));
1317
1366
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
1318
1367
 
1319
1368
  if (rendition && rendition.hooks && rendition.hooks.content) {
1320
1369
  const session = documentSessionId;
1321
1370
  const activeRendition = rendition;
1322
1371
  rendition.hooks.content.register((contents) => {
1372
+ const removeGestureListeners = window.PapyrusEpubReader.install(contents, {
1373
+ mode: () => epubViewMode === 'single' ? 'paged' : 'chapter-scroll',
1374
+ rtl: () => book.package?.metadata?.direction === 'rtl' || book.spine?.direction === 'rtl',
1375
+ blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
1376
+ annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
1377
+ current: () => session === documentSessionId && activeRendition === rendition,
1378
+ tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
1379
+ turn: direction => {
1380
+ epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
1381
+ if(session !== documentSessionId || activeRendition !== rendition)return;
1382
+ return direction<0?activeRendition.prev():activeRendition.next();
1383
+ });
1384
+ epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
1385
+ },
1386
+ });
1387
+ const gestureCleanup = () => {removeGestureListeners();epubSelectionCleanups.delete(gestureCleanup);};
1388
+ epubSelectionCleanups.add(gestureCleanup);
1389
+ contents.__papyrusGestureCleanup=gestureCleanup;
1323
1390
  const href = spineItems[contents.sectionIndex]?.href;
1324
1391
  if (href && window.PapyrusEpubAnnotations) {
1325
1392
  const renderer = window.PapyrusEpubAnnotations.install(contents, {
1326
1393
  href, get documentId() { return annotationDocumentId; },
1327
1394
  isCurrent: () => session === documentSessionId && activeRendition === rendition,
1395
+ pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
1328
1396
  annotations: () => epubAnnotations, labels: () => annotationLabels,
1329
1397
  onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
1330
1398
  });
@@ -1343,8 +1411,7 @@
1343
1411
  typeof selectionWindow.getSelection === 'function'
1344
1412
  ) {
1345
1413
  let selectionTimer = null;
1346
- let lastSelectionKey = '';
1347
- let lastSelectionAt = 0;
1414
+
1348
1415
  const reportSelection = () => {
1349
1416
  selectionTimer = null;
1350
1417
  if (session !== documentSessionId || activeRendition !== rendition) return;
@@ -1359,11 +1426,7 @@
1359
1426
  try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
1360
1427
  const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
1361
1428
  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;
1429
+ if (nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
1367
1430
  const payload = {documentSessionId: session, text: anchor.quote,
1368
1431
  pageIndex: contents.sectionIndex + 1, anchor};
1369
1432
  nativeMenuSelection = {contents, payload};
@@ -1378,6 +1441,7 @@
1378
1441
  // The content document still emits selectionchange, so resolve its
1379
1442
  // live Range to a CFI before the native menu action can fire.
1380
1443
  if (text) reportSelection();
1444
+ else if (nativeMenuSelection?.contents === contents) nativeMenuSelection = null;
1381
1445
  };
1382
1446
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
1383
1447
  const selectionCleanup = () => {
@@ -1400,6 +1464,7 @@
1400
1464
  typeof rendition.hooks.unloaded.register === 'function'
1401
1465
  ) {
1402
1466
  rendition.hooks.unloaded.register((view) => {
1467
+ view?.contents?.__papyrusGestureCleanup?.();
1403
1468
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
1404
1469
  if (typeof cleanup === 'function') cleanup();
1405
1470
  const renderer = epubAnnotationRenderers.get(view?.contents);
@@ -1415,21 +1480,27 @@
1415
1480
  if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
1416
1481
  const href = spineItems[contents.sectionIndex]?.href;
1417
1482
  const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
1418
- if (!anchor) return;
1483
+ if (!anchor || nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
1419
1484
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
1420
1485
  const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
1421
1486
  nativeMenuSelection = {contents, payload};
1422
1487
  sendEvent('EPUB_TEXT_SELECTED', payload);
1423
1488
  });
1424
- ['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
1489
+ ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
1425
1490
  if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1426
1491
  }));
1427
1492
  rendition.on('relocated', (location) => {
1493
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
1428
1494
  const start = location && location.start;
1429
1495
  let index = start && Number.isInteger(start.index) ? start.index : -1;
1430
1496
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
1431
1497
  if (index < 0 || index >= spineItems.length) return;
1432
1498
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
1499
+ epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll');
1500
+ if(epubLocation){
1501
+ if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
1502
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
1503
+ }
1433
1504
  const nextPage = index + 1;
1434
1505
  if (nextPage === currentPage) return;
1435
1506
  currentPage = nextPage;
@@ -1437,9 +1508,10 @@
1437
1508
  });
1438
1509
  }
1439
1510
 
1511
+ applyEpubZoom();
1440
1512
  sendEpubDiagnostic('epub.display.start', diagnosticPayload);
1441
1513
  await withEpubStageTimeout(
1442
- displayEpubPage(0),
1514
+ rendition.display(target || spineItems[0]?.href),
1443
1515
  'rendition.display',
1444
1516
  diagnosticPayload,
1445
1517
  );
@@ -1451,7 +1523,6 @@
1451
1523
  typeof rendition.manager.on === 'function' &&
1452
1524
  typeof rendition.on === 'function'
1453
1525
  ) {
1454
- let touchStart = null;
1455
1526
  let lastScrollOffset = -1;
1456
1527
  const manager = rendition.manager;
1457
1528
  epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
@@ -1526,48 +1597,19 @@
1526
1597
  return result;
1527
1598
  };
1528
1599
  }
1529
- const getTouchPoint = (event) => {
1530
- const touch =
1531
- (event && event.changedTouches && event.changedTouches[0]) ||
1532
- (event && event.touches && event.touches[0]);
1533
- if (!touch) return null;
1534
- const x = Number(touch.screenX ?? touch.clientX);
1535
- const y = Number(touch.screenY ?? touch.clientY);
1536
- return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
1537
- };
1538
- const handleScroll = (event) => {
1600
+ const handleScroll = event => {
1539
1601
  const offsetY = Math.max(0, Number(event && event.top) || 0);
1540
1602
  epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
1541
- if (Math.abs(offsetY - lastScrollOffset) < 1) return;
1542
- lastScrollOffset = offsetY;
1543
- sendEvent('VIEWER_SCROLL', { offsetY });
1544
- };
1545
- const handleTouchStart = (event) => {
1546
- touchStart = getTouchPoint(event);
1547
- epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
1548
- };
1549
- const handleTouchMove = (event) => {
1550
- epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
1551
- };
1552
- const handleTouchEnd = (event) => {
1553
- const end = getTouchPoint(event);
1554
- const start = touchStart;
1555
- touchStart = null;
1556
- const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
1557
- if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
1558
- sendEvent('VIEWER_TAP', {});
1559
- }
1560
- epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
1603
+ if (Math.abs(offsetY-lastScrollOffset)<1) return;
1604
+ lastScrollOffset=offsetY;
1605
+ sendEvent('VIEWER_SCROLL',{offsetY});
1561
1606
  };
1562
-
1563
1607
  manager.on('scroll', handleScroll);
1564
1608
  const handleScrolled = (event) => {
1565
1609
  epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
1566
1610
  };
1567
1611
  manager.on('scrolled', handleScrolled);
1568
- rendition.on('touchstart', handleTouchStart);
1569
- rendition.on('touchmove', handleTouchMove);
1570
- rendition.on('touchend', handleTouchEnd);
1612
+
1571
1613
  epubInteractionCleanup = () => {
1572
1614
  epubSelectionCleanups.forEach((cleanup) => cleanup());
1573
1615
  epubSelectionCleanups.clear();
@@ -1576,16 +1618,9 @@
1576
1618
  if (originalEnqueue) queue.enqueue = originalEnqueue;
1577
1619
  if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
1578
1620
  if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
1579
- if (typeof rendition.off === 'function') {
1580
- rendition.off('touchstart', handleTouchStart);
1581
- rendition.off('touchmove', handleTouchMove);
1582
- rendition.off('touchend', handleTouchEnd);
1583
- }
1584
1621
  epubScrollDiagnostics = null;
1585
1622
  };
1586
1623
  }
1587
- applyEpubZoom();
1588
-
1589
1624
  const outline = await withEpubStageTimeout(
1590
1625
  buildOutline(),
1591
1626
  'outline',
@@ -1599,6 +1634,8 @@
1599
1634
  return { pageCount, outline };
1600
1635
  };
1601
1636
 
1637
+ let epubNavigationQueue = Promise.resolve();
1638
+ let epubNavigationRequest = 0;
1602
1639
  const displayEpubPage = async (pageIndex) => {
1603
1640
  if (!rendition) return;
1604
1641
  const item = spineItems[pageIndex];
@@ -1608,9 +1645,18 @@
1608
1645
  target,
1609
1646
  pageIndex,
1610
1647
  });
1611
- await rendition.display(target);
1612
- currentPage = pageIndex + 1;
1613
- sendState();
1648
+ const activeRendition = rendition;
1649
+ const session = documentSessionId;
1650
+ const request = ++epubNavigationRequest;
1651
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
1652
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
1653
+ await activeRendition.display(target);
1654
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
1655
+ currentPage = pageIndex + 1;
1656
+ sendState();
1657
+ });
1658
+ epubNavigationQueue = navigation;
1659
+ await navigation;
1614
1660
  };
1615
1661
 
1616
1662
  const getTextContent = async (pageIndex) => {
@@ -1712,6 +1758,8 @@
1712
1758
  };
1713
1759
 
1714
1760
  let nativeMenuSelection = null;
1761
+ let epubViewMode = "single";
1762
+ let epubLocation = null;
1715
1763
 
1716
1764
  const handleCommand = async (message) => {
1717
1765
  const { id, kind, payload } = message;
@@ -1724,6 +1772,11 @@
1724
1772
  epubAnnotations = [];
1725
1773
  annotationDocumentId = undefined;
1726
1774
  zoom = 1.0;
1775
+ epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
1776
+ epubLocation = null;
1777
+ nativeMenuSelection = null;
1778
+ epubNavigationRequest += 1;
1779
+ epubNavigationQueue = Promise.resolve();
1727
1780
 
1728
1781
  if (currentType === 'text') {
1729
1782
  const result = await loadText(
@@ -1778,17 +1831,23 @@
1778
1831
  epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1779
1832
  } else {
1780
1833
  const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
1781
- if (!annotation) throw new Error('Annotation not available');
1834
+ if (!annotation || (annotation.anchor.documentId && annotation.anchor.documentId !== annotationDocumentId)) throw new Error('Annotation not available for this document');
1782
1835
  // Navigation uses the CFI, never a synthetic percentage/page rectangle.
1783
1836
  const navigationSession = documentSessionId;
1784
1837
  const activeRendition = rendition;
1785
1838
  // Open the confirmed chapter first, resolve its quote, then navigate its actual range.
1786
- await activeRendition.display(annotation.anchor.href);
1787
- if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
1788
- const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
1789
- const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
1790
- if(!range)throw new Error('EPUB anchor unresolved');
1791
- await activeRendition.display(contents.cfiFromRange(range));
1839
+ epubNavigationRequest += 1;
1840
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
1841
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
1842
+ await activeRendition.display(annotation.anchor.href);
1843
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
1844
+ const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
1845
+ const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
1846
+ if(!range)throw new Error('EPUB anchor unresolved');
1847
+ await activeRendition.display(contents.cfiFromRange(range));
1848
+ });
1849
+ epubNavigationQueue = navigation;
1850
+ await navigation;
1792
1851
  }
1793
1852
  sendResponse(id,true,null); return;
1794
1853
  }
@@ -1815,12 +1874,54 @@
1815
1874
  return;
1816
1875
  }
1817
1876
 
1877
+ if (kind === 'epub-turn-page') {
1878
+ if (epubViewMode !== 'single') {sendResponse(id,true,null); return;}
1879
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB navigation');
1880
+ const active = rendition;
1881
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
1882
+ if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
1883
+ return payload.direction < 0 ? active.prev() : active.next();
1884
+ });
1885
+ await epubNavigationQueue;
1886
+ sendResponse(id,true,null); return;
1887
+ }
1888
+
1889
+ if (kind === 'set-epub-view-mode') {
1890
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
1891
+ const next=payload.epubMode ? (payload.epubMode==='paged'?'single':'continuous') : (payload.viewMode==='single'?'single':'continuous');
1892
+ {
1893
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
1894
+ if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
1895
+ const target=epubLocation?.cfi;
1896
+ epubViewMode=next;
1897
+ await renderEpubRendition('layout',epubLoadGeneration,target);
1898
+ });
1899
+ await epubNavigationQueue;
1900
+ }
1901
+ sendResponse(id,true,null);return;
1902
+ }
1903
+ if(kind === 'epub-go-to-location'){
1904
+ if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
1905
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>payload.documentSessionId===documentSessionId?rendition.display(payload.cfi):undefined);
1906
+ await epubNavigationQueue;sendResponse(id,true,null);return;
1907
+ }
1908
+
1818
1909
  if (kind === 'set-zoom') {
1819
- zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
1820
- applyZoom();
1821
- sendState();
1822
- sendResponse(id, true, { zoom });
1823
- return;
1910
+ const requestedZoom=Math.max(0.5,Math.min(4.0,payload.zoom || 1.0));
1911
+ if(currentType === 'epub') {
1912
+ const session=documentSessionId;
1913
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
1914
+ if(session!==documentSessionId)return;
1915
+ const active=rendition;
1916
+ const cfi=epubLocation?.cfi;
1917
+ zoom=requestedZoom;applyEpubZoom();
1918
+ // Let the content CSS update before redisplaying the logical anchor.
1919
+ await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));
1920
+ if(cfi && active===rendition && session===documentSessionId)await active.display(cfi);
1921
+ });
1922
+ await epubNavigationQueue;
1923
+ } else {zoom=requestedZoom;applyZoom();}
1924
+ sendState();sendResponse(id,true,{zoom});return;
1824
1925
  }
1825
1926
 
1826
1927
  if (kind === 'set-rotation') {
@@ -24,11 +24,13 @@ const runtimeBodyStart = runtime.indexOf("\n(function () {", runtime.indexOf(com
24
24
  if (runtimeBodyStart < 0) {
25
25
  throw new Error("Main runtime body not found");
26
26
  }
27
- const runtimeBody = runtime.slice(runtimeBodyStart + 1).trim();
27
+ const readerSource=await readFile(resolve(runtimeDirectory,"epubReader.js"),"utf8");
28
+ const readerRuntime=replaceMarkedSource(runtime,"/* @papyrus-epub-reader:start */","/* @papyrus-epub-reader:end */",`\n${readerSource}\n`);
29
+ const runtimeBody = readerRuntime.slice(readerRuntime.indexOf("\n(function () {",readerRuntime.indexOf(comicRuntimeEnd))+1).trim();
28
30
  await writeFile(
29
31
  runtimePath,
30
32
  replaceMarkedSource(
31
- runtime,
33
+ readerRuntime,
32
34
  "/* @papyrus-comic-runtime:start */",
33
35
  "/* @papyrus-comic-runtime:end */",
34
36
  `\n${source.trim()}\n`
@@ -37,12 +39,13 @@ await writeFile(
37
39
 
38
40
  const htmlPath = resolve(runtimeDirectory, "index.html");
39
41
  const html = await readFile(htmlPath, "utf8");
40
- const htmlWithComicRuntime = replaceMarkedSource(
42
+ let htmlWithComicRuntime = replaceMarkedSource(
41
43
  html,
42
44
  "<!-- @papyrus-comic-runtime:start -->",
43
45
  "<!-- @papyrus-comic-runtime:end -->",
44
46
  `\n <script>\n${source}\n </script>\n`
45
47
  );
48
+ htmlWithComicRuntime=htmlWithComicRuntime.replace(/<!-- @papyrus-epub-reader:start -->[\s\S]*?<!-- @papyrus-epub-reader:end -->\n?/g,"");
46
49
  const htmlComicRuntimeEnd = "<!-- @papyrus-comic-runtime:end -->";
47
50
  const htmlBodyStart = htmlWithComicRuntime.indexOf(
48
51
  "<script>",