@papyrus-sdk/ui-react-native 0.2.35-beta.9 → 0.2.36

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,67 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20483
20483
  <!-- @papyrus-comic-runtime:end -->
20484
20484
  <script>
20485
20485
  (function () {
20486
+ /* @papyrus-epub-reader:start */
20487
+ (function(root){
20488
+ function mode(value){return value==='chapter-scroll'?'chapter-scroll':'paged';}
20489
+ function validCfi(value){return typeof value==='string' && value.length<=4096 && /^epubcfi\([^\n<>]+\)$/.test(value);}
20490
+ function options(value){return {manager:'default',flow:mode(value)==='paged'?'paginated':'scrolled-doc',spread:'none',width:'100%',height:'100%',allowScriptedContent:false};}
20491
+ function gesture(start,end,state){
20492
+ if(!start || !end || state.selected || state.interactive || state.multitouch)return null;
20493
+ var dx=end.x-start.x,dy=end.y-start.y,elapsed=end.time-start.time;
20494
+ 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';
20495
+ if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
20496
+ return null;
20497
+ }
20498
+ function location(raw,count,readingMode,geometry){
20499
+ var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
20500
+ var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
20501
+ var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
20502
+ var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
20503
+ var atStart=Boolean(raw.atStart),atEnd=Boolean(raw.atEnd);
20504
+ // Chromium may round a column offset down by a fraction of a CSS pixel.
20505
+ // Keep epub.js authoritative outside a 1px boundary, and for RTL layouts.
20506
+ if(visualPage!==null && total>0 && geometry && geometry.direction==='ltr' && geometry.pageWidth>0 && Number.isFinite(geometry.offset)){
20507
+ var boundary=Math.round(geometry.offset/geometry.pageWidth);
20508
+ if(Math.abs(geometry.offset-boundary*geometry.pageWidth)<=1){
20509
+ visualPage=Math.min(total,Math.max(1,boundary+1));
20510
+ atStart=chapter===(geometry.firstIndex ?? 0)+1 && visualPage===1;
20511
+ atEnd=chapter===(geometry.lastIndex ?? count-1)+1 && visualPage===total;
20512
+ }
20513
+ }
20514
+ var fraction=total?Math.max(0,(visualPage-1)/total):0;
20515
+ 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:atStart,atEnd:atEnd};
20516
+ }
20517
+ function installResizeAnchor(manager,readCfi){
20518
+ var original=manager.resize,anchor=null;
20519
+ function resize(width,height,target){
20520
+ if(validCfi(target))anchor=target;
20521
+ if(!anchor){var current=readCfi();if(validCfi(current))anchor=current;}
20522
+ // epub.js accepts a logical target as its third argument. Keep it across
20523
+ // keyboard/dialog reflows instead of adopting each new viewport start.
20524
+ return original.call(this,width,height,anchor || target);
20525
+ }
20526
+ manager.resize=resize;
20527
+ return {cfi:function(){return anchor;},invalidate:function(){anchor=null;},destroy:function(){if(manager.resize===resize)manager.resize=original;anchor=null;}};
20528
+ }
20529
+ function install(contents,config){
20530
+ var doc=contents.document,win=contents.window,start=null,blocked=false;
20531
+ 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;}
20532
+ function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
20533
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay],[data-papyrus-ink-capture]'));}
20534
+ function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
20535
+ function invalidateScroll(finish){if(config.current() && start && finish && config.mode()==='chapter-scroll' && !blocked && !selected() && Math.abs(finish.y-start.y)>12 && Math.abs(finish.y-start.y)>Math.abs(finish.x-start.x) && config.interact)config.interact();}
20536
+ function move(e){if(e.touches.length!==1)blocked=true;invalidateScroll(point(e));}
20537
+ function end(e){var finish=point(e);invalidateScroll(finish);var action=gesture(start,finish,{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);}
20538
+ function cancel(){start=null;blocked=true;}
20539
+ function link(e){if(config.current() && e.target?.closest?.('a') && config.interact)config.interact();}
20540
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});doc.addEventListener('click',link);
20541
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);doc.removeEventListener('click',link);};
20542
+ }
20543
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,installResizeAnchor:installResizeAnchor,install:install};
20544
+ })(typeof window==='undefined'?globalThis:window);
20545
+
20546
+ /* @papyrus-epub-reader:end */
20486
20547
  /* @papyrus-epub-annotations:start */
20487
20548
  (function (root, factory) {
20488
20549
  if (typeof module === 'object' && module.exports) module.exports = factory();
@@ -20494,6 +20555,84 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20494
20555
  typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
20495
20556
  typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
20496
20557
  }
20558
+ function simplifyPath(points, tolerance, maxPoints) {
20559
+ if (!Array.isArray(points) || points.length <= 2) return Array.isArray(points) ? points.slice() : [];
20560
+ tolerance = Number.isFinite(tolerance) ? Math.max(0, tolerance) : 1.5;
20561
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 512;
20562
+ var keep = new Uint8Array(points.length); keep[0] = 1; keep[points.length - 1] = 1;
20563
+ var stack = [[0, points.length - 1]], toleranceSquared = tolerance * tolerance;
20564
+ while (stack.length) {
20565
+ var interval = stack.pop(), first = interval[0], last = interval[1], a = points[first], b = points[last];
20566
+ var dx = b.x - a.x, dy = b.y - a.y, lengthSquared = dx * dx + dy * dy, maxDistance = toleranceSquared, split = -1;
20567
+ for (var i = first + 1; i < last; i++) {
20568
+ var p = points[i], ratio = lengthSquared ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSquared)) : 0;
20569
+ var px = a.x + ratio * dx, py = a.y + ratio * dy, ox = p.x - px, oy = p.y - py, distance = ox * ox + oy * oy;
20570
+ if (distance > maxDistance) { maxDistance = distance; split = i; }
20571
+ }
20572
+ if (split >= 0) { keep[split] = 1; stack.push([first, split], [split, last]); }
20573
+ }
20574
+ var result = []; for (var j = 0; j < points.length; j++) if (keep[j]) result.push(points[j]);
20575
+ if (result.length <= maxPoints) return result;
20576
+ var capped = []; for (var k = 0; k < maxPoints; k++) capped.push(result[Math.round(k * (result.length - 1) / (maxPoints - 1))]);
20577
+ return capped;
20578
+ }
20579
+ function compactGesturePoints(points, maxPoints) {
20580
+ if (!Array.isArray(points)) return [];
20581
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 1024;
20582
+ if (points.length <= maxPoints) return points.slice();
20583
+ var compacted = [];
20584
+ for (var i = 0; i < maxPoints; i++) compacted.push(points[Math.round(i * (points.length - 1) / (maxPoints - 1))]);
20585
+ return compacted;
20586
+ }
20587
+ function densifyPath(points, maxStep, maxPoints) {
20588
+ if (!Array.isArray(points) || !points.length) return [];
20589
+ if (points.length === 1) return points.slice();
20590
+ maxStep = Number.isFinite(maxStep) ? Math.max(1, maxStep) : 12;
20591
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 512;
20592
+ var cumulative = [0], total = 0;
20593
+ for (var i = 1; i < points.length; i++) {
20594
+ if (![points[i-1].x,points[i-1].y,points[i].x,points[i].y].every(Number.isFinite)) return [];
20595
+ total += Math.hypot(points[i].x-points[i-1].x, points[i].y-points[i-1].y);
20596
+ cumulative.push(total);
20597
+ }
20598
+ if (!total) return [points[0], points[points.length-1]];
20599
+ var intervals = Math.min(maxPoints-1, Math.max(1, Math.ceil(total/maxStep))), result = [], segment = 1;
20600
+ for (var sample = 0; sample <= intervals; sample++) {
20601
+ var distance = total*sample/intervals;
20602
+ while (segment < cumulative.length-1 && cumulative[segment] < distance) segment++;
20603
+ var start = points[segment-1], end = points[segment], span = cumulative[segment]-cumulative[segment-1];
20604
+ var ratio = span ? (distance-cumulative[segment-1])/span : 0;
20605
+ result.push({x:start.x+(end.x-start.x)*ratio,y:start.y+(end.y-start.y)*ratio});
20606
+ }
20607
+ return result;
20608
+ }
20609
+ function createSpatialIndex(entries, cellSize) {
20610
+ cellSize = Number.isFinite(cellSize) ? Math.max(8, cellSize) : 64;
20611
+ var cells = new Map();
20612
+ entries.forEach(function(entry) {
20613
+ var left = Math.floor(entry.left / cellSize), right = Math.floor(entry.right / cellSize);
20614
+ var top = Math.floor(entry.top / cellSize), bottom = Math.floor(entry.bottom / cellSize);
20615
+ for (var x = left; x <= right; x++) for (var y = top; y <= bottom; y++) {
20616
+ var key = x + ':' + y, bucket = cells.get(key); if (!bucket) { bucket = []; cells.set(key, bucket); } bucket.push(entry);
20617
+ }
20618
+ });
20619
+ return {cells:cells,cellSize:cellSize};
20620
+ }
20621
+ function queryNearestTextRect(index, x, y, maxDistance) {
20622
+ if (!index || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(maxDistance) || maxDistance < 0) return null;
20623
+ var size = index.cellSize, minX = Math.floor((x-maxDistance)/size), maxX = Math.floor((x+maxDistance)/size);
20624
+ var minY = Math.floor((y-maxDistance)/size), maxY = Math.floor((y+maxDistance)/size), seen = new Set(), best = null;
20625
+ for (var cx=minX; cx<=maxX; cx++) for (var cy=minY; cy<=maxY; cy++) {
20626
+ var bucket = index.cells.get(cx+':'+cy); if (!bucket) continue;
20627
+ bucket.forEach(function(entry) {
20628
+ if (seen.has(entry)) return; seen.add(entry);
20629
+ var nearestX = Math.max(entry.left,Math.min(entry.right,x)), nearestY = Math.max(entry.top,Math.min(entry.bottom,y));
20630
+ var dx=x-nearestX,dy=y-nearestY,distance=Math.sqrt(dx*dx+dy*dy);
20631
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={entry:entry,distance:distance,x:nearestX,y:nearestY};
20632
+ });
20633
+ }
20634
+ return best;
20635
+ }
20497
20636
  function textNodes(document) {
20498
20637
  var walker = document.createTreeWalker(document.body, 4);
20499
20638
  var nodes = [], text = '', node;
@@ -20543,13 +20682,196 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20543
20682
  if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
20544
20683
  doc.body.appendChild(overlay);
20545
20684
  var registry = new Map(), hitRegions = [];
20685
+ var inkRegistry = new Map(), rangeCache = new Map(), spatialIndex = null, activeGesture = null, inkConfig = {enabled:false,preset:'ink'};
20686
+ var inkSvg = doc.createElementNS('http://www.w3.org/2000/svg','svg');
20687
+ inkSvg.setAttribute('data-papyrus-ink-overlay','');
20688
+ inkSvg.style.cssText = 'position:absolute;left:0;top:0;overflow:visible;pointer-events:none;z-index:1';
20689
+ overlay.appendChild(inkSvg);
20690
+ var inkCanvas = doc.createElement('canvas');
20691
+ inkCanvas.dataset.papyrusInkCapture = '';
20692
+ inkCanvas.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;z-index:2147483000;background:transparent;pointer-events:none;touch-action:none';
20693
+ doc.body.appendChild(inkCanvas);
20694
+ var inkContext = null; try { if(typeof win.CanvasRenderingContext2D==='function')inkContext = inkCanvas.getContext('2d'); } catch (_) {}
20695
+ function resizeCanvas() {
20696
+ var bodyRect=doc.body.getBoundingClientRect(), ratio = Math.max(1,Math.min(3,win.devicePixelRatio || 1));
20697
+ var width=Math.max(1,Math.min(4096,doc.body.clientWidth||bodyRect.width||Math.min(4096,win.innerWidth||1)));
20698
+ var height=Math.max(1,Math.min(4096,doc.body.clientHeight||bodyRect.height||Math.min(4096,win.innerHeight||1)));
20699
+ inkCanvas.style.width=width+'px';inkCanvas.style.height=height+'px';
20700
+ inkCanvas.width = Math.round(width*ratio); inkCanvas.height = Math.round(height*ratio);
20701
+ if (inkContext) { inkContext.setTransform(ratio,0,0,ratio,0,0); inkContext.clearRect(0,0,width,height); }
20702
+ }
20703
+ function collectTextRects() {
20704
+ var walker = doc.createTreeWalker(doc.body,4), node, entries=[];
20705
+ while ((node=walker.nextNode())) {
20706
+ var parent=node.parentElement;
20707
+ if (!parent || parent.closest('script,style,noscript,textarea,[data-papyrus-overlay],[data-papyrus-ink-capture]')) continue;
20708
+ var text=node.nodeValue || '', style=win.getComputedStyle(parent), computed=parseFloat(style.lineHeight);
20709
+ for (var start=0; start<text.length;) {
20710
+ var end=Math.min(text.length,start+24), last=text.charCodeAt(end-1), next=text.charCodeAt(end);
20711
+ if(end<text.length&&last>=0xD800&&last<=0xDBFF&&next>=0xDC00&&next<=0xDFFF)end++;
20712
+ var range=doc.createRange(); range.setStart(node,start); range.setEnd(node,end);
20713
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
20714
+ rects.forEach(function(rect) {
20715
+ if (!(rect.width>0 && rect.height>0)) return;
20716
+ entries.push({left:rect.left+win.scrollX,top:rect.top+win.scrollY,right:rect.right+win.scrollX,bottom:rect.bottom+win.scrollY,width:rect.width,height:rect.height,
20717
+ node:node,start:start,end:end,lineHeight:Number.isFinite(computed)&&computed>0?computed:rect.height});
20718
+ });
20719
+ start=end;
20720
+ }
20721
+ }
20722
+ return entries;
20723
+ }
20724
+ function rebuildSpatialIndex() { spatialIndex=createSpatialIndex(collectTextRects(),64); rangeCache.clear(); }
20725
+ function inkViewportExtent() {
20726
+ var root=doc.documentElement,body=doc.body,bodyRect=body.getBoundingClientRect();
20727
+ var width=root.clientWidth||body.clientWidth||bodyRect.width||win.innerWidth||1;
20728
+ var height=root.clientHeight||body.clientHeight||bodyRect.height||win.innerHeight||1;
20729
+ return Math.max(1,Math.min(4096,width),Math.min(4096,height));
20730
+ }
20731
+ function inkStrokeWidthPx(width,preset) {
20732
+ var scale=preset==='highlight'?0.8:preset==='underline'?0.1:0.15;
20733
+ return Math.max(1,Math.min(24,(Number.isFinite(width)?width:0.006)*inkViewportExtent()*scale));
20734
+ }
20735
+ function closestGlyph(entry,x,y,maxDistance) {
20736
+ var source=(entry.node.nodeValue || '').slice(entry.start,entry.end), best=null, offset=0;
20737
+ for (var glyph of source) {
20738
+ var range=doc.createRange(); range.setStart(entry.node,entry.start+offset); range.setEnd(entry.node,entry.start+offset+glyph.length); offset+=glyph.length;
20739
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
20740
+ rects.forEach(function(rect) {
20741
+ if (!(rect.width>0 && rect.height>0)) return;
20742
+ var nx=Math.max(rect.left+win.scrollX,Math.min(rect.right+win.scrollX,x)), ny=Math.max(rect.top+win.scrollY,Math.min(rect.bottom+win.scrollY,y));
20743
+ var dx=x-nx,dy=y-ny,distance=Math.sqrt(dx*dx+dy*dy);
20744
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={range:range,rect:rect,distance:distance};
20745
+ });
20746
+ }
20747
+ if (!best || typeof contents.cfiFromRange!=='function') return null;
20748
+ var quote=best.range.toString(), cfiRange; try { cfiRange=contents.cfiFromRange(best.range); } catch (_) { return null; }
20749
+ if (!quote || typeof cfiRange!=='string' || !/^epubcfi\([^\n<>]+\)$/.test(cfiRange)) return null;
20750
+ var lineHeight=Math.max(1,entry.lineHeight || best.rect.height), px=x-(best.rect.left+win.scrollX), py=y-(best.rect.top+win.scrollY);
20751
+ return {cfiRange:cfiRange,quote:quote,x:Math.max(-3,Math.min(3,px/lineHeight)),y:Math.max(-3,Math.min(3,py/lineHeight))};
20752
+ }
20753
+ function renderInkAnnotation(annotation,keep,bodyRect) {
20754
+ var baseRect=bodyRect || doc.body.getBoundingClientRect() || {left:0,top:0};
20755
+ var data=annotation.epubInk;
20756
+ if (!data || data.version!==1 || data.kind!=='epub-ink' || !Array.isArray(data.segments)) return;
20757
+ keep.add(annotation.id);
20758
+ var group=inkRegistry.get(annotation.id);
20759
+ if (!group) { group=doc.createElementNS('http://www.w3.org/2000/svg','g'); group.setAttribute('data-epub-ink-id',annotation.id); inkSvg.appendChild(group); inkRegistry.set(annotation.id,group); }
20760
+ group.replaceChildren();
20761
+ var color=/^#[0-9a-f]{6}$/i.test(annotation.color)?annotation.color:'#2563eb';
20762
+ var opacity=Number.isFinite(annotation.opacity)?Math.max(0,Math.min(1,annotation.opacity)):1;
20763
+ var preset=['ink','highlight','underline'].indexOf(data.preset)>=0?data.preset:'ink';
20764
+ var stroke=inkStrokeWidthPx(annotation.strokeWidth,preset);
20765
+ data.segments.forEach(function(segment) {
20766
+ if (segment.href!==options.href || !Array.isArray(segment.points)) return;
20767
+ var paths=[], current=[];
20768
+ segment.points.forEach(function(point) {
20769
+ var range=rangeCache.get(point.cfiRange);
20770
+ if (!range) { try { range=contents.range(point.cfiRange); } catch (_) { range=null; } if (range) rangeCache.set(point.cfiRange,range); }
20771
+ if (!range || range.toString()!==point.quote) { if(current.length>1)paths.push(current);current=[];return; }
20772
+ var rect; try { rect=range.getBoundingClientRect(); } catch (_) { rect=null; }
20773
+ if (!rect || !(rect.width>0 && rect.height>0)) { if(current.length>1)paths.push(current);current=[];return; }
20774
+ var lineHeight=Math.max(1,rect.height), x=rect.left-baseRect.left+point.x*lineHeight, y=rect.top-baseRect.top+point.y*lineHeight;
20775
+ var previous=current[current.length-1], jumpLimit=Math.max(24,lineHeight*2.5);
20776
+ if (previous && Math.hypot(x-previous.x,y-previous.y)>jumpLimit) { if(current.length>1)paths.push(current);current=[]; }
20777
+ current.push({x:x,y:y,lineHeight:lineHeight});
20778
+ });
20779
+ if(current.length>1)paths.push(current);
20780
+ paths.forEach(function(points) {
20781
+ var d='M '+points[0].x.toFixed(1)+' '+points[0].y.toFixed(1);
20782
+ for(var i=1;i<points.length;i++){var prev=points[i-1],point=points[i],mx=(prev.x+point.x)/2,my=(prev.y+point.y)/2;d+=' Q '+prev.x.toFixed(1)+' '+prev.y.toFixed(1)+' '+mx.toFixed(1)+' '+my.toFixed(1);}
20783
+ var last=points[points.length-1];d+=' L '+last.x.toFixed(1)+' '+last.y.toFixed(1);
20784
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('d',d);path.setAttribute('fill','none');path.setAttribute('stroke',color);path.setAttribute('stroke-width',String(stroke));path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(opacity));group.appendChild(path);
20785
+ });
20786
+ });
20787
+ }
20788
+ function clearPreview() {
20789
+ if(inkContext)inkContext.clearRect(0,0,inkCanvas.width,inkCanvas.height);
20790
+ inkSvg.querySelectorAll('[data-papyrus-ink-preview]').forEach(function(path){path.remove();});
20791
+ }
20792
+ function createPreviewPath(x,y) {
20793
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');
20794
+ path.setAttribute('data-papyrus-ink-preview','');path.setAttribute('d','M '+x.toFixed(1)+' '+y.toFixed(1));path.setAttribute('fill','none');
20795
+ path.setAttribute('stroke',inkConfig.color);path.setAttribute('stroke-width',String(inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset)));
20796
+ path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(inkConfig.opacity));
20797
+ path.style.pointerEvents='none';inkSvg.appendChild(path);return path;
20798
+ }
20799
+ function updatePreviewPath(clientX,clientY) {
20800
+ var bodyRect=doc.body.getBoundingClientRect(),x=clientX-bodyRect.left,y=clientY-bodyRect.top;
20801
+ var gesture=activeGesture;
20802
+ if(!gesture.previewPath||gesture.previewPathPoints>=32){
20803
+ gesture.previewPath=createPreviewPath(gesture.previewLast?gesture.previewLast.x:x,gesture.previewLast?gesture.previewLast.y:y);
20804
+ gesture.previewPathPoints=1;
20805
+ }
20806
+ var pathData=gesture.previewPath.getAttribute('d')||'';
20807
+ gesture.previewPath.setAttribute('d',pathData+' L '+x.toFixed(1)+' '+y.toFixed(1));
20808
+ gesture.previewPathPoints++;
20809
+ gesture.previewLast={x:x,y:y};
20810
+ }
20811
+ function cancelGesture() {
20812
+ var gesture=activeGesture; activeGesture=null; clearPreview();
20813
+ if(gesture)try{inkCanvas.releasePointerCapture(gesture.pointerId);}catch(_){}
20814
+ }
20815
+ function commitGesture() {
20816
+ var gesture=activeGesture; activeGesture=null; clearPreview();
20817
+ if(!gesture || !alive || !options.isCurrent() || !inkConfig.enabled || !spatialIndex) return;
20818
+ var simplified=simplifyPath(gesture.points,1.5,512), anchorSamples=densifyPath(simplified,12,512), segments=[], current=[];
20819
+ anchorSamples.forEach(function(point) {
20820
+ var coarse=queryNearestTextRect(spatialIndex,point.x,point.y,28);
20821
+ var cutoff=coarse?Math.max(12,Math.min(28,0.8*coarse.entry.lineHeight)):12;
20822
+ var anchored=coarse && closestGlyph(coarse.entry,point.x,point.y,cutoff);
20823
+ if(!anchored){if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});current=[];return;}
20824
+ current.push(anchored);
20825
+ });
20826
+ if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});
20827
+ segments=segments.slice(0,64);
20828
+ var pointCount=segments.reduce(function(total,segment){return total+segment.points.length;},0);
20829
+ if(!pointCount)return;
20830
+ var width=Math.max(1,doc.documentElement.scrollWidth,win.innerWidth),height=Math.max(1,doc.documentElement.scrollHeight,win.innerHeight);
20831
+ var normalized=simplified.map(function(p){return {x:Math.max(0,Math.min(1,p.x/width)),y:Math.max(0,Math.min(1,p.y/height))};});
20832
+ var minX=Math.min.apply(null,normalized.map(function(p){return p.x;})),maxX=Math.max.apply(null,normalized.map(function(p){return p.x;}));
20833
+ var minY=Math.min.apply(null,normalized.map(function(p){return p.y;})),maxY=Math.max.apply(null,normalized.map(function(p){return p.y;}));
20834
+ var first=segments[0].points[0], href=segments[0].href, documentId=typeof options.documentId==='function'?options.documentId():options.documentId;
20835
+ var annotation={id:'epub-ink-'+Date.now()+'-'+Math.random().toString(36).slice(2,9),type:'ink',pageIndex:(options.spineIndex||0)+1,
20836
+ rect:{x:minX,y:minY,width:Math.min(1-minX,Math.max(0.001,maxX-minX)),height:Math.min(1-minY,Math.max(0.001,maxY-minY))},path:normalized,color:inkConfig.color,opacity:inkConfig.opacity,strokeWidth:inkConfig.strokeWidth,createdAt:Date.now(),content:first.quote,
20837
+ anchor:{version:1,kind:'epub-cfi',cfiRange:first.cfiRange,href:href,spineIndex:options.spineIndex,documentId:documentId,quote:first.quote},
20838
+ epubInk:{version:1,kind:'epub-ink',documentId:documentId,preset:inkConfig.preset,segments:segments}};
20839
+ if(typeof options.onInkCommit==='function')options.onInkCommit(annotation);
20840
+ }
20841
+ function pointerDown(event) {
20842
+ if(!inkConfig.enabled || !alive || !options.isCurrent() || event.button!==undefined && event.button!==0)return;
20843
+ if(activeGesture){cancelGesture();return;}
20844
+ event.preventDefault();event.stopPropagation();
20845
+ activeGesture={pointerId:event.pointerId,points:[{x:event.clientX+win.scrollX,y:event.clientY+win.scrollY}],last:{x:event.clientX,y:event.clientY},previewPath:null,previewPathPoints:0,previewLast:null};
20846
+ updatePreviewPath(event.clientX,event.clientY);
20847
+ try{inkCanvas.setPointerCapture(event.pointerId);}catch(_){}
20848
+ }
20849
+ function pointerMove(event) {
20850
+ if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;
20851
+ if(!options.isCurrent()||!inkConfig.enabled){cancelGesture();return;}
20852
+ event.preventDefault();event.stopPropagation();
20853
+ var point={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY};activeGesture.points.push(point);
20854
+ if(activeGesture.points.length>2048)activeGesture.points=compactGesturePoints(activeGesture.points,1024);
20855
+ if(inkContext){inkContext.beginPath();inkContext.moveTo(activeGesture.last.x,activeGesture.last.y);inkContext.lineTo(event.clientX,event.clientY);inkContext.strokeStyle=inkConfig.color;inkContext.globalAlpha=inkConfig.opacity;inkContext.lineWidth=inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset);inkContext.lineCap='round';inkContext.lineJoin='round';inkContext.stroke();}
20856
+ updatePreviewPath(event.clientX,event.clientY);
20857
+ activeGesture.last={x:event.clientX,y:event.clientY};
20858
+ }
20859
+ function pointerUp(event) {if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;event.preventDefault();event.stopPropagation();var end={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY},last=activeGesture.points[activeGesture.points.length-1];if(Math.hypot(end.x-last.x,end.y-last.y)>0.5){activeGesture.points.push(end);updatePreviewPath(event.clientX,event.clientY);}commitGesture();}
20860
+ function pointerCancel(event) {if(activeGesture && (event.pointerId===undefined || activeGesture.pointerId===event.pointerId))cancelGesture();}
20861
+ resizeCanvas();rebuildSpatialIndex();
20862
+ inkCanvas.addEventListener('pointerdown',pointerDown);inkCanvas.addEventListener('pointermove',pointerMove);inkCanvas.addEventListener('pointerup',pointerUp);inkCanvas.addEventListener('pointercancel',pointerCancel);inkCanvas.addEventListener('lostpointercapture',pointerCancel);
20863
+ win.addEventListener('blur',pointerCancel);win.addEventListener('resize',resizeCanvas);
20546
20864
  function render() {
20547
20865
  frame = 0; if (!alive || !options.isCurrent()) return;
20548
- var keep = new Set(), noteGroups = new Map();
20866
+ var keep = new Set(), keepInk = new Set(), noteGroups = new Map();
20549
20867
  hitRegions = [];
20868
+ var bodyRect=doc.body.getBoundingClientRect() || {left:0,top:0};
20869
+ inkSvg.setAttribute('width',String(Math.max(doc.documentElement.scrollWidth,win.innerWidth)));
20870
+ inkSvg.setAttribute('height',String(Math.max(doc.documentElement.scrollHeight,win.innerHeight)));
20550
20871
  options.annotations().forEach(function(annotation) {
20551
20872
  var anchor = annotation.anchor;
20552
20873
  if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
20874
+ if(annotation.type==='ink' && annotation.epubInk){renderInkAnnotation(annotation,keepInk,bodyRect);return;}
20553
20875
  var range = resolveRange(contents,anchor); if (!range) return;
20554
20876
  var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
20555
20877
  if (!rects.length) return;
@@ -20561,6 +20883,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20561
20883
  var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
20562
20884
  var style = annotation.markupStyle || annotation.type;
20563
20885
  var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
20886
+ if(style === 'highlight')opacity=Math.min(0.35,opacity);
20564
20887
  rects.forEach(function(rect) {
20565
20888
  var mark = doc.createElement('div');
20566
20889
  var bodyRect=doc.body.getBoundingClientRect();
@@ -20573,8 +20896,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20573
20896
  if (hasNote) {
20574
20897
  var last = rects[rects.length-1], button = doc.createElement('button');
20575
20898
  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);
20899
+ var pageWidth=Math.max(44, options.pageWidth ? options.pageWidth() : win.innerWidth);
20900
+ var bodyRect=doc.body.getBoundingClientRect(), column=Math.max(0,Math.floor((last.left-bodyRect.left)/pageWidth));
20901
+ 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';
20902
+ var key = column+":"+Math.floor(last.top/44);
20578
20903
  var neighbors=noteGroups.get(key);
20579
20904
  if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
20580
20905
  neighbors.ids.push(annotation.id);
@@ -20585,22 +20910,26 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20585
20910
  group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
20586
20911
  });
20587
20912
  registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
20913
+ inkRegistry.forEach(function(group,id){if (!keepInk.has(id)) {group.remove();inkRegistry.delete(id);}});
20588
20914
  }
20589
20915
  function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
20916
+ function rebuildAndSchedule(){if(!alive)return;rebuildSpatialIndex();schedule();}
20917
+ function handleResize(){resizeCanvas();rebuildAndSchedule();}
20918
+ function handleDocumentLoad(){rebuildAndSchedule();}
20590
20919
  function tapMarkup(event) {
20591
20920
  if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
20592
20921
  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
20922
  if(hit) {event.stopPropagation();options.onTap(hit.id);}
20594
20923
  }
20595
20924
  doc.addEventListener('click',tapMarkup);
20596
- var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
20925
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(rebuildAndSchedule) : null;
20597
20926
  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);
20927
+ win.addEventListener('resize',handleResize);win.addEventListener('scroll',schedule); doc.addEventListener('load',handleDocumentLoad,true);
20928
+ if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(function(){if(alive){rebuildSpatialIndex();schedule();}});
20600
20929
  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();}};
20930
+ return {schedule:schedule, setInkMode:function(config){var preset=['ink','highlight','underline'].indexOf(config?.preset)>=0?config.preset:'ink';inkConfig={enabled:!!config?.enabled,preset:preset,color:/^#[0-9a-f]{6}$/i.test(config?.color)?config.color:'#2563eb',opacity:Number.isFinite(config?.opacity)?Math.max(0,Math.min(1,config.opacity)):1,strokeWidth:Number.isFinite(config?.strokeWidth)?Math.max(0.0025,Math.min(0.02,config.strokeWidth)):0.006};inkCanvas.style.pointerEvents=inkConfig.enabled?'auto':'none';if(!inkConfig.enabled)cancelGesture();}, invalidateLayout:rebuildAndSchedule, 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;cancelGesture();if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',handleResize);win.removeEventListener('scroll',schedule);win.removeEventListener('blur',pointerCancel);doc.removeEventListener('load',handleDocumentLoad,true);doc.removeEventListener('click',tapMarkup);inkCanvas.removeEventListener('pointerdown',pointerDown);inkCanvas.removeEventListener('pointermove',pointerMove);inkCanvas.removeEventListener('pointerup',pointerUp);inkCanvas.removeEventListener('pointercancel',pointerCancel);inkCanvas.removeEventListener('lostpointercapture',pointerCancel);inkCanvas.remove();overlay.remove();doc.body.style.position=originalPosition;registry.clear();inkRegistry.clear();rangeCache.clear();}};
20602
20931
  }
20603
- return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
20932
+ return {validAnchor:validAnchor,simplifyPath:simplifyPath,compactGesturePoints:compactGesturePoints,densifyPath:densifyPath,createSpatialIndex:createSpatialIndex,queryNearestTextRect:queryNearestTextRect,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
20604
20933
  });
20605
20934
 
20606
20935
  /* @papyrus-epub-annotations:end */
@@ -20611,10 +20940,12 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20611
20940
  let currentType = null;
20612
20941
  let book = null;
20613
20942
  let rendition = null;
20943
+ let epubResizeAnchor = null;
20614
20944
  let documentSessionId = null;
20615
20945
  let annotationDocumentId;
20616
20946
  let annotationLabels = {};
20617
20947
  let epubAnnotations = [];
20948
+ let epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
20618
20949
  const epubAnnotationRenderers = new Map();
20619
20950
  let spineItems = [];
20620
20951
  let textPages = [];
@@ -21023,6 +21354,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21023
21354
  };
21024
21355
 
21025
21356
  const clearViewer = () => {
21357
+ if(epubResizeAnchor){epubResizeAnchor.destroy();epubResizeAnchor=null;}
21026
21358
  epubAnnotationRenderers.forEach(renderer => renderer.destroy());
21027
21359
  epubAnnotationRenderers.clear();
21028
21360
  if (epubInteractionCleanup) {
@@ -21679,29 +22011,63 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21679
22011
  pageCount,
21680
22012
  });
21681
22013
 
22014
+ if (book.locations?.generate) {
22015
+ const activeBook=book;
22016
+ activeBook.locations.generate(150).then(() => {
22017
+ if (isCurrentLoad() && activeBook===book && epubLocation) {
22018
+ epubLocation.progress=activeBook.locations.percentageFromCfi(epubLocation.cfi);
22019
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
22020
+ }
22021
+ }).catch(() => {});
22022
+ }
22023
+ return renderEpubRendition(loadId,generation,null);
22024
+ };
22025
+
22026
+ const renderEpubRendition = async (loadId,generation,target) => {
22027
+ const diagnosticPayload={loadId};
22028
+ const isCurrentLoad=()=>generation===epubLoadGeneration;
22029
+ if(epubInteractionCleanup)epubInteractionCleanup();
22030
+ if(rendition)rendition.destroy();
21682
22031
  clearViewer();
21683
- rendition = book.renderTo(viewer, {
21684
- manager: 'continuous',
21685
- allowScriptedContent: false,
21686
- width: '100%',
21687
- height: '100%',
21688
- flow: 'scrolled-continuous',
21689
- spread: 'none',
21690
- });
22032
+ rendition = book.renderTo(viewer, window.PapyrusEpubReader.options(epubViewMode === 'single' ? 'paged' : 'chapter-scroll'));
21691
22033
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
21692
22034
 
21693
22035
  if (rendition && rendition.hooks && rendition.hooks.content) {
21694
22036
  const session = documentSessionId;
21695
22037
  const activeRendition = rendition;
21696
22038
  rendition.hooks.content.register((contents) => {
22039
+ const removeGestureListeners = window.PapyrusEpubReader.install(contents, {
22040
+ mode: () => epubViewMode === 'single' ? 'paged' : 'chapter-scroll',
22041
+ rtl: () => book.package?.metadata?.direction === 'rtl' || book.spine?.direction === 'rtl',
22042
+ blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
22043
+ annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
22044
+ current: () => session === documentSessionId && activeRendition === rendition,
22045
+ interact: () => epubResizeAnchor?.invalidate(),
22046
+ tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
22047
+ turn: direction => {
22048
+ epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
22049
+ if(session !== documentSessionId || activeRendition !== rendition)return;
22050
+ epubResizeAnchor?.invalidate();
22051
+ return direction<0?activeRendition.prev():activeRendition.next();
22052
+ });
22053
+ epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
22054
+ },
22055
+ });
22056
+ const gestureCleanup = () => {removeGestureListeners();epubSelectionCleanups.delete(gestureCleanup);};
22057
+ epubSelectionCleanups.add(gestureCleanup);
22058
+ contents.__papyrusGestureCleanup=gestureCleanup;
21697
22059
  const href = spineItems[contents.sectionIndex]?.href;
21698
22060
  if (href && window.PapyrusEpubAnnotations) {
21699
22061
  const renderer = window.PapyrusEpubAnnotations.install(contents, {
21700
22062
  href, get documentId() { return annotationDocumentId; },
22063
+ spineIndex:contents.sectionIndex,
21701
22064
  isCurrent: () => session === documentSessionId && activeRendition === rendition,
22065
+ pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
21702
22066
  annotations: () => epubAnnotations, labels: () => annotationLabels,
22067
+ onInkCommit: annotation => sendEvent('EPUB_INK_CREATED',{documentSessionId:session,annotation}),
21703
22068
  onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
21704
22069
  });
22070
+ renderer.setInkMode(epubInkMode);
21705
22071
  epubAnnotationRenderers.set(contents,renderer);
21706
22072
  }
21707
22073
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
@@ -21717,8 +22083,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21717
22083
  typeof selectionWindow.getSelection === 'function'
21718
22084
  ) {
21719
22085
  let selectionTimer = null;
21720
- let lastSelectionKey = '';
21721
- let lastSelectionAt = 0;
22086
+
21722
22087
  const reportSelection = () => {
21723
22088
  selectionTimer = null;
21724
22089
  if (session !== documentSessionId || activeRendition !== rendition) return;
@@ -21733,11 +22098,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21733
22098
  try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
21734
22099
  const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
21735
22100
  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;
22101
+ if (nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
21741
22102
  const payload = {documentSessionId: session, text: anchor.quote,
21742
22103
  pageIndex: contents.sectionIndex + 1, anchor};
21743
22104
  nativeMenuSelection = {contents, payload};
@@ -21752,6 +22113,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21752
22113
  // The content document still emits selectionchange, so resolve its
21753
22114
  // live Range to a CFI before the native menu action can fire.
21754
22115
  if (text) reportSelection();
22116
+ else if (nativeMenuSelection?.contents === contents) nativeMenuSelection = null;
21755
22117
  };
21756
22118
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
21757
22119
  const selectionCleanup = () => {
@@ -21774,6 +22136,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21774
22136
  typeof rendition.hooks.unloaded.register === 'function'
21775
22137
  ) {
21776
22138
  rendition.hooks.unloaded.register((view) => {
22139
+ view?.contents?.__papyrusGestureCleanup?.();
21777
22140
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
21778
22141
  if (typeof cleanup === 'function') cleanup();
21779
22142
  const renderer = epubAnnotationRenderers.get(view?.contents);
@@ -21789,21 +22152,37 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21789
22152
  if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21790
22153
  const href = spineItems[contents.sectionIndex]?.href;
21791
22154
  const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
21792
- if (!anchor) return;
22155
+ if (!anchor || nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
21793
22156
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
21794
22157
  const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
21795
22158
  nativeMenuSelection = {contents, payload};
21796
22159
  sendEvent('EPUB_TEXT_SELECTED', payload);
21797
22160
  });
21798
- ['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
21799
- if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
22161
+ ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
22162
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => name==='relocated'?renderer.schedule():renderer.invalidateLayout());
21800
22163
  }));
21801
22164
  rendition.on('relocated', (location) => {
22165
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21802
22166
  const start = location && location.start;
21803
22167
  let index = start && Number.isInteger(start.index) ? start.index : -1;
21804
22168
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
21805
22169
  if (index < 0 || index >= spineItems.length) return;
21806
22170
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
22171
+ let pageGeometry;
22172
+ const manager=rendition.manager,layout=manager?.layout;
22173
+ if(epubViewMode==='single' && layout?.divisor===1 && !manager?.settings?.fullsize && manager?.settings?.direction==='ltr'){
22174
+ const visible=manager.visible?.();
22175
+ const view=visible?.find(view=>view.index===index);
22176
+ if(view && manager.container){
22177
+ pageGeometry={direction:'ltr',firstIndex:book.spine.first()?.index,lastIndex:book.spine.last()?.index,pageWidth:layout.pageWidth,offset:manager.container.getBoundingClientRect().left-view.position().left};
22178
+ }
22179
+ }
22180
+ epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll',pageGeometry);
22181
+ if(epubLocation){
22182
+ epubLocation.cfi=epubResizeAnchor?.cfi() || epubLocation.cfi;
22183
+ if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
22184
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
22185
+ }
21807
22186
  const nextPage = index + 1;
21808
22187
  if (nextPage === currentPage) return;
21809
22188
  currentPage = nextPage;
@@ -21811,21 +22190,23 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21811
22190
  });
21812
22191
  }
21813
22192
 
22193
+ applyEpubZoom();
21814
22194
  sendEpubDiagnostic('epub.display.start', diagnosticPayload);
21815
22195
  await withEpubStageTimeout(
21816
- displayEpubPage(0),
22196
+ rendition.display(target || spineItems[0]?.href),
21817
22197
  'rendition.display',
21818
22198
  diagnosticPayload,
21819
22199
  );
21820
22200
  if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21821
22201
  sendEpubDiagnostic('epub.display.end', diagnosticPayload);
22202
+ const resizeRendition=rendition;
22203
+ epubResizeAnchor=window.PapyrusEpubReader.installResizeAnchor(resizeRendition.manager,()=>resizeRendition.location?.start?.cfi);
21822
22204
  if (
21823
22205
  rendition &&
21824
22206
  rendition.manager &&
21825
22207
  typeof rendition.manager.on === 'function' &&
21826
22208
  typeof rendition.on === 'function'
21827
22209
  ) {
21828
- let touchStart = null;
21829
22210
  let lastScrollOffset = -1;
21830
22211
  const manager = rendition.manager;
21831
22212
  epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
@@ -21900,48 +22281,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21900
22281
  return result;
21901
22282
  };
21902
22283
  }
21903
- const getTouchPoint = (event) => {
21904
- const touch =
21905
- (event && event.changedTouches && event.changedTouches[0]) ||
21906
- (event && event.touches && event.touches[0]);
21907
- if (!touch) return null;
21908
- const x = Number(touch.screenX ?? touch.clientX);
21909
- const y = Number(touch.screenY ?? touch.clientY);
21910
- return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
21911
- };
21912
- const handleScroll = (event) => {
22284
+ const handleScroll = event => {
21913
22285
  const offsetY = Math.max(0, Number(event && event.top) || 0);
21914
22286
  epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
21915
- if (Math.abs(offsetY - lastScrollOffset) < 1) return;
21916
- lastScrollOffset = offsetY;
21917
- sendEvent('VIEWER_SCROLL', { offsetY });
21918
- };
21919
- const handleTouchStart = (event) => {
21920
- touchStart = getTouchPoint(event);
21921
- epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
21922
- };
21923
- const handleTouchMove = (event) => {
21924
- epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
21925
- };
21926
- const handleTouchEnd = (event) => {
21927
- const end = getTouchPoint(event);
21928
- const start = touchStart;
21929
- touchStart = null;
21930
- const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
21931
- if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
21932
- sendEvent('VIEWER_TAP', {});
21933
- }
21934
- epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
22287
+ if (Math.abs(offsetY-lastScrollOffset)<1) return;
22288
+ lastScrollOffset=offsetY;
22289
+ sendEvent('VIEWER_SCROLL',{offsetY});
21935
22290
  };
21936
-
21937
22291
  manager.on('scroll', handleScroll);
21938
22292
  const handleScrolled = (event) => {
21939
22293
  epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
21940
22294
  };
21941
22295
  manager.on('scrolled', handleScrolled);
21942
- rendition.on('touchstart', handleTouchStart);
21943
- rendition.on('touchmove', handleTouchMove);
21944
- rendition.on('touchend', handleTouchEnd);
22296
+
21945
22297
  epubInteractionCleanup = () => {
21946
22298
  epubSelectionCleanups.forEach((cleanup) => cleanup());
21947
22299
  epubSelectionCleanups.clear();
@@ -21950,16 +22302,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21950
22302
  if (originalEnqueue) queue.enqueue = originalEnqueue;
21951
22303
  if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
21952
22304
  if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
21953
- if (typeof rendition.off === 'function') {
21954
- rendition.off('touchstart', handleTouchStart);
21955
- rendition.off('touchmove', handleTouchMove);
21956
- rendition.off('touchend', handleTouchEnd);
21957
- }
21958
22305
  epubScrollDiagnostics = null;
21959
22306
  };
21960
22307
  }
21961
- applyEpubZoom();
21962
-
21963
22308
  const outline = await withEpubStageTimeout(
21964
22309
  buildOutline(),
21965
22310
  'outline',
@@ -21973,6 +22318,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21973
22318
  return { pageCount, outline };
21974
22319
  };
21975
22320
 
22321
+ let epubNavigationQueue = Promise.resolve();
22322
+ let epubNavigationRequest = 0;
21976
22323
  const displayEpubPage = async (pageIndex) => {
21977
22324
  if (!rendition) return;
21978
22325
  const item = spineItems[pageIndex];
@@ -21982,9 +22329,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21982
22329
  target,
21983
22330
  pageIndex,
21984
22331
  });
21985
- await rendition.display(target);
21986
- currentPage = pageIndex + 1;
21987
- sendState();
22332
+ const activeRendition = rendition;
22333
+ const session = documentSessionId;
22334
+ const request = ++epubNavigationRequest;
22335
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22336
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22337
+ epubResizeAnchor?.invalidate();
22338
+ await activeRendition.display(target);
22339
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22340
+ currentPage = pageIndex + 1;
22341
+ sendState();
22342
+ });
22343
+ epubNavigationQueue = navigation;
22344
+ await navigation;
21988
22345
  };
21989
22346
 
21990
22347
  const getTextContent = async (pageIndex) => {
@@ -22086,6 +22443,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22086
22443
  };
22087
22444
 
22088
22445
  let nativeMenuSelection = null;
22446
+ let epubViewMode = "single";
22447
+ let epubLocation = null;
22089
22448
 
22090
22449
  const handleCommand = async (message) => {
22091
22450
  const { id, kind, payload } = message;
@@ -22097,7 +22456,13 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22097
22456
  documentSessionId = payload.documentSessionId || null;
22098
22457
  epubAnnotations = [];
22099
22458
  annotationDocumentId = undefined;
22459
+ epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
22100
22460
  zoom = 1.0;
22461
+ epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
22462
+ epubLocation = null;
22463
+ nativeMenuSelection = null;
22464
+ epubNavigationRequest += 1;
22465
+ epubNavigationQueue = Promise.resolve();
22101
22466
 
22102
22467
  if (currentType === 'text') {
22103
22468
  const result = await loadText(
@@ -22152,17 +22517,24 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22152
22517
  epubAnnotationRenderers.forEach(renderer => renderer.schedule());
22153
22518
  } else {
22154
22519
  const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
22155
- if (!annotation) throw new Error('Annotation not available');
22520
+ if (!annotation || (annotation.anchor.documentId && annotation.anchor.documentId !== annotationDocumentId)) throw new Error('Annotation not available for this document');
22156
22521
  // Navigation uses the CFI, never a synthetic percentage/page rectangle.
22157
22522
  const navigationSession = documentSessionId;
22158
22523
  const activeRendition = rendition;
22159
22524
  // Open the confirmed chapter first, resolve its quote, then navigate its actual range.
22160
- await activeRendition.display(annotation.anchor.href);
22161
- if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22162
- const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
22163
- const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
22164
- if(!range)throw new Error('EPUB anchor unresolved');
22165
- await activeRendition.display(contents.cfiFromRange(range));
22525
+ epubNavigationRequest += 1;
22526
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22527
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22528
+ epubResizeAnchor?.invalidate();
22529
+ await activeRendition.display(annotation.anchor.href);
22530
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22531
+ const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
22532
+ const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
22533
+ if(!range)throw new Error('EPUB anchor unresolved');
22534
+ await activeRendition.display(contents.cfiFromRange(range));
22535
+ });
22536
+ epubNavigationQueue = navigation;
22537
+ await navigation;
22166
22538
  }
22167
22539
  sendResponse(id,true,null); return;
22168
22540
  }
@@ -22189,12 +22561,61 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22189
22561
  return;
22190
22562
  }
22191
22563
 
22564
+ if (kind === 'epub-turn-page') {
22565
+ if (epubViewMode !== 'single') {sendResponse(id,true,null); return;}
22566
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB navigation');
22567
+ const active = rendition;
22568
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
22569
+ if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
22570
+ epubResizeAnchor?.invalidate();
22571
+ return payload.direction < 0 ? active.prev() : active.next();
22572
+ });
22573
+ await epubNavigationQueue;
22574
+ sendResponse(id,true,null); return;
22575
+ }
22576
+
22577
+ if (kind === 'set-epub-view-mode') {
22578
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
22579
+ const next=payload.epubMode ? (payload.epubMode==='paged'?'single':'continuous') : (payload.viewMode==='single'?'single':'continuous');
22580
+ {
22581
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22582
+ if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
22583
+ const target=epubResizeAnchor?.cfi() || epubLocation?.cfi;
22584
+ epubViewMode=next;
22585
+ await renderEpubRendition('layout',epubLoadGeneration,target);
22586
+ });
22587
+ await epubNavigationQueue;
22588
+ }
22589
+ sendResponse(id,true,null);return;
22590
+ }
22591
+ if (kind === 'set-epub-ink-mode') {
22592
+ if(currentType!=='epub'||!documentSessionId||payload.documentSessionId!==documentSessionId)throw new Error('Stale EPUB ink session');
22593
+ epubInkMode={enabled:payload.enabled===true,preset:['ink','highlight','underline'].indexOf(payload.preset)>=0?payload.preset:'ink',color:/^#[0-9a-f]{6}$/i.test(payload.color||'')?payload.color:'#2563eb',opacity:Number.isFinite(payload.opacity)?Math.max(0,Math.min(1,payload.opacity)):1,strokeWidth:Number.isFinite(payload.strokeWidth)?Math.max(0.0025,Math.min(0.02,payload.strokeWidth)):0.006};
22594
+ epubAnnotationRenderers.forEach(renderer=>renderer.setInkMode(epubInkMode));
22595
+ sendResponse(id,true,null);return;
22596
+ }
22597
+ if(kind === 'epub-go-to-location'){
22598
+ if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
22599
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>{if(payload.documentSessionId!==documentSessionId)return;epubResizeAnchor?.invalidate();return rendition.display(payload.cfi);});
22600
+ await epubNavigationQueue;sendResponse(id,true,null);return;
22601
+ }
22602
+
22192
22603
  if (kind === 'set-zoom') {
22193
- zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
22194
- applyZoom();
22195
- sendState();
22196
- sendResponse(id, true, { zoom });
22197
- return;
22604
+ const requestedZoom=Math.max(0.5,Math.min(4.0,payload.zoom || 1.0));
22605
+ if(currentType === 'epub') {
22606
+ const session=documentSessionId;
22607
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22608
+ if(session!==documentSessionId)return;
22609
+ const active=rendition;
22610
+ const cfi=epubResizeAnchor?.cfi() || epubLocation?.cfi;
22611
+ zoom=requestedZoom;applyEpubZoom();
22612
+ // Let the content CSS update before redisplaying the logical anchor.
22613
+ await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));
22614
+ if(cfi && active===rendition && session===documentSessionId)await active.display(cfi);
22615
+ });
22616
+ await epubNavigationQueue;
22617
+ } else {zoom=requestedZoom;applyZoom();}
22618
+ sendState();sendResponse(id,true,{zoom});return;
22198
22619
  }
22199
22620
 
22200
22621
  if (kind === 'set-rotation') {