@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.
@@ -20483,6 +20483,127 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20483
20483
  <!-- @papyrus-comic-runtime:end -->
20484
20484
  <script>
20485
20485
  (function () {
20486
+ /* @papyrus-epub-annotations:start */
20487
+ (function (root, factory) {
20488
+ if (typeof module === 'object' && module.exports) module.exports = factory();
20489
+ else root.PapyrusEpubAnnotations = factory();
20490
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
20491
+ function validAnchor(anchor) {
20492
+ return anchor && anchor.version === 1 && anchor.kind === 'epub-cfi' &&
20493
+ typeof anchor.cfiRange === 'string' && anchor.cfiRange.length <= 4096 && /^epubcfi\([^\n<>]+\)$/.test(anchor.cfiRange) &&
20494
+ typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
20495
+ typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
20496
+ }
20497
+ function textNodes(document) {
20498
+ var walker = document.createTreeWalker(document.body, 4);
20499
+ var nodes = [], text = '', node;
20500
+ while ((node = walker.nextNode())) {
20501
+ if (node.parentElement.closest('script,style,[data-papyrus-overlay]')) continue;
20502
+ nodes.push({node:node, start:text.length}); text += node.textContent;
20503
+ }
20504
+ return {nodes:nodes,text:text};
20505
+ }
20506
+ function recoverRange(document, anchor) {
20507
+ var source = textNodes(document), match = null, start = source.text.indexOf(anchor.quote);
20508
+ while (start >= 0) {
20509
+ var end = start + anchor.quote.length;
20510
+ if ((!anchor.prefix || source.text.slice(Math.max(0,start-anchor.prefix.length),start) === anchor.prefix) &&
20511
+ (!anchor.suffix || source.text.slice(end,end+anchor.suffix.length) === anchor.suffix)) {
20512
+ if (match) return null; match = {start:start,end:end};
20513
+ }
20514
+ start = source.text.indexOf(anchor.quote,start+1);
20515
+ }
20516
+ if (!match) return null;
20517
+ var range = document.createRange();
20518
+ var first = source.nodes.find(function(n){return n.start+n.node.length > match.start;});
20519
+ var last = source.nodes.find(function(n){return n.start+n.node.length >= match.end;});
20520
+ if (!first || !last) return null;
20521
+ range.setStart(first.node,match.start-first.start); range.setEnd(last.node,match.end-last.start);
20522
+ return range;
20523
+ }
20524
+ function resolveRange(contents, anchor) {
20525
+ if (!validAnchor(anchor)) return null;
20526
+ try { var range = contents.range(anchor.cfiRange); if (range && range.toString() === anchor.quote) return range; } catch (_) {}
20527
+ return recoverRange(contents.document,anchor);
20528
+ }
20529
+ function selection(contents, cfiRange, href, documentId) {
20530
+ var range;
20531
+ try { range = contents.range(cfiRange); } catch (_) { return null; }
20532
+ if (!range || !range.toString()) return null;
20533
+ var before = contents.document.createRange(); before.selectNodeContents(contents.document.body); before.setEnd(range.startContainer,range.startOffset);
20534
+ var after = contents.document.createRange(); after.selectNodeContents(contents.document.body); after.setStart(range.endContainer,range.endOffset);
20535
+ 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)};
20536
+ return validAnchor(anchor) ? anchor : null;
20537
+ }
20538
+ function install(contents, options) {
20539
+ var doc = contents.document, win = contents.window, alive = true, frame = 0;
20540
+ var overlay = doc.createElement('div'); overlay.dataset.papyrusOverlay = 'annotations';
20541
+ overlay.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;z-index:3';
20542
+ var originalPosition=doc.body.style.position;
20543
+ if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
20544
+ doc.body.appendChild(overlay);
20545
+ var registry = new Map(), hitRegions = [];
20546
+ function render() {
20547
+ frame = 0; if (!alive || !options.isCurrent()) return;
20548
+ var keep = new Set(), noteGroups = new Map();
20549
+ hitRegions = [];
20550
+ options.annotations().forEach(function(annotation) {
20551
+ var anchor = annotation.anchor;
20552
+ if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
20553
+ var range = resolveRange(contents,anchor); if (!range) return;
20554
+ var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
20555
+ if (!rects.length) return;
20556
+ keep.add(annotation.id);
20557
+ var group = registry.get(annotation.id);
20558
+ 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'}); } }
20559
+ group.replaceChildren();
20560
+ hitRegions.push({id:annotation.id,rects:rects});
20561
+ var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
20562
+ var style = annotation.markupStyle || annotation.type;
20563
+ var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
20564
+ rects.forEach(function(rect) {
20565
+ var mark = doc.createElement('div');
20566
+ var bodyRect=doc.body.getBoundingClientRect();
20567
+ var height = style === 'highlight' ? rect.height : 2;
20568
+ var y = style === 'strikeout' ? rect.top+rect.height/2 : style === 'underline' ? rect.bottom-2 : rect.top;
20569
+ 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;
20570
+ if (['highlight','underline','strikeout'].includes(style)) group.appendChild(mark);
20571
+ });
20572
+ var hasNote = annotation.noteContent !== undefined || annotation.type === 'comment' || annotation.type === 'text';
20573
+ if (hasNote) {
20574
+ var last = rects[rects.length-1], button = doc.createElement('button');
20575
+ button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
20576
+ 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';
20577
+ var key = Math.floor(last.top/44);
20578
+ var neighbors=noteGroups.get(key);
20579
+ if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
20580
+ neighbors.ids.push(annotation.id);
20581
+ }
20582
+ });
20583
+ noteGroups.forEach(function(group){
20584
+ group.button.textContent=group.ids.length>1?String(group.ids.length):'●';
20585
+ group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
20586
+ });
20587
+ registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
20588
+ }
20589
+ function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
20590
+ function tapMarkup(event) {
20591
+ if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
20592
+ 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;});});
20593
+ if(hit) {event.stopPropagation();options.onTap(hit.id);}
20594
+ }
20595
+ doc.addEventListener('click',tapMarkup);
20596
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
20597
+ if (observer) observer.observe(doc.body);
20598
+ win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
20599
+ if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
20600
+ schedule();
20601
+ 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();}};
20602
+ }
20603
+ return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
20604
+ });
20605
+
20606
+ /* @papyrus-epub-annotations:end */
20486
20607
  const viewer = document.getElementById('viewer');
20487
20608
  const DEFAULT_FONT_SIZE = 16;
20488
20609
  const TEXT_PAGE_CHUNK = 1600;
@@ -20490,6 +20611,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20490
20611
  let currentType = null;
20491
20612
  let book = null;
20492
20613
  let rendition = null;
20614
+ let documentSessionId = null;
20615
+ let annotationDocumentId;
20616
+ let annotationLabels = {};
20617
+ let epubAnnotations = [];
20618
+ const epubAnnotationRenderers = new Map();
20493
20619
  let spineItems = [];
20494
20620
  let textPages = [];
20495
20621
  let textContainer = null;
@@ -20897,6 +21023,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20897
21023
  };
20898
21024
 
20899
21025
  const clearViewer = () => {
21026
+ epubAnnotationRenderers.forEach(renderer => renderer.destroy());
21027
+ epubAnnotationRenderers.clear();
20900
21028
  if (epubInteractionCleanup) {
20901
21029
  epubInteractionCleanup();
20902
21030
  epubInteractionCleanup = null;
@@ -21554,6 +21682,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21554
21682
  clearViewer();
21555
21683
  rendition = book.renderTo(viewer, {
21556
21684
  manager: 'continuous',
21685
+ allowScriptedContent: false,
21557
21686
  width: '100%',
21558
21687
  height: '100%',
21559
21688
  flow: 'scrolled-continuous',
@@ -21562,10 +21691,22 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21562
21691
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
21563
21692
 
21564
21693
  if (rendition && rendition.hooks && rendition.hooks.content) {
21694
+ const session = documentSessionId;
21695
+ const activeRendition = rendition;
21565
21696
  rendition.hooks.content.register((contents) => {
21697
+ const href = spineItems[contents.sectionIndex]?.href;
21698
+ if (href && window.PapyrusEpubAnnotations) {
21699
+ const renderer = window.PapyrusEpubAnnotations.install(contents, {
21700
+ href, get documentId() { return annotationDocumentId; },
21701
+ isCurrent: () => session === documentSessionId && activeRendition === rendition,
21702
+ annotations: () => epubAnnotations, labels: () => annotationLabels,
21703
+ onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
21704
+ });
21705
+ epubAnnotationRenderers.set(contents,renderer);
21706
+ }
21566
21707
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
21567
21708
  if (frame) {
21568
- frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
21709
+ frame.setAttribute('sandbox', 'allow-same-origin');
21569
21710
  }
21570
21711
  const selectionDocument = contents && contents.document;
21571
21712
  const selectionWindow = contents && contents.window;
@@ -21575,13 +21716,46 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21575
21716
  selectionWindow &&
21576
21717
  typeof selectionWindow.getSelection === 'function'
21577
21718
  ) {
21719
+ let selectionTimer = null;
21720
+ let lastSelectionKey = '';
21721
+ let lastSelectionAt = 0;
21722
+ const reportSelection = () => {
21723
+ selectionTimer = null;
21724
+ if (session !== documentSessionId || activeRendition !== rendition) return;
21725
+ const selected = selectionWindow.getSelection();
21726
+ if (!selected || selected.isCollapsed || selected.rangeCount === 0 ||
21727
+ typeof contents.cfiFromRange !== 'function') return;
21728
+ const range = selected.getRangeAt(0).cloneRange();
21729
+ if (!range.toString().trim()) return;
21730
+ const href = spineItems[contents.sectionIndex]?.href;
21731
+ if (!href) return;
21732
+ let cfi;
21733
+ try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
21734
+ const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
21735
+ if (!anchor) return;
21736
+ const key = anchor.cfiRange + ':' + anchor.quote;
21737
+ const now = Date.now();
21738
+ if (key === lastSelectionKey && now - lastSelectionAt < 750) return;
21739
+ lastSelectionKey = key;
21740
+ lastSelectionAt = now;
21741
+ const payload = {documentSessionId: session, text: anchor.quote,
21742
+ pageIndex: contents.sectionIndex + 1, anchor};
21743
+ nativeMenuSelection = {contents, payload};
21744
+ sendEvent('EPUB_TEXT_SELECTED', payload);
21745
+ };
21578
21746
  const handleSelectionChange = () => {
21579
21747
  const selection = selectionWindow.getSelection();
21580
21748
  const text = selection ? selection.toString().trim() : '';
21581
21749
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
21750
+ if (selectionTimer !== null) clearTimeout(selectionTimer);
21751
+ // epub.js "selected" is not reliable on every iOS WKWebView path.
21752
+ // The content document still emits selectionchange, so resolve its
21753
+ // live Range to a CFI before the native menu action can fire.
21754
+ if (text) reportSelection();
21582
21755
  };
21583
21756
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
21584
21757
  const selectionCleanup = () => {
21758
+ if (selectionTimer !== null) clearTimeout(selectionTimer);
21585
21759
  selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
21586
21760
  epubSelectionCleanups.delete(selectionCleanup);
21587
21761
  };
@@ -21602,22 +21776,30 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21602
21776
  rendition.hooks.unloaded.register((view) => {
21603
21777
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
21604
21778
  if (typeof cleanup === 'function') cleanup();
21779
+ const renderer = epubAnnotationRenderers.get(view?.contents);
21780
+ if (renderer) renderer.destroy();
21781
+ epubAnnotationRenderers.delete(view?.contents);
21605
21782
  });
21606
21783
  }
21607
21784
 
21608
21785
  if (rendition && typeof rendition.on === 'function') {
21786
+ const selectionSession = documentSessionId;
21787
+ const selectionRendition = rendition;
21609
21788
  rendition.on('selected', (cfiRange, contents) => {
21610
- const selection = contents && contents.window ? contents.window.getSelection() : null;
21611
- const text = selection ? selection.toString().trim() : '';
21612
- epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
21613
- if (text) {
21614
- sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
21615
- }
21616
- if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21617
- rendition.annotations.remove(cfiRange, 'highlight');
21618
- }
21789
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21790
+ const href = spineItems[contents.sectionIndex]?.href;
21791
+ const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
21792
+ if (!anchor) return;
21793
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
21794
+ const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
21795
+ nativeMenuSelection = {contents, payload};
21796
+ sendEvent('EPUB_TEXT_SELECTED', payload);
21619
21797
  });
21798
+ ['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
21799
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
21800
+ }));
21620
21801
  rendition.on('relocated', (location) => {
21802
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21621
21803
  const start = location && location.start;
21622
21804
  let index = start && Number.isInteger(start.index) ? start.index : -1;
21623
21805
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
@@ -21736,17 +21918,27 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21736
21918
  sendEvent('VIEWER_SCROLL', { offsetY });
21737
21919
  };
21738
21920
  const handleTouchStart = (event) => {
21739
- touchStart = getTouchPoint(event);
21921
+ touchStart = event?.touches?.length > 1 ? null : getTouchPoint(event);
21740
21922
  epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
21741
21923
  };
21742
21924
  const handleTouchMove = (event) => {
21925
+ if (event?.touches?.length > 1) touchStart = null;
21743
21926
  epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
21744
21927
  };
21745
21928
  const handleTouchEnd = (event) => {
21746
21929
  const end = getTouchPoint(event);
21747
21930
  const start = touchStart;
21748
21931
  touchStart = null;
21749
- if (start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
21932
+ const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
21933
+ 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) {
21934
+ const active = rendition;
21935
+ const interactionSession = documentSessionId;
21936
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
21937
+ if (active !== rendition || interactionSession !== documentSessionId) return;
21938
+ return end.x < start.x ? active.next() : active.prev();
21939
+ });
21940
+ epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR', {message:String(error)}));
21941
+ } else if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
21750
21942
  sendEvent('VIEWER_TAP', {});
21751
21943
  }
21752
21944
  epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
@@ -21791,6 +21983,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21791
21983
  return { pageCount, outline };
21792
21984
  };
21793
21985
 
21986
+ let epubNavigationQueue = Promise.resolve();
21987
+ let epubNavigationRequest = 0;
21794
21988
  const displayEpubPage = async (pageIndex) => {
21795
21989
  if (!rendition) return;
21796
21990
  const item = spineItems[pageIndex];
@@ -21800,9 +21994,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21800
21994
  target,
21801
21995
  pageIndex,
21802
21996
  });
21803
- await rendition.display(target);
21804
- currentPage = pageIndex + 1;
21805
- sendState();
21997
+ const activeRendition = rendition;
21998
+ const session = documentSessionId;
21999
+ const request = ++epubNavigationRequest;
22000
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22001
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22002
+ await activeRendition.display(target);
22003
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22004
+ currentPage = pageIndex + 1;
22005
+ sendState();
22006
+ });
22007
+ epubNavigationQueue = navigation;
22008
+ await navigation;
21806
22009
  };
21807
22010
 
21808
22011
  const getTextContent = async (pageIndex) => {
@@ -21903,6 +22106,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21903
22106
  }
21904
22107
  };
21905
22108
 
22109
+ let nativeMenuSelection = null;
22110
+ let epubViewMode = "continuous";
22111
+
21906
22112
  const handleCommand = async (message) => {
21907
22113
  const { id, kind, payload } = message;
21908
22114
 
@@ -21910,7 +22116,13 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21910
22116
  if (kind === 'load') {
21911
22117
  const generation = (epubLoadGeneration += 1);
21912
22118
  currentType = payload.type;
22119
+ documentSessionId = payload.documentSessionId || null;
22120
+ epubAnnotations = [];
22121
+ annotationDocumentId = undefined;
21913
22122
  zoom = 1.0;
22123
+ epubViewMode = "continuous";
22124
+ epubNavigationRequest += 1;
22125
+ epubNavigationQueue = Promise.resolve();
21914
22126
 
21915
22127
  if (currentType === 'text') {
21916
22128
  const result = await loadText(
@@ -21950,6 +22162,42 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21950
22162
  throw new Error('Unsupported document type');
21951
22163
  }
21952
22164
 
22165
+ if (kind === 'epub-annotations' || kind === 'epub-annotation-navigate') {
22166
+ if (currentType !== 'epub' || !documentSessionId || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB annotation session');
22167
+ if (kind === 'epub-annotations') {
22168
+ const incoming = payload.annotations;
22169
+ 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');
22170
+ const ids = new Set();
22171
+ epubAnnotations = incoming.filter(a => {
22172
+ if (ids.has(a.id) || !window.PapyrusEpubAnnotations.validAnchor(a.anchor)) return false;
22173
+ ids.add(a.id); return true;
22174
+ });
22175
+ annotationDocumentId = typeof payload.documentId === 'string' ? payload.documentId : undefined;
22176
+ annotationLabels = {comment: typeof payload.labels?.comment === 'string' ? payload.labels.comment.slice(0,128) : ''};
22177
+ epubAnnotationRenderers.forEach(renderer => renderer.schedule());
22178
+ } else {
22179
+ const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
22180
+ if (!annotation || (annotation.anchor.documentId && annotation.anchor.documentId !== annotationDocumentId)) throw new Error('Annotation not available for this document');
22181
+ // Navigation uses the CFI, never a synthetic percentage/page rectangle.
22182
+ const navigationSession = documentSessionId;
22183
+ const activeRendition = rendition;
22184
+ // Open the confirmed chapter first, resolve its quote, then navigate its actual range.
22185
+ epubNavigationRequest += 1;
22186
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22187
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22188
+ await activeRendition.display(annotation.anchor.href);
22189
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22190
+ const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
22191
+ const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
22192
+ if(!range)throw new Error('EPUB anchor unresolved');
22193
+ await activeRendition.display(contents.cfiFromRange(range));
22194
+ });
22195
+ epubNavigationQueue = navigation;
22196
+ await navigation;
22197
+ }
22198
+ sendResponse(id,true,null); return;
22199
+ }
22200
+
21953
22201
  if (kind === 'go-to-page') {
21954
22202
  const page = Math.max(1, payload.page || 1);
21955
22203
  if (currentType === 'text') {
@@ -21972,6 +22220,33 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21972
22220
  return;
21973
22221
  }
21974
22222
 
22223
+ if (kind === 'epub-turn-page') {
22224
+ if (epubViewMode !== 'single') {sendResponse(id,true,null); return;}
22225
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB navigation');
22226
+ const active = rendition;
22227
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
22228
+ if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
22229
+ return payload.direction < 0 ? active.prev() : active.next();
22230
+ });
22231
+ await epubNavigationQueue;
22232
+ sendResponse(id,true,null); return;
22233
+ }
22234
+
22235
+ if (kind === 'set-epub-view-mode') {
22236
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
22237
+ epubViewMode = payload.viewMode === 'single' ? 'single' : 'continuous';
22238
+ const active = rendition;
22239
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
22240
+ if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
22241
+ const flow = epubViewMode === 'single' ? 'paginated' : 'scrolled-continuous';
22242
+ if (active.settings.flow === flow) return;
22243
+ active.flow(flow);
22244
+ return active.q.enqueue(() => Promise.resolve());
22245
+ });
22246
+ await epubNavigationQueue;
22247
+ sendResponse(id,true,null); return;
22248
+ }
22249
+
21975
22250
  if (kind === 'set-zoom') {
21976
22251
  zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
21977
22252
  applyZoom();
@@ -22119,6 +22394,26 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22119
22394
  return;
22120
22395
  }
22121
22396
 
22397
+ if (message.type === 'epub-selection-action') {
22398
+ const selected = nativeMenuSelection;
22399
+ if (!selected || message.documentSessionId !== documentSessionId ||
22400
+ selected.payload.documentSessionId !== documentSessionId) return;
22401
+ if (message.key === 'selectAll') {
22402
+ const doc = selected.contents.document;
22403
+ const range = doc.createRange();
22404
+ range.selectNodeContents(doc.body);
22405
+ const selection = selected.contents.window.getSelection();
22406
+ selection.removeAllRanges();
22407
+ selection.addRange(range);
22408
+ return;
22409
+ }
22410
+ if (!['copy','define','highlight','underline','strikeout','comment'].includes(message.key)) return;
22411
+ sendEvent('EPUB_SELECTION_ACTION', {...selected.payload, action: message.key});
22412
+ selected.contents.window.getSelection()?.removeAllRanges();
22413
+ nativeMenuSelection = null;
22414
+ return;
22415
+ }
22416
+
22122
22417
  if (!message.kind) return;
22123
22418
  handleCommand(message);
22124
22419
  };