From 2a399077e2eacd4a6938f33a7fe2488925d4c000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Thu, 26 Feb 2026 16:21:12 +0100 Subject: [PATCH 01/19] remove getLocatorFragments from bridge --- .../nota/flutter_readium/ReadiumReaderWidget.kt | 8 ++++++-- flutter_readium/assets/helpers/epub.js | 2 +- flutter_readium/lib/reader_channel.dart | 15 --------------- flutter_readium/lib/reader_widget.dart | 9 --------- flutter_readium/lib/reader_widget_web.dart | 7 ------- .../src/extensions/readium_string_extensions.dart | 2 -- .../lib/src/extensions/strings.dart | 7 +++++++ .../lib/src/reader/reader_widget_interface.dart | 3 --- .../lib/src/shared/publication/link.dart | 12 ++++-------- .../lib/src/shared/publication/locator.dart | 7 +++---- .../lib/src/shared/publication/publication.dart | 15 ++++++--------- 11 files changed, 27 insertions(+), 60 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt index 4f81f1ce..096ebe14 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt @@ -246,9 +246,13 @@ class ReadiumReaderWidget( Log.d(TAG, "::onMethodCall ${call.method}") when (call.method) { "setPreferences" -> { - @Suppress("UNCHECKED_CAST") - val prefsMap = call.arguments as Map try { + @Suppress("UNCHECKED_CAST") + val prefsMap = call.arguments as? Map ?: run { + result.error("FlutterReadium", "Failed to set preferences", "Invalid argument") + return@launch + } + setPreferencesFromMap(prefsMap) result.success(null) } catch (ex: Exception) { diff --git a/flutter_readium/assets/helpers/epub.js b/flutter_readium/assets/helpers/epub.js index fc8af5ff..226bfe77 100644 --- a/flutter_readium/assets/helpers/epub.js +++ b/flutter_readium/assets/helpers/epub.js @@ -1 +1 @@ -var epub;(()=>{var e={324(e,t,n){"use strict";n.r(t)},679(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const o=document.createElement("tbody"),r=document.createElement("tr"),i=document.createElement("tr");r.innerHTML=" ",i.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,r.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),i.appendChild(t)}})),o.appendChild(r),o.appendChild(i),e.appendChild(o)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const o=e.rows[0];o&&o.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=o,n=Array.from(o.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const o=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",o[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}},50(e){self,e.exports=(()=>{"use strict";var e={d:(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function n(e){return"object"==typeof e&&null!==e&&e.nodeType===Node.ELEMENT_NODE}e.r(t),e.d(t,{default:()=>K,getCssSelector:()=>X});const o={NONE:"",DESCENDANT:" ",CHILD:" > "},r={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},i="CssSelectorGenerator";function l(e="unknown problem",...t){console.warn(`${i}: ${e}`,...t)}const s={selectors:[r.id,r.class,r.tag,r.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function c(e){return e instanceof RegExp}function a(e){return["string","function"].includes(typeof e)||c(e)}function d(e){return Array.isArray(e)?e.filter(a):[]}function u(e){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(e){return e instanceof Node}(e)&&t.includes(e.nodeType)}function h(e,t){if(u(e))return e.contains(t)||l("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will not work as intended."),e;const n=t.getRootNode({composed:!1});return u(n)?(n!==document&&l("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):y(t)}function f(e){return"number"==typeof e?e:Number.POSITIVE_INFINITY}function g(e=[]){const[t=[],...n]=e;return 0===n.length?t:n.reduce(((e,t)=>e.filter((e=>t.includes(e)))),t)}function m(e){return[].concat(...e)}function v(e){const t=e.map((e=>{if(c(e))return t=>e.test(t);if("function"==typeof e)return t=>{const n=e(t);return"boolean"!=typeof n?(l("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",e),!1):n};if("string"==typeof e){const t=new RegExp("^"+e.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return e=>t.test(e)}return l("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",e),()=>!1}));return e=>t.some((t=>t(e)))}function p(e,t,n){const o=Array.from(h(n,e[0]).querySelectorAll(t));return o.length===e.length&&e.every((e=>o.includes(e)))}function _(e,t){t=null!=t?t:y(e);const o=[];let r=e;for(;n(r)&&r!==t;)o.push(r),r=r.parentElement;return o}function b(e,t){return g(e.map((e=>_(e,t))))}function y(e){return e.ownerDocument.querySelector(":root")}const N=", ",S=new RegExp(["^$","\\s"].join("|")),E=new RegExp(["^$"].join("|")),L=[r.nthoftype,r.tag,r.id,r.class,r.attribute,r.nthchild],C=v(["class","id","ng-*"]);function T({name:e}){return`[${e}]`}function w({name:e,value:t}){return`[${e}='${t}']`}function x({nodeName:e,nodeValue:t}){return{name:H(e),value:H(null!=t?t:void 0)}}function P(e){const t=Array.from(e.attributes).filter((t=>function({nodeName:e,nodeValue:t},n){const o=n.tagName.toLowerCase();return!(["input","option"].includes(o)&&"value"===e||"src"===e&&(null==t?void 0:t.startsWith("data:"))||C(e))}(t,e))).map(x);return[...t.map(T),...t.map(w)]}function O(e){var t;return(null!==(t=e.getAttribute("class"))&&void 0!==t?t:"").trim().split(/\s+/).filter((e=>!E.test(e))).map((e=>`.${H(e)}`))}function A(e){var t;const n=null!==(t=e.getAttribute("id"))&&void 0!==t?t:"",o=`#${H(n)}`,r=e.getRootNode({composed:!1});return!S.test(n)&&p([e],o,r)?[o]:[]}function R(e){const t=e.parentNode;if(t){const o=Array.from(t.childNodes).filter(n).indexOf(e);if(o>-1)return[`:nth-child(${String(o+1)})`]}return[]}function I(e){return[H(e.tagName.toLowerCase())]}function $(e){const t=[...new Set(m(e.map(I)))];return 0===t.length||t.length>1?[]:[t[0]]}function k(e){const t=$([e])[0],n=e.parentElement;if(n){const o=Array.from(n.children).filter((e=>e.tagName.toLowerCase()===t)),r=o.indexOf(e);if(r>-1)return[`${t}:nth-of-type(${String(r+1)})`]}return[]}function M(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){return Array.from(function*(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){let n=0,o=F(1);for(;o.length<=e.length&&ne[t]));yield t,o=D(o,e.length-1)}}(e,{maxResults:t}))}function D(e=[],t=0){const n=e.length;if(0===n)return[];const o=[...e];o[n-1]+=1;for(let e=n-1;e>=0;e--)if(o[e]>t){if(0===e)return F(n+1);o[e-1]++,o[e]=o[e-1]+1}return o[n-1]>t?F(n+1):o}function F(e=1){return Array.from(Array(e).keys())}const q=":".charCodeAt(0).toString(16).toUpperCase(),j=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function H(e=""){return CSS?CSS.escape(e):function(e=""){return e.split("").map((e=>":"===e?`\\${q} `:j.test(e)?`\\${e}`:escape(e).replace(/%/g,"\\"))).join("")}(e)}const V={tag:$,id:function(e){return 0===e.length||e.length>1?[]:A(e[0])},class:function(e){return g(e.map(O))},attribute:function(e){return g(e.map(P))},nthchild:function(e){return g(e.map(R))},nthoftype:function(e){return g(e.map(k))}},W={tag:I,id:A,class:O,attribute:P,nthchild:R,nthoftype:k};function B(e){return e.includes(r.tag)||e.includes(r.nthoftype)?[...e]:[...e,r.tag]}function U(e={}){const t=[...L];return e[r.tag]&&e[r.nthoftype]&&t.splice(t.indexOf(r.tag),1),t.map((t=>{return(o=e)[n=t]?o[n].join(""):"";var n,o})).join("")}function Y(e,t,n="",r){const i=function(e,t){return""===t?e:function(e,t){return[...e.map((e=>t+o.DESCENDANT+e)),...e.map((e=>t+o.CHILD+e))]}(e,t)}(function(e,t,n){const o=function(e,t){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=t,l=v(n),s=v(o);return function(e){const{selectors:t,includeTag:n}=e,o=[...t];return n&&!o.includes("tag")&&o.push("tag"),o}(t).reduce(((t,n)=>{const o=function(e,t){return(0,V[t])(e)}(e,n),c=function(e=[],t,n){return e.filter((e=>n(e)||!t(e)))}(o,l,s),a=function(e=[],t){return e.sort(((e,n)=>{const o=t(e),r=t(n);return o&&!r?-1:!o&&r?1:0}))}(c,s);return t[n]=r?M(a,{maxResults:i}):a.map((e=>[e])),t}),{})}(e,n),r=function(e,t){return function(e){const{selectors:t,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=e,i=n?M(t,{maxResults:r}):t.map((e=>[e]));return o?i.map(B):i}(t).map((t=>function(e,t){const n={};return e.forEach((e=>{const o=t[e];o&&o.length>0&&(n[e]=o)})),function(e={}){let t=[];return Object.entries(e).forEach((([e,n])=>{t=n.flatMap((n=>0===t.length?[{[e]:n}]:t.map((t=>Object.assign(Object.assign({},t),{[e]:n})))))})),t}(n).map(U)}(t,e))).filter((e=>e.length>0))}(o,n),i=m(r);return[...new Set(i)]}(e,0,r),n);for(const n of i)if(p(e,n,t))return n;return null}function G(e){return{value:e,include:!1}}function z({selectors:e,operator:t}){let n=[...L];e[r.tag]&&e[r.nthoftype]&&(n=n.filter((e=>e!==r.tag)));let o="";return n.forEach((t=>{var n;(null!==(n=e[t])&&void 0!==n?n:[]).forEach((({value:e,include:t})=>{t&&(o+=e)}))})),t+o}function J(e){return[":root",..._(e).reverse().map((e=>{const t=function(e,t,n=o.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(e,t){return W[t](e)}(e,t).map(G))})),{element:e,operator:n,selectors:r}}(e,[r.nthchild],o.CHILD);return t.selectors.nthchild.forEach((e=>{e.include=!0})),t})).map(z)].join("")}function X(e,t={}){var o;const i=function(e){(e instanceof NodeList||e instanceof HTMLCollection)&&(e=Array.from(e));const t=(Array.isArray(e)?e:[e]).filter(n);return[...new Set(t)]}(e),l=function(e,t={}){const n=Object.assign(Object.assign({},s),t);return{selectors:(o=n.selectors,Array.isArray(o)?o.filter((e=>{return t=r,n=e,Object.values(t).includes(n);var t,n})):[]),whitelist:d(n.whitelist),blacklist:d(n.blacklist),root:h(n.root,e),combineWithinSelector:!!n.combineWithinSelector,combineBetweenSelectors:!!n.combineBetweenSelectors,includeTag:!!n.includeTag,maxCombinations:f(n.maxCombinations),maxCandidates:f(n.maxCandidates)};var o}(i[0],t),c=null!==(o=l.root)&&void 0!==o?o:y(i[0]);let a="",u=c;function g(){return function(e,t,n="",o){if(0===e.length)return null;const r=[e.length>1?e:[],...b(e,t).map((e=>[e]))];for(const e of r){const r=Y(e,t,n,o);if(r)return{foundElements:e,selector:r}}return null}(i,u,a,l)}let m=g();for(;m;){const{foundElements:e,selector:t}=m;if(p(i,t,c))return t;u=e[0],a=t,m=g()}return i.length>1?i.map((e=>X(e,l))).join(N):function(e){return e.map(J).join(N)}(i)}const K=X;return t})()}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var o={};(()=>{"use strict";var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(50),r=n(679);n(324);class i{constructor(){this._headingTagNames=["h1","h2","h3","h4","h5","h6"],this._activeLocationId="activeLocation",this._locationTag="span",this._documentRange=document.createRange(),document.documentElement.style.setProperty("-webkit-line-box-contain","block inline replaced")}isReaderReady(){return!!readium}setLocation(e,t){var n,o,r,i,l,s,c,a,d,u;this._debugLog(e);try{if(null==e)return void this._debugLog("No locator set");if(this._removeLocation(),this._setLocation(e,t),this._isComicBook()){const t=null!==(s=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:null===(l=null===(i=null===(r=null==e?void 0:e.locations)||void 0===r?void 0:r.domRange)||void 0===i?void 0:i.start)||void 0===l?void 0:l.cssSelector)&&void 0!==s?s:null===(d=null===(a=null===(c=null==e?void 0:e.locations)||void 0===c?void 0:c.domRange)||void 0===a?void 0:a.end)||void 0===d?void 0:d.cssSelector;if(null==t)return void this._errorLog("Css selector not set!");const h=this._getDurationFragment(null===(u=null==e?void 0:e.locations)||void 0===u?void 0:u.fragments);if(null==h)return void this._errorLog("Duration not set!");window.GotoComicFrame(t,1e3*h)}}catch(e){this._errorLog(e)}}scrollToLocations(e,t,n){try{const o=this._processLocations(e);if(null!=o)return this._scrollToProcessedRange(o,t,n),!0;const r=e.progression;if(null!=r)return null===readium||void 0===readium||readium.scrollToPosition(r),!0;this._debugLog("ScrollToLocations: Unknown range",e)}catch(e){this._errorLog(e)}return!1}isLocatorVisible(e){var t,n,o;this._debugLog(e);try{const r=e.locations,i=null!==(t=r.cssSelector)&&void 0!==t?t:null===(o=null===(n=r.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;if(this._isComicBook()){const e=null!=document.querySelector(i);return this._debugLog("Comic book",r,{found:e,selector:i}),e}const l=this._processLocations(r);return null==l?(this._debugLog("isLocatorVisible: Unknown range",r),!1):this._isProcessedRangeVisible(l)&&!!document.querySelector(`${i} #${this._activeLocationId}`)}catch(e){return this._errorLog(e),!0}}getLocatorFragments(e,t){var n,o,r,i;try{const l=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:this._findFirstVisibleCssSelector();if(null==l||!(null==l?void 0:l.length))return this._debugLog("getLocatorFragments: selector not found, returning locator from args"),e;const s=[...this._getPageFragments(t),...this._getTocFragments(l),...this._getPhysicalPageFragments(l)];return Object.assign(Object.assign({},e),{locations:Object.assign(Object.assign({cssSelector:l},e.locations),{fragments:[...null!==(i=null===(r=e.locations)||void 0===r?void 0:r.fragments)&&void 0!==i?i:[],...s]})})}catch(t){return this._errorLog(t),e}}_isComicBook(){try{return!!(null===comicBookPage||void 0===comicBookPage?void 0:comicBookPage.isComicBook())}catch(e){return!1}}_isTextNode(e){const t=e.nodeType;return 3===t||4===t}_clamp(e,t,n){return Math.min(Math.max(e,t),n)}_findAndSplitOffset(e,t){return function e(n){if(3===n.nodeType||4===n.nodeType){const e=n;if(t<=0)return e;const o=e.length;if(t=0;){if(""!==n[t].trim())return{node:e,charOffset:t};--t}t=void 0}e=this._previousNode(e)}}_findNonWhitespace(e,t){var n;return null!==(n=this._findNonWhitespaceBackward(e,t))&&void 0!==n?n:this._findNonWhitespaceForward(e,t)}_setLocation(e,t){var n,o,r,i,l,s,c,a,d,u,h;const f=readium.link;this._debugLog("create:",e,f);const g=e.locations,m=null!==(n=g.cssSelector)&&void 0!==n?n:null===(r=null===(o=null==g?void 0:g.domRange)||void 0===o?void 0:o.start)||void 0===r?void 0:r.cssSelector;if(!m)return void this._errorLog("Start css selector not found");const v=document.querySelector(m);if(!v)return void this._errorLog("Start parent not found");const p=null!==(l=null===(i=null==g?void 0:g.domRange)||void 0===i?void 0:i.end.cssSelector)&&void 0!==l?l:m,_=p===m?v:document.querySelector(p);if(!_)return void this._errorLog("End parent not found");const b=null===(c=null===(s=null==g?void 0:g.domRange)||void 0===s?void 0:s.start)||void 0===c?void 0:c.charOffset,y=null===(d=null===(a=null==g?void 0:g.domRange)||void 0===a?void 0:a.end)||void 0===d?void 0:d.charOffset;if(!b&&!y&&t)return void this._wrapWithLocationElement(v);const N=null!==(u=this._findAndSplitOffset(v,b))&&void 0!==u?u:this._nextNodeNotChild(v),S=null!==(h=this._findAndSplitOffset(_,y))&&void 0!==h?h:this._nextNodeNotChild(_),E=new Array;for(let e=N;e&&e!==S;e=this._nextNode(e))this._isTextNode(e)&&E.push(e);for(const e of E){const t=this._setLocationElement();t.appendChild(e.cloneNode(!0)),e.replaceWith(t)}}_removeLocation(){this._debugLog("Remove old location");const e=document.querySelectorAll(`#${this._activeLocationId}`);null==e||e.forEach((e=>{if(this._isAndroid())return void e.removeAttribute("id");const t=e.parentNode;e.replaceWith(...e.childNodes),t.normalize()}))}_safeVisibleRect(e){const{innerWidth:t,innerHeight:n}=window;return e?{left:0,top:.05*n,right:t,bottom:.95*n}:{left:0,top:0,right:t,bottom:n}}*_descendentTextNodes(e){if(this._isTextNode(e))yield e;else for(const t of e.childNodes)yield*this._descendentTextNodes(t)}_findTextPosition(e,t){if(null!=e)if(t<0||isNaN(t))this._errorLog(`findTextPosition: invalid charOffset, node=${e.nodeValue}, charOffset=${t}`);else{if(0===t)return{node:e,charOffset:t};for(const n of this._descendentTextNodes(e)){const e=n.length;if(t<=e)return{node:n,charOffset:t};t-=e}this._errorLog(`findTextPosition: failed, node=${this._debugNode(e)}, charOffset=${t}`)}else this._errorLog(`findTextPosition: no node, charOffset=${t}`)}_processDomRange(e){const{start:t,end:n}=e,{cssSelector:o,charOffset:r}=t,{cssSelector:i,charOffset:l}=null!=n&&void 0!==n?n:t,s=document.querySelector(o),c=this._findTextPosition(s,null!=r?r:0);if(null==c)return void this._errorLog(`DomRange bad start, selector=${o}`);const a=i===o?s:document.querySelector(i),d=this._findTextPosition(a,null!=l&&void 0!==l?l:0);if(null!=d){try{this._documentRange.setStart(c.node,c.charOffset)}catch(e){this._errorLog(`${this._debugNode(c.node)}, ${c.charOffset}`,e),this._documentRange.setStartAfter(c.node)}try{this._documentRange.setEnd(d.node,d.charOffset)}catch(e){this._errorLog(`${this._debugNode(d.node)}, ${d.charOffset}`,e),this._documentRange.setEndAfter(d.node)}if(0===this._documentRange.getClientRects().length){const e=this._findNonWhitespace(c.node,c.charOffset);if(null==e)return void this._errorLog("Couldn't find any non-whitespace characters in the document!'");const{node:t,charOffset:n}=e;this._documentRange.setStart(t,n),this._documentRange.setEnd(t,n+1)}return this._documentRange}this._errorLog(`DomRange bad end, selector=${i}`)}_processCssSelector(e){const t=document.querySelector(e);if(null!=t)return"none"===window.getComputedStyle(t).display&&(t.style.display=this._isPageBreakElement(t)?"flex":"block"),this._documentRange.selectNode(t),this._documentRange;this._errorLog(`processCssSelector: error: node not found ${e}`)}_processLocations(e){var t,n,o;if(null==e)return void this._errorLog("location not set");if(e.domRange)return this._processDomRange(e.domRange);const r=null!==(t=e.cssSelector)&&void 0!==t?t:null===(o=null===(n=e.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;return r?this._processCssSelector(r):void 0}_scrollToProcessedRange(e,t,n){!n&&this._isProcessedRangeVisible(e)||this._scrollToBoundingClientRect(e,t)}_scrollToBoundingClientRect(e,t){const{top:n,right:o,bottom:r,left:i}=e.getBoundingClientRect();if(0===n&&0===o&&0===r&&0===i)return void this._debugLog("scrollToBoundingClientRect: Scrolling to defective bounding rect, abort! ",e.getClientRects(),e.getClientRects().length);const{scrollLeft:l,scrollWidth:s,scrollTop:c,scrollHeight:a}=document.scrollingElement;if(t){const{top:e,bottom:o}=this._safeVisibleRect(t);if(no){const t=this._clamp((c+n-e)/a,0,1);null===readium||void 0===readium||readium.scrollToPosition(t)}}else{const e=(l+.5*(i+o))/s;null===readium||void 0===readium||readium.scrollToPosition(e)}}_isProcessedRangeVisible(e){const{innerWidth:t,innerHeight:n}=window,{top:o,right:r,bottom:i,left:l}=e.getBoundingClientRect();return oe.includes("duration=")));if(!t)return void this._errorLog("Duration fragment not found.");const n=/duration=(\d+(?:\.\d+)?)/.exec(t);return n?(this._debugLog("Duration fragment:",n[1]),parseFloat(n[1])):void this._errorLog("Invalid duration format.")}catch(e){return void this._errorLog("Could not retrieve duration fragment!")}}_getTocFragments(e){var t;try{const n=null===(t=this._findPrecedingAncestorSiblingHeadings(e)[0])||void 0===t?void 0:t.id;return null==n?[]:[`toc=${n}`]}catch(e){return this._errorLog(e),[]}}_getPhysicalPageFragments(e){try{const t=this._findCurrentPhysicalPage(e);return null==t?[]:[`physicalPage=${t}`]}catch(t){return this._errorLog(`Selector:${e} -- ${t}`),[]}}_findPrecedingAncestorSiblingHeadings(e){var t,n;const o=document.querySelector(e),r=null!==(t=null==o?void 0:o.querySelectorAll(this._headingTagNames.join(","))[0])&&void 0!==t?t:o;if(null==r)return;if(!this._allHeadings){const e=Array.from(window.document.querySelectorAll(this._headingTagNames.join(",")));for(const t of e)if(t){const e=t,n=e.textContent||e.getAttribute("title")||e.getAttribute("aria-label");let o=e.getAttribute("id");if(!o){let t,n=e;for(;(t=n.parentNode)&&(null==t?void 0:t.nodeType)===Node.ELEMENT_NODE&&t.firstElementChild===n;){const e=t.getAttribute("id");if(e){o=e;break}n=t}}const r={element:e,id:o||null,level:parseInt(e.localName.substring(1),10),text:n};this._allHeadings||(this._allHeadings=[]),this._allHeadings.push(r)}this._allHeadings||(this._allHeadings=[])}let i;for(let e=this._allHeadings.length-1;e>=0;e--){const t=this._allHeadings[e],n=r.compareDocumentPosition(t.element);(0===n||n&Node.DOCUMENT_POSITION_PRECEDING||n&Node.DOCUMENT_POSITION_CONTAINS)&&(i||(i=[]),(null==t?void 0:t.id)&&i.push({id:t.id,level:t.level,text:t.text}))}if(null==i?void 0:i.length)return i;const l=null!==(n=o.closest("section"))&&void 0!==n?n:o.closest("body");return l?[{id:l.id,level:0,text:l.innerText}]:void 0}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(e){var t;let n=document.querySelector(e);if(null!=n){if(this._isPageBreakElement(n))return this._getPhysicalPageIndexFromElement(n);for(;n.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(n);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(n)));t>=0;t--){const n=e[t],o=this._findPhysicalPageIndex(n);if(null!=o)return o}if(n=n.parentNode,null==n||"body"===n.nodeName.toLowerCase())return null===(t=document.querySelector("head [name='webpub:currentPage']"))||void 0===t?void 0:t.getAttribute("content")}}}_findFirstVisibleCssSelector(){return this._getCssSelector(this._getFirstVisibleElement())}_getCssSelector(e){var n,o;try{const r=(0,t.getCssSelector)(e,{root:document.querySelector("body")}),i=null!==(o=null===(n=null==r?void 0:r.replace(":root > :nth-child(2)","body"))||void 0===n?void 0:n.trim())&&void 0!==o?o:"body";return this._debugLog(i),i}catch(e){return this._errorLog(e),"body"}}_getFirstVisibleElement(){const e=this._findFirstVisibleElement(document.body);return this._debugLog("First visible element:",{tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className}),e}_findFirstVisibleElement(e){const t={tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className};for(const n of e.children){const o={tagName:n.nodeName.toLocaleLowerCase(),id:n.id,className:n.className};if(this._isElementVisible(n))if(this._shouldIgnoreElement(n))this._debugLog("Element is ignored - continue",o);else{if(n.id.includes(`${this._activeLocationId}`))return this._debugLog("Child is an active location element, return closest element with id",{childData:o,nodeData:t}),e.id?e:this._findClosestElementWithId(n);if(n.hasChildNodes())return this._debugLog("Loop into children",o),this._findFirstVisibleElement(n);if(!n.id)return this._debugLog("Element has no ID attribute - return closest element with id",o),e.id?e:this._findClosestElementWithId(n)}}return this._debugLog("return:",t),e}_findClosestElementWithId(e){let t=e.parentElement;for(;null!==t;){if(t.id)return t;t=t.parentElement}return this._debugLog("No element with id attr found!"),e}_isElementVisible(e,t=!1){if(null===readium||void 0===readium?void 0:readium.isFixedLayout)return!0;if(e===document.body||e===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;const n=e.getBoundingClientRect();return t?this._isScrollModeEnabled()?n.top>=0&&n.top<=document.documentElement.clientHeight:n.left>=1:this._isScrollModeEnabled()?n.bottom>0&&n.top0&&n.lefte instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_debugNode(e){var t,n;if(e instanceof Node){return(new XMLSerializer).serializeToString(e)}if("innerHTML"in e||"textContent"in e){const o=e;return null!==(n=null!==(t=o.innerHTML)&&void 0!==t?t:o.textContent)&&void 0!==n?n:"?"}}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function l(){window.epubPage||((0,r.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",l),window.epubPage=new i)}e.EpubPage=i,"loading"!==document.readyState?window.setTimeout(l):document.addEventListener("DOMContentLoaded",l)})(),epub=o})(); \ No newline at end of file +var epub;(()=>{var e={324(e,t,n){"use strict";n.r(t)},679(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const o=document.createElement("tbody"),r=document.createElement("tr"),i=document.createElement("tr");r.innerHTML=" ",i.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,r.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),i.appendChild(t)}})),o.appendChild(r),o.appendChild(i),e.appendChild(o)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const o=e.rows[0];o&&o.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=o,n=Array.from(o.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const o=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",o[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}},50(e){self,e.exports=(()=>{"use strict";var e={d:(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function n(e){return"object"==typeof e&&null!==e&&e.nodeType===Node.ELEMENT_NODE}e.r(t),e.d(t,{default:()=>K,getCssSelector:()=>X});const o={NONE:"",DESCENDANT:" ",CHILD:" > "},r={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},i="CssSelectorGenerator";function l(e="unknown problem",...t){console.warn(`${i}: ${e}`,...t)}const s={selectors:[r.id,r.class,r.tag,r.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function c(e){return e instanceof RegExp}function a(e){return["string","function"].includes(typeof e)||c(e)}function d(e){return Array.isArray(e)?e.filter(a):[]}function u(e){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(e){return e instanceof Node}(e)&&t.includes(e.nodeType)}function h(e,t){if(u(e))return e.contains(t)||l("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will not work as intended."),e;const n=t.getRootNode({composed:!1});return u(n)?(n!==document&&l("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):y(t)}function f(e){return"number"==typeof e?e:Number.POSITIVE_INFINITY}function g(e=[]){const[t=[],...n]=e;return 0===n.length?t:n.reduce(((e,t)=>e.filter((e=>t.includes(e)))),t)}function m(e){return[].concat(...e)}function v(e){const t=e.map((e=>{if(c(e))return t=>e.test(t);if("function"==typeof e)return t=>{const n=e(t);return"boolean"!=typeof n?(l("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",e),!1):n};if("string"==typeof e){const t=new RegExp("^"+e.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return e=>t.test(e)}return l("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",e),()=>!1}));return e=>t.some((t=>t(e)))}function _(e,t,n){const o=Array.from(h(n,e[0]).querySelectorAll(t));return o.length===e.length&&e.every((e=>o.includes(e)))}function p(e,t){t=null!=t?t:y(e);const o=[];let r=e;for(;n(r)&&r!==t;)o.push(r),r=r.parentElement;return o}function b(e,t){return g(e.map((e=>p(e,t))))}function y(e){return e.ownerDocument.querySelector(":root")}const N=", ",S=new RegExp(["^$","\\s"].join("|")),E=new RegExp(["^$"].join("|")),L=[r.nthoftype,r.tag,r.id,r.class,r.attribute,r.nthchild],C=v(["class","id","ng-*"]);function T({name:e}){return`[${e}]`}function w({name:e,value:t}){return`[${e}='${t}']`}function x({nodeName:e,nodeValue:t}){return{name:H(e),value:H(null!=t?t:void 0)}}function P(e){const t=Array.from(e.attributes).filter((t=>function({nodeName:e,nodeValue:t},n){const o=n.tagName.toLowerCase();return!(["input","option"].includes(o)&&"value"===e||"src"===e&&(null==t?void 0:t.startsWith("data:"))||C(e))}(t,e))).map(x);return[...t.map(T),...t.map(w)]}function O(e){var t;return(null!==(t=e.getAttribute("class"))&&void 0!==t?t:"").trim().split(/\s+/).filter((e=>!E.test(e))).map((e=>`.${H(e)}`))}function A(e){var t;const n=null!==(t=e.getAttribute("id"))&&void 0!==t?t:"",o=`#${H(n)}`,r=e.getRootNode({composed:!1});return!S.test(n)&&_([e],o,r)?[o]:[]}function R(e){const t=e.parentNode;if(t){const o=Array.from(t.childNodes).filter(n).indexOf(e);if(o>-1)return[`:nth-child(${String(o+1)})`]}return[]}function I(e){return[H(e.tagName.toLowerCase())]}function $(e){const t=[...new Set(m(e.map(I)))];return 0===t.length||t.length>1?[]:[t[0]]}function M(e){const t=$([e])[0],n=e.parentElement;if(n){const o=Array.from(n.children).filter((e=>e.tagName.toLowerCase()===t)),r=o.indexOf(e);if(r>-1)return[`${t}:nth-of-type(${String(r+1)})`]}return[]}function k(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){return Array.from(function*(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){let n=0,o=F(1);for(;o.length<=e.length&&ne[t]));yield t,o=D(o,e.length-1)}}(e,{maxResults:t}))}function D(e=[],t=0){const n=e.length;if(0===n)return[];const o=[...e];o[n-1]+=1;for(let e=n-1;e>=0;e--)if(o[e]>t){if(0===e)return F(n+1);o[e-1]++,o[e]=o[e-1]+1}return o[n-1]>t?F(n+1):o}function F(e=1){return Array.from(Array(e).keys())}const q=":".charCodeAt(0).toString(16).toUpperCase(),j=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function H(e=""){return CSS?CSS.escape(e):function(e=""){return e.split("").map((e=>":"===e?`\\${q} `:j.test(e)?`\\${e}`:escape(e).replace(/%/g,"\\"))).join("")}(e)}const V={tag:$,id:function(e){return 0===e.length||e.length>1?[]:A(e[0])},class:function(e){return g(e.map(O))},attribute:function(e){return g(e.map(P))},nthchild:function(e){return g(e.map(R))},nthoftype:function(e){return g(e.map(M))}},W={tag:I,id:A,class:O,attribute:P,nthchild:R,nthoftype:M};function B(e){return e.includes(r.tag)||e.includes(r.nthoftype)?[...e]:[...e,r.tag]}function U(e={}){const t=[...L];return e[r.tag]&&e[r.nthoftype]&&t.splice(t.indexOf(r.tag),1),t.map((t=>{return(o=e)[n=t]?o[n].join(""):"";var n,o})).join("")}function Y(e,t,n="",r){const i=function(e,t){return""===t?e:function(e,t){return[...e.map((e=>t+o.DESCENDANT+e)),...e.map((e=>t+o.CHILD+e))]}(e,t)}(function(e,t,n){const o=function(e,t){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=t,l=v(n),s=v(o);return function(e){const{selectors:t,includeTag:n}=e,o=[...t];return n&&!o.includes("tag")&&o.push("tag"),o}(t).reduce(((t,n)=>{const o=function(e,t){return(0,V[t])(e)}(e,n),c=function(e=[],t,n){return e.filter((e=>n(e)||!t(e)))}(o,l,s),a=function(e=[],t){return e.sort(((e,n)=>{const o=t(e),r=t(n);return o&&!r?-1:!o&&r?1:0}))}(c,s);return t[n]=r?k(a,{maxResults:i}):a.map((e=>[e])),t}),{})}(e,n),r=function(e,t){return function(e){const{selectors:t,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=e,i=n?k(t,{maxResults:r}):t.map((e=>[e]));return o?i.map(B):i}(t).map((t=>function(e,t){const n={};return e.forEach((e=>{const o=t[e];o&&o.length>0&&(n[e]=o)})),function(e={}){let t=[];return Object.entries(e).forEach((([e,n])=>{t=n.flatMap((n=>0===t.length?[{[e]:n}]:t.map((t=>Object.assign(Object.assign({},t),{[e]:n})))))})),t}(n).map(U)}(t,e))).filter((e=>e.length>0))}(o,n),i=m(r);return[...new Set(i)]}(e,0,r),n);for(const n of i)if(_(e,n,t))return n;return null}function G(e){return{value:e,include:!1}}function z({selectors:e,operator:t}){let n=[...L];e[r.tag]&&e[r.nthoftype]&&(n=n.filter((e=>e!==r.tag)));let o="";return n.forEach((t=>{var n;(null!==(n=e[t])&&void 0!==n?n:[]).forEach((({value:e,include:t})=>{t&&(o+=e)}))})),t+o}function J(e){return[":root",...p(e).reverse().map((e=>{const t=function(e,t,n=o.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(e,t){return W[t](e)}(e,t).map(G))})),{element:e,operator:n,selectors:r}}(e,[r.nthchild],o.CHILD);return t.selectors.nthchild.forEach((e=>{e.include=!0})),t})).map(z)].join("")}function X(e,t={}){var o;const i=function(e){(e instanceof NodeList||e instanceof HTMLCollection)&&(e=Array.from(e));const t=(Array.isArray(e)?e:[e]).filter(n);return[...new Set(t)]}(e),l=function(e,t={}){const n=Object.assign(Object.assign({},s),t);return{selectors:(o=n.selectors,Array.isArray(o)?o.filter((e=>{return t=r,n=e,Object.values(t).includes(n);var t,n})):[]),whitelist:d(n.whitelist),blacklist:d(n.blacklist),root:h(n.root,e),combineWithinSelector:!!n.combineWithinSelector,combineBetweenSelectors:!!n.combineBetweenSelectors,includeTag:!!n.includeTag,maxCombinations:f(n.maxCombinations),maxCandidates:f(n.maxCandidates)};var o}(i[0],t),c=null!==(o=l.root)&&void 0!==o?o:y(i[0]);let a="",u=c;function g(){return function(e,t,n="",o){if(0===e.length)return null;const r=[e.length>1?e:[],...b(e,t).map((e=>[e]))];for(const e of r){const r=Y(e,t,n,o);if(r)return{foundElements:e,selector:r}}return null}(i,u,a,l)}let m=g();for(;m;){const{foundElements:e,selector:t}=m;if(_(i,t,c))return t;u=e[0],a=t,m=g()}return i.length>1?i.map((e=>X(e,l))).join(N):function(e){return e.map(J).join(N)}(i)}const K=X;return t})()}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var o={};(()=>{"use strict";var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(50),r=n(679);n(324);class i{constructor(){this._headingTagNames=["h1","h2","h3","h4","h5","h6"],this._activeLocationId="activeLocation",this._locationTag="span",this._documentRange=document.createRange()}setLocation(e,t){var n,o,r,i,l,s,c,a,d,u;this._debugLog(e);try{if(null==e)return void this._debugLog("No locator set");if(this._removeLocation(),this._setLocation(e,t),this._isComicBook()){const t=null!==(s=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:null===(l=null===(i=null===(r=null==e?void 0:e.locations)||void 0===r?void 0:r.domRange)||void 0===i?void 0:i.start)||void 0===l?void 0:l.cssSelector)&&void 0!==s?s:null===(d=null===(a=null===(c=null==e?void 0:e.locations)||void 0===c?void 0:c.domRange)||void 0===a?void 0:a.end)||void 0===d?void 0:d.cssSelector;if(null==t)return void this._errorLog("Css selector not set!");const h=this._getDurationFragment(null===(u=null==e?void 0:e.locations)||void 0===u?void 0:u.fragments);if(null==h)return void this._errorLog("Duration not set!");window.GotoComicFrame(t,1e3*h)}}catch(e){this._errorLog(e)}}scrollToLocations(e,t,n){try{const o=this._processLocations(e);if(null!=o)return this._scrollToProcessedRange(o,t,n),!0;const r=e.progression;if(null!=r)return null===readium||void 0===readium||readium.scrollToPosition(r),!0;this._debugLog("ScrollToLocations: Unknown range",e)}catch(e){this._errorLog(e)}return!1}isLocatorVisible(e){var t,n,o;this._debugLog(e);try{const r=e.locations,i=null!==(t=r.cssSelector)&&void 0!==t?t:null===(o=null===(n=r.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;if(this._isComicBook()){const e=null!=document.querySelector(i);return this._debugLog("Comic book",r,{found:e,selector:i}),e}const l=this._processLocations(r);return null==l?(this._debugLog("isLocatorVisible: Unknown range",r),!1):this._isProcessedRangeVisible(l)&&!!document.querySelector(`${i} #${this._activeLocationId}`)}catch(e){return this._errorLog(e),!0}}getLocatorFragments(e,t){var n,o,r,i;try{const l=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:this._findFirstVisibleCssSelector();if(null==l||!(null==l?void 0:l.length))return this._debugLog("getLocatorFragments: selector not found, returning locator from args"),e;const s=[...this._getPageFragments(t),...this._getTocFragments(l),...this._getPhysicalPageFragments(l)];return Object.assign(Object.assign({},e),{locations:Object.assign(Object.assign({cssSelector:l},e.locations),{fragments:[...null!==(i=null===(r=e.locations)||void 0===r?void 0:r.fragments)&&void 0!==i?i:[],...s]})})}catch(t){return this._errorLog(t),e}}_isComicBook(){try{return!!(null===comicBookPage||void 0===comicBookPage?void 0:comicBookPage.isComicBook())}catch(e){return!1}}_isTextNode(e){const t=e.nodeType;return 3===t||4===t}_clamp(e,t,n){return Math.min(Math.max(e,t),n)}_findAndSplitOffset(e,t){return function e(n){if(3===n.nodeType||4===n.nodeType){const e=n;if(t<=0)return e;const o=e.length;if(t=0;){if(""!==n[t].trim())return{node:e,charOffset:t};--t}t=void 0}e=this._previousNode(e)}}_findNonWhitespace(e,t){var n;return null!==(n=this._findNonWhitespaceBackward(e,t))&&void 0!==n?n:this._findNonWhitespaceForward(e,t)}_setLocation(e,t){var n,o,r,i,l,s,c,a,d,u,h;const f=readium.link;this._debugLog("create:",e,f);const g=e.locations,m=null!==(n=g.cssSelector)&&void 0!==n?n:null===(r=null===(o=null==g?void 0:g.domRange)||void 0===o?void 0:o.start)||void 0===r?void 0:r.cssSelector;if(!m)return void this._errorLog("Start css selector not found");const v=document.querySelector(m);if(!v)return void this._errorLog("Start parent not found");const _=null!==(l=null===(i=null==g?void 0:g.domRange)||void 0===i?void 0:i.end.cssSelector)&&void 0!==l?l:m,p=_===m?v:document.querySelector(_);if(!p)return void this._errorLog("End parent not found");const b=null===(c=null===(s=null==g?void 0:g.domRange)||void 0===s?void 0:s.start)||void 0===c?void 0:c.charOffset,y=null===(d=null===(a=null==g?void 0:g.domRange)||void 0===a?void 0:a.end)||void 0===d?void 0:d.charOffset;if(!b&&!y&&t)return void this._wrapWithLocationElement(v);const N=null!==(u=this._findAndSplitOffset(v,b))&&void 0!==u?u:this._nextNodeNotChild(v),S=null!==(h=this._findAndSplitOffset(p,y))&&void 0!==h?h:this._nextNodeNotChild(p),E=new Array;for(let e=N;e&&e!==S;e=this._nextNode(e))this._isTextNode(e)&&E.push(e);for(const e of E){const t=this._setLocationElement();t.appendChild(e.cloneNode(!0)),e.replaceWith(t)}}_removeLocation(){this._debugLog("Remove old location");const e=document.querySelectorAll(`#${this._activeLocationId}`);null==e||e.forEach((e=>{if(this._isAndroid())return void e.removeAttribute("id");const t=e.parentNode;e.replaceWith(...e.childNodes),t.normalize()}))}_safeVisibleRect(e){const{innerWidth:t,innerHeight:n}=window;return e?{left:0,top:.05*n,right:t,bottom:.95*n}:{left:0,top:0,right:t,bottom:n}}*_descendentTextNodes(e){if(this._isTextNode(e))yield e;else for(const t of e.childNodes)yield*this._descendentTextNodes(t)}_findTextPosition(e,t){if(null!=e)if(t<0||isNaN(t))this._errorLog(`findTextPosition: invalid charOffset, node=${e.nodeValue}, charOffset=${t}`);else{if(0===t)return{node:e,charOffset:t};for(const n of this._descendentTextNodes(e)){const e=n.length;if(t<=e)return{node:n,charOffset:t};t-=e}this._errorLog(`findTextPosition: failed, node=${this._debugNode(e)}, charOffset=${t}`)}else this._errorLog(`findTextPosition: no node, charOffset=${t}`)}_processDomRange(e){const{start:t,end:n}=e,{cssSelector:o,charOffset:r}=t,{cssSelector:i,charOffset:l}=null!=n&&void 0!==n?n:t,s=document.querySelector(o),c=this._findTextPosition(s,null!=r?r:0);if(null==c)return void this._errorLog(`DomRange bad start, selector=${o}`);const a=i===o?s:document.querySelector(i),d=this._findTextPosition(a,null!=l&&void 0!==l?l:0);if(null!=d){try{this._documentRange.setStart(c.node,c.charOffset)}catch(e){this._errorLog(`${this._debugNode(c.node)}, ${c.charOffset}`,e),this._documentRange.setStartAfter(c.node)}try{this._documentRange.setEnd(d.node,d.charOffset)}catch(e){this._errorLog(`${this._debugNode(d.node)}, ${d.charOffset}`,e),this._documentRange.setEndAfter(d.node)}if(0===this._documentRange.getClientRects().length){const e=this._findNonWhitespace(c.node,c.charOffset);if(null==e)return void this._errorLog("Couldn't find any non-whitespace characters in the document!'");const{node:t,charOffset:n}=e;this._documentRange.setStart(t,n),this._documentRange.setEnd(t,n+1)}return this._documentRange}this._errorLog(`DomRange bad end, selector=${i}`)}_processCssSelector(e){const t=document.querySelector(e);if(null!=t)return"none"===window.getComputedStyle(t).display&&(t.style.display=this._isPageBreakElement(t)?"flex":"block"),this._documentRange.selectNode(t),this._documentRange;this._errorLog(`processCssSelector: error: node not found ${e}`)}_processLocations(e){var t,n,o;if(null==e)return void this._errorLog("location not set");if(e.domRange)return this._processDomRange(e.domRange);const r=null!==(t=e.cssSelector)&&void 0!==t?t:null===(o=null===(n=e.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;return r?this._processCssSelector(r):void 0}_scrollToProcessedRange(e,t,n){!n&&this._isProcessedRangeVisible(e)||this._scrollToBoundingClientRect(e,t)}_scrollToBoundingClientRect(e,t){const{top:n,right:o,bottom:r,left:i}=e.getBoundingClientRect();if(0===n&&0===o&&0===r&&0===i)return void this._debugLog("scrollToBoundingClientRect: Scrolling to defective bounding rect, abort! ",e.getClientRects(),e.getClientRects().length);const{scrollLeft:l,scrollWidth:s,scrollTop:c,scrollHeight:a}=document.scrollingElement;if(t){const{top:e,bottom:o}=this._safeVisibleRect(t);if(no){const t=this._clamp((c+n-e)/a,0,1);null===readium||void 0===readium||readium.scrollToPosition(t)}}else{const e=(l+.5*(i+o))/s;null===readium||void 0===readium||readium.scrollToPosition(e)}}_isProcessedRangeVisible(e){const{innerWidth:t,innerHeight:n}=window,{top:o,right:r,bottom:i,left:l}=e.getBoundingClientRect();return oe.includes("duration=")));if(!t)return void this._errorLog("Duration fragment not found.");const n=/duration=(\d+(?:\.\d+)?)/.exec(t);return n?(this._debugLog("Duration fragment:",n[1]),parseFloat(n[1])):void this._errorLog("Invalid duration format.")}catch(e){return void this._errorLog("Could not retrieve duration fragment!")}}_getTocFragments(e){var t;try{const n=null===(t=this._findPrecedingAncestorSiblingHeadings(e)[0])||void 0===t?void 0:t.id;return null==n?[]:[`toc=${n}`]}catch(e){return this._errorLog(e),[]}}_getPhysicalPageFragments(e){try{const t=this._findCurrentPhysicalPage(e);return null==t?[]:[`physicalPage=${t}`]}catch(t){return this._errorLog(`Selector:${e} -- ${t}`),[]}}_findPrecedingAncestorSiblingHeadings(e){var t,n;const o=document.querySelector(e),r=null!==(t=null==o?void 0:o.querySelectorAll(this._headingTagNames.join(","))[0])&&void 0!==t?t:o;if(null==r)return;if(!this._allHeadings){const e=Array.from(window.document.querySelectorAll(this._headingTagNames.join(",")));for(const t of e)if(t){const e=t,n=e.textContent||e.getAttribute("title")||e.getAttribute("aria-label");let o=e.getAttribute("id");if(!o){let t,n=e;for(;(t=n.parentNode)&&(null==t?void 0:t.nodeType)===Node.ELEMENT_NODE&&t.firstElementChild===n;){const e=t.getAttribute("id");if(e){o=e;break}n=t}}const r={element:e,id:o||null,level:parseInt(e.localName.substring(1),10),text:n};this._allHeadings||(this._allHeadings=[]),this._allHeadings.push(r)}this._allHeadings||(this._allHeadings=[])}let i;for(let e=this._allHeadings.length-1;e>=0;e--){const t=this._allHeadings[e],n=r.compareDocumentPosition(t.element);(0===n||n&Node.DOCUMENT_POSITION_PRECEDING||n&Node.DOCUMENT_POSITION_CONTAINS)&&(i||(i=[]),(null==t?void 0:t.id)&&i.push({id:t.id,level:t.level,text:t.text}))}if(null==i?void 0:i.length)return i;const l=null!==(n=o.closest("section"))&&void 0!==n?n:o.closest("body");return l?[{id:l.id,level:0,text:l.innerText}]:void 0}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(e){var t;let n=document.querySelector(e);if(null!=n){if(this._isPageBreakElement(n))return this._getPhysicalPageIndexFromElement(n);for(;n.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(n);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(n)));t>=0;t--){const n=e[t],o=this._findPhysicalPageIndex(n);if(null!=o)return o}if(n=n.parentNode,null==n||"body"===n.nodeName.toLowerCase())return null===(t=document.querySelector("head [name='webpub:currentPage']"))||void 0===t?void 0:t.getAttribute("content")}}}_findFirstVisibleCssSelector(){return this._getCssSelector(this._getFirstVisibleElement())}_getCssSelector(e){var n,o;try{const r=(0,t.getCssSelector)(e,{root:document.querySelector("body")}),i=null!==(o=null===(n=null==r?void 0:r.replace(":root > :nth-child(2)","body"))||void 0===n?void 0:n.trim())&&void 0!==o?o:"body";return this._debugLog(i),i}catch(e){return this._errorLog(e),"body"}}_getFirstVisibleElement(){const e=this._findFirstVisibleElement(document.body);return this._debugLog("First visible element:",{tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className}),e}_findFirstVisibleElement(e){const t={tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className};for(const n of e.children){const o={tagName:n.nodeName.toLocaleLowerCase(),id:n.id,className:n.className};if(this._isElementVisible(n))if(this._shouldIgnoreElement(n))this._debugLog("Element is ignored - continue",o);else{if(n.id.includes(`${this._activeLocationId}`))return this._debugLog("Child is an active location element, return closest element with id",{childData:o,nodeData:t}),e.id?e:this._findClosestElementWithId(n);if(n.hasChildNodes())return this._debugLog("Loop into children",o),this._findFirstVisibleElement(n);if(!n.id)return this._debugLog("Element has no ID attribute - return closest element with id",o),e.id?e:this._findClosestElementWithId(n)}}return this._debugLog("return:",t),e}_findClosestElementWithId(e){let t=e.parentElement;for(;null!==t;){if(t.id)return t;t=t.parentElement}return this._debugLog("No element with id attr found!"),e}_isElementVisible(e,t=!1){if(null===readium||void 0===readium?void 0:readium.isFixedLayout)return!0;if(e===document.body||e===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;const n=e.getBoundingClientRect();return t?this._isScrollModeEnabled()?n.top>=0&&n.top<=document.documentElement.clientHeight:n.left>=1:this._isScrollModeEnabled()?n.bottom>0&&n.top0&&n.lefte instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_debugNode(e){var t,n;if(e instanceof Node){return(new XMLSerializer).serializeToString(e)}if("innerHTML"in e||"textContent"in e){const o=e;return null!==(n=null!==(t=o.innerHTML)&&void 0!==t?t:o.textContent)&&void 0!==n?n:"?"}}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function l(){window.epubPage||((0,r.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",l),window.epubPage=new i)}e.EpubPage=i,"loading"!==document.readyState?window.setTimeout(l):document.addEventListener("DOMContentLoaded",l)})(),epub=o})(); \ No newline at end of file diff --git a/flutter_readium/lib/reader_channel.dart b/flutter_readium/lib/reader_channel.dart index 0f65ff68..599ede44 100644 --- a/flutter_readium/lib/reader_channel.dart +++ b/flutter_readium/lib/reader_channel.dart @@ -10,7 +10,6 @@ enum _ReaderChannelMethodInvoke { goLeft, goRight, getCurrentLocator, - getLocatorFragments, setLocation, isLocatorVisible, dispose, @@ -50,20 +49,6 @@ class ReadiumReaderChannel extends MethodChannel { return _invokeMethod(_ReaderChannelMethodInvoke.goRight, animated); } - /// Get locator fragments for the given [locator]. - Future getLocatorFragments(final Locator locator) { - R2Log.d('locator: ${locator.toString()}'); - - return _invokeMethod( - _ReaderChannelMethodInvoke.getLocatorFragments, - json.encode(locator.toJson()), - ).then((final value) => Locator.fromJson(json.decode(value))).onError((final error, final _) { - R2Log.e(error ?? 'Unknown Error'); - - throw ReadiumException('getLocatorFragments failed $locator'); - }); - } - /// Set the current location to the given [locator]. Future setLocation(final Locator locator, final bool isAudioBookWithText) async => _invokeMethod(_ReaderChannelMethodInvoke.setLocation, [json.encode(locator), isAudioBookWithText]); diff --git a/flutter_readium/lib/reader_widget.dart b/flutter_readium/lib/reader_widget.dart index 48415b5a..64bbd5f4 100644 --- a/flutter_readium/lib/reader_widget.dart +++ b/flutter_readium/lib/reader_widget.dart @@ -155,15 +155,6 @@ class _ReadiumReaderWidgetState extends State implements Re } } - @override - Future getLocatorFragments(final Locator locator) async { - R2Log.d('getLocatorFragments: $locator'); - - await _awaitNativeViewReady(); - - return await _channel?.getLocatorFragments(locator); - } - @override Future getCurrentLocator() async { R2Log.d('GetCurrentLocator()'); diff --git a/flutter_readium/lib/reader_widget_web.dart b/flutter_readium/lib/reader_widget_web.dart index 944fa016..f746a2b2 100644 --- a/flutter_readium/lib/reader_widget_web.dart +++ b/flutter_readium/lib/reader_widget_web.dart @@ -75,13 +75,6 @@ class _ReadiumReaderWidgetState extends State implements Re JsPublicationChannel.goRight(); } - @override - // ignore: prefer_expression_function_bodies - Future getLocatorFragments(final Locator locator) async { - // Implement this method if needed - return null; - } - @override Future skipToPrevious({final bool animated = true}) async { R2Log.d('skipToPrevious not implemented in web version'); diff --git a/flutter_readium_platform_interface/lib/src/extensions/readium_string_extensions.dart b/flutter_readium_platform_interface/lib/src/extensions/readium_string_extensions.dart index 4fbeccd9..cb4c6522 100644 --- a/flutter_readium_platform_interface/lib/src/extensions/readium_string_extensions.dart +++ b/flutter_readium_platform_interface/lib/src/extensions/readium_string_extensions.dart @@ -53,6 +53,4 @@ extension ReadiumStringExtension on String { /// /// Returns `null` if path couldn't be retrieved from uri. String? get path => Uri.tryParse(this)?.path; - - String stripLeadingSlash() => startsWith('/') ? substring(1) : this; } diff --git a/flutter_readium_platform_interface/lib/src/extensions/strings.dart b/flutter_readium_platform_interface/lib/src/extensions/strings.dart index f950111c..3504e9c1 100644 --- a/flutter_readium_platform_interface/lib/src/extensions/strings.dart +++ b/flutter_readium_platform_interface/lib/src/extensions/strings.dart @@ -105,6 +105,13 @@ extension StringExtension on String { /// /// @sample samples.text.Strings.take String takeWhile(bool Function(String) predicate) => characters.takeWhile(predicate).string; + + (String, String?) splitPathAndFragment() { + final components = split('#'); + final path = components.firstOrDefault(this); + final fragment = (components.length > 1 && components[1].isNotEmpty) ? components[1] : null; + return (path, fragment); + } } extension StringHashExtension on String { diff --git a/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart b/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart index 642e5c51..42ffabbd 100644 --- a/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart +++ b/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart @@ -19,9 +19,6 @@ abstract class ReadiumReaderWidgetInterface { /// Gets the current Navigator's locator. Future getCurrentLocator(); - /// Get a locator with relevant fragments - Future getLocatorFragments(final Locator locator); - /// Set EPUB preferences Future setEPUBPreferences(EPUBPreferences preferences); diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/link.dart b/flutter_readium_platform_interface/lib/src/shared/publication/link.dart index 0124452b..7614002f 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/link.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/link.dart @@ -10,11 +10,7 @@ import 'package:fimber/fimber.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; -import '../../utils/href.dart'; -import '../../utils/jsonable.dart'; -import '../../utils/uri_template.dart'; -import '../mediatype/mediatype.dart'; -import 'properties.dart'; +import '../../../flutter_readium_platform_interface.dart'; export 'link_list_extension.dart'; @@ -123,11 +119,11 @@ class Link with EquatableMixin implements JSONable { /// given collection role. final List children; - List get _hrefParts => href.split('#'); + (String, String?) get _hrefParts => href.splitPathAndFragment(); - String get hrefPart => _hrefParts[0]; + String get hrefPart => _hrefParts.$1; - String? get elementId => (_hrefParts.length > 1) ? _hrefParts[1] : null; + String? get elementId => _hrefParts.$2; Link copyWith({ String? id, diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart b/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart index 7b99d8b9..62fdd5d6 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/locator.dart @@ -4,7 +4,6 @@ import 'dart:convert'; -import 'package:dartx/dartx.dart'; import 'package:dfunc/dfunc.dart'; import 'package:equatable/equatable.dart'; import 'package:fimber/fimber.dart'; @@ -12,6 +11,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; import '../../extensions/readium_string_extensions.dart'; +import '../../extensions/strings.dart'; import '../../utils/additional_properties.dart'; import '../../utils/jsonable.dart'; import '../../utils/take.dart'; @@ -383,11 +383,10 @@ class LocatorText with EquatableMixin implements JSONable { extension LinkLocator on Link { /// Creates a [Locator] from a reading order [Link]. Locator toLocator() { - final components = href.split('#'); - final fragment = (components.length > 1 && components[1].isNotEmpty) ? components[1] : null; + final (hrefPath, fragment) = href.splitPathAndFragment(); return Locator( - href: components.firstOrDefault(href).stripLeadingSlash(), + href: hrefPath, type: type ?? '', title: title, text: LocatorText(), diff --git a/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart b/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart index e3960017..ef964773 100644 --- a/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart +++ b/flutter_readium_platform_interface/lib/src/shared/publication/publication.dart @@ -145,24 +145,21 @@ class Publication with EquatableMixin implements JSONable { } Locator? locatorFromLink(final Link link, {final MediaType? typeOverride}) { - final href = link.href; - final hashIndex = href.indexOf(_hrefEnd); - final hrefHead = hashIndex == -1 ? href : href.substring(0, hashIndex); - final hrefTail = hashIndex == -1 ? null : href.substring(hashIndex + 1); - final resourceLink = linkWithHref(hrefHead); + final (href, fragments) = link.href.splitPathAndFragment(); + final resourceLink = linkWithHref(href); final type = resourceLink?.type ?? typeOverride?.name; final linkIndex = resourceLink == null ? -1 : readingOrder.indexOf(resourceLink); return type == null ? null : Locator( - href: hrefHead.stripLeadingSlash(), + href: href, type: type, title: resourceLink!.title ?? link.title, text: LocatorText(), locations: Locations( - cssSelector: hrefTail != null && hrefTail.isNotEmpty ? '#$hrefTail' : null, - fragments: hrefTail == null ? [] : [hrefTail], - progression: hrefTail == null ? 0 : null, + cssSelector: fragments != null && fragments.isNotEmpty ? '#$fragments' : null, + fragments: fragments == null ? [] : [fragments], + progression: fragments == null ? 0 : null, position: linkIndex == -1 ? null : linkIndex + 1, ), ); From d3f980dd4310a434f2102e2f6f33f467a0cbbc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Thu, 26 Feb 2026 16:25:40 +0100 Subject: [PATCH 02/19] chor: remove getCurrentLocator - Use onTextLocatorChanged instead --- .../flutter_readium/ReadiumReaderWidget.kt | 4 ---- .../flutter_readium/ReadiumReaderView.swift | 20 ++----------------- flutter_readium/lib/reader_channel.dart | 7 ------- flutter_readium/lib/reader_widget.dart | 15 +++----------- flutter_readium/lib/reader_widget_web.dart | 6 ------ .../src/reader/reader_widget_interface.dart | 3 --- 6 files changed, 5 insertions(+), 50 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt index 096ebe14..e32d6ee7 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt @@ -348,10 +348,6 @@ class ReadiumReaderWidget( result.success(null) } - "getCurrentLocator" -> { - result.success(ReadiumReader.epubCurrentLocator?.let { jsonEncode(it.toJSON()) }) - } - else -> { Log.e(TAG, "Unhandled call ${call.method}") result.notImplemented() diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift index 9069dcaa..58a57391 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift @@ -91,10 +91,10 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele config.preloadPreviousPositionCount = 2 config.preloadNextPositionCount = 4 config.debugState = true - + // TODO: Use experimentalPositioning for now. It places highlights on z-index -1 behind text, instead of in-front. config.decorationTemplates = HTMLDecorationTemplate.defaultTemplates(alpha: 1.0, experimentalPositioning: true) - + // TODO: This is a PoC for adding custom editing actions, like user highlights. It should be configurable from Flutter. config.editingActions = [.lookup, .translate, EditingAction(title: "Custom Highlight Action", action: #selector(onCustomEditingAction))] @@ -409,22 +409,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele } } break - case "getCurrentLocator": - let args = call.arguments as? String ?? "null" - print(TAG, "onMethodCall[currentLocator] args = \(args)") - Task.detached(priority: .high) { [isVerticalScroll] in - let json = await self.readiumViewController.currentLocation?.jsonString ?? nil - if (json == nil) { - await MainActor.run() { - return result(nil) - } - } - let data = await self.getLocatorFragments(json!, isVerticalScroll) - await MainActor.run() { - return result(data?.jsonString) - } - } - break case "isLocatorVisible": let args = call.arguments as! String print(TAG, "onMethodCall[isLocatorVisible] locator = \(args)") diff --git a/flutter_readium/lib/reader_channel.dart b/flutter_readium/lib/reader_channel.dart index 599ede44..a95b8634 100644 --- a/flutter_readium/lib/reader_channel.dart +++ b/flutter_readium/lib/reader_channel.dart @@ -9,7 +9,6 @@ enum _ReaderChannelMethodInvoke { go, goLeft, goRight, - getCurrentLocator, setLocation, isLocatorVisible, dispose, @@ -63,12 +62,6 @@ class ReadiumReaderChannel extends MethodChannel { return await _invokeMethod(_ReaderChannelMethodInvoke.applyDecorations, [id, decorations.map((d) => d.toJson())]); } - /// Get the current locator. - Future getCurrentLocator() async => await _invokeMethod( - _ReaderChannelMethodInvoke.getCurrentLocator, - [], - ).then((locStr) => locStr != null ? Locator.fromJson(json.decode(locStr) as Map) : null); - /// Check if a locator is currently visible on screen. Future isLocatorVisible(final Locator locator) => _invokeMethod( _ReaderChannelMethodInvoke.isLocatorVisible, diff --git a/flutter_readium/lib/reader_widget.dart b/flutter_readium/lib/reader_widget.dart index 64bbd5f4..edb6d086 100644 --- a/flutter_readium/lib/reader_widget.dart +++ b/flutter_readium/lib/reader_widget.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart' as mq show Orientation; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_readium_platform_interface/flutter_readium_platform_interface.dart'; +import 'package:flutter_readium/flutter_readium.dart'; import 'package:rxdart/rxdart.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -53,6 +53,7 @@ class _ReadiumReaderWidgetState extends State implements Re final _isReadyCompleter = Completer(); final _readium = FlutterReadiumPlatform.instance; + final FlutterReadium _flutterReadium = FlutterReadium(); mq.Orientation? _lastOrientation; late Widget _readerWidget; @@ -124,7 +125,7 @@ class _ReadiumReaderWidgetState extends State implements Re // TODO: Find a better way to do this, maybe a `lastVisibleLocator` ? if (_readium.defaultPreferences?.verticalScroll != true) { await _channel?.goRight(animated: false); - final loc = await _channel?.getCurrentLocator(); + final loc = await _flutterReadium.onTextLocatorChanged.first; currentHref = getTextLocatorHrefWithTocFragment(loc); } @@ -155,12 +156,6 @@ class _ReadiumReaderWidgetState extends State implements Re } } - @override - Future getCurrentLocator() async { - R2Log.d('GetCurrentLocator()'); - return _channel?.getCurrentLocator(); - } - @override Future setEPUBPreferences(EPUBPreferences preferences) async { _channel?.setEPUBPreferences(preferences); @@ -279,10 +274,6 @@ class _ReadiumReaderWidgetState extends State implements Re }); } - Future _awaitNativeViewReady() { - return _isReadyCompleter.future; - } - /// Gets a Locator's href with toc fragment appended as identifier String? getTextLocatorHrefWithTocFragment(Locator? locator) { if (locator == null) { diff --git a/flutter_readium/lib/reader_widget_web.dart b/flutter_readium/lib/reader_widget_web.dart index f746a2b2..54e83eb0 100644 --- a/flutter_readium/lib/reader_widget_web.dart +++ b/flutter_readium/lib/reader_widget_web.dart @@ -85,12 +85,6 @@ class _ReadiumReaderWidgetState extends State implements Re R2Log.d('skipToNext not implemented in web version'); } - @override - Future getCurrentLocator() async { - R2Log.d('getCurrentLocator not implemented in web version'); - return null; - } - @override Future setEPUBPreferences(EPUBPreferences preferences) async { R2Log.d('setEPUBPreferences not implemented in web version'); diff --git a/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart b/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart index 42ffabbd..7bd2f6a6 100644 --- a/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart +++ b/flutter_readium_platform_interface/lib/src/reader/reader_widget_interface.dart @@ -16,9 +16,6 @@ abstract class ReadiumReaderWidgetInterface { /// Skip to next chapter (toc) Future skipToNext({final bool animated = true}); - /// Gets the current Navigator's locator. - Future getCurrentLocator(); - /// Set EPUB preferences Future setEPUBPreferences(EPUBPreferences preferences); From c3c7a20ba926dd1fff8367e791623867428beafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Fri, 27 Feb 2026 16:51:37 +0100 Subject: [PATCH 03/19] feat(android): find current toc item for EPUB in both reader widget and TTS navigator --- .../nota/flutter_readium/ReadiumExtensions.kt | 76 +++++++++++++ .../dk/nota/flutter_readium/ReadiumReader.kt | 70 +++++++++++- .../flutter_readium/ReadiumReaderWidget.kt | 100 ++++-------------- .../kotlin/dk/nota/flutter_readium/Utils.kt | 10 ++ .../navigators/EpubNavigator.kt | 54 +--------- .../navigators/TTSNavigator.kt | 6 +- 6 files changed, 181 insertions(+), 135 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index fe16944f..6d5500e5 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -18,7 +18,10 @@ import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.flatten import org.readium.r2.shared.publication.html.cssSelector +import org.readium.r2.shared.publication.services.content.Content +import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingResource @@ -251,3 +254,76 @@ fun Locator.copyWithTimeFragment(time: Double): Locator { ) ) } + +/** + * Helper for getting all cssSelectors for a HTML document. + */ +suspend fun Publication.findAllCssSelectors(href: Url): List? { + if (!conformsTo(Publication.Profile.EPUB)) { + Log.d(TAG, ":findAllCssSelectors - this only works for an EPUB Profile") + return null + } + + val contentService = findService(ContentService::class) ?: run { + Log.d(TAG, ":findAllCssSelectors - no content service found") + return null + } + + val cleanHref = href.removeQuery().removeFragment() + + val ids = arrayListOf() + for (element in contentService.content(Locator(href = cleanHref, mediaType = MediaType.XHTML))) { + if (element !is Content.TextElement) { + continue + } + + if (element.locator.href.removeQuery() + .removeFragment() != cleanHref + ) { + // We iterated to the next document, stopping + break + } + + // We are only interested in #id type of cssSelectors. + val cssSelector = + element.locator.locations.cssSelector?.takeIf { it.startsWith("#") } ?: continue + + ids.add(cssSelector) + } + + return ids +} + +/** + * Find the cssSelector for a locator. If it already have one return it, otherwise we need to look it up. + */ +suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { + locator.locations.cssSelector?.takeIf { it.startsWith("#") }?.let { return it } + + val contentService = findService(ContentService::class) ?: run { + Log.d(TAG, ":findCssSelectorForLocator - no content service found") + return null + } + + val cleanHref = locator.href.removeQuery().removeFragment() + + for (element in contentService.content(locator)) { + if (element !is Content.TextElement) { + continue + } + + if (element.locator.href.removeQuery() + .removeFragment() != cleanHref + ) { + // We iterated to the next document, stopping + break + } + + val cssSelector = + element.locator.locations.cssSelector?.takeIf { it.startsWith("#") } ?: continue + + return cssSelector + } + + return null +} diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index 191292c4..2ae31d28 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -46,6 +46,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml +import org.readium.r2.shared.publication.flatten import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language @@ -398,6 +399,12 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua state[currentPublicationUrlKey] = value } + /*** + * Maps a URL to a HTML document to a list of all the ids in the document. + * This is used to find the current ToC item. + */ + private var currentPublicationContentIdsMap: MutableMap>? = null + /** * Sets the headers used in the HTTP requests for fetching publication resources, including * resources in already created `Publication` objects. @@ -579,6 +586,7 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua mainScope.async { _currentPublication?.close() _currentPublication = null + currentPublicationContentIdsMap = null ttsNavigator?.dispose() ttsNavigator = null @@ -631,6 +639,47 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua currentReaderWidget?.go(locator, true) } + /** + * Find the current table of content item from a locator. + */ + suspend fun epubFindCurrentToc(locator: Locator): Locator { + val publication = currentPublication ?: run { + Log.e(TAG, ":epubFindCurrentToc, no currentPublication") + return locator + } + + val cssSelector = publication.findCssSelectorForLocator(locator) ?: run { + Log.e(TAG, ":epubFindCurrentToc, missing cssSelector in locator") + return locator + } + + val resultLocator = locator.copyWithLocations(otherLocations = locator.locations.otherLocations + ("cssLocator" to cssSelector)) + + val contentIds = epubGetAllDocumentCssSelectors(locator.href) + val idx = contentIds.indexOf(cssSelector).takeIf { it > -1 } ?: run { + Log.d(TAG, ":epubFindCurrentToc cssSelector:${cssSelector} not found in contentIds") + return resultLocator + } + + val toc = publication.tableOfContents.flatten().filter { + it.href.resolve().removeQuery() + .removeFragment() == resultLocator.href.removeFragment().removeQuery() + }.associateBy { contentIds.indexOf("${it.href.resolve().fragment}") } + + val tocItem = toc.entries.lastOrNull { it.key <= idx }?.value + ?: toc.entries.firstOrNull()?.value ?: run { + Log.d(TAG, ":epubFindCurrentToc - no tocItem found") + return resultLocator + } + + return resultLocator.copy( + title = tocItem.title + ).copyWithLocations( + otherLocations = resultLocator.locations.otherLocations + ("toc" to tocItem.href.resolve() + .toString()) + ) + } + @OptIn(InternalReadiumApi::class) suspend fun epubEnable( initialLocator: Locator?, @@ -929,10 +978,25 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua } /** - * Get locator fragments from EPUB navigator. + * Get first visible locator from the EPUB navigator. */ - suspend fun epubGetLocatorFragments(locator: Locator): Locator? { - return epubNavigator?.getLocatorFragments(locator) + suspend fun firstVisibleElementLocator(): Locator? { + return epubNavigator?.firstVisibleElementLocator() + } + + /** + * Get all cssSelectors for an EPUB file. + */ + suspend fun epubGetAllDocumentCssSelectors(href: Url): List { + val contentIdsMap = currentPublicationContentIdsMap ?: mutableMapOf() + currentPublicationContentIdsMap = contentIdsMap + + val cleanHref = href.removeQuery().removeFragment() + return contentIdsMap.getOrPut(cleanHref) { + currentPublication?.findAllCssSelectors( + cleanHref + ) ?: listOf() + } } /** diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt index e32d6ee7..187bdba0 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt @@ -41,8 +41,8 @@ class ReadiumReaderWidget( creationParams: Map, messenger: BinaryMessenger, attrs: AttributeSet? = null -) : PlatformView, MethodChannel.MethodCallHandler, - EpubReaderFragment.Listener, EpubNavigator.VisualListener { +) : PlatformView, MethodChannel.MethodCallHandler, EpubReaderFragment.Listener, + EpubNavigator.VisualListener { private val channel: ReadiumReaderChannel @@ -94,8 +94,8 @@ class ReadiumReaderWidget( init { Log.d(TAG, "::init") - @Suppress("UNCHECKED_CAST") - val initPrefsMap = creationParams["preferences"] as Map? + @Suppress("UNCHECKED_CAST") val initPrefsMap = + creationParams["preferences"] as Map? val publication = ReadiumReader.currentPublication val locatorString = creationParams["initialLocator"] as String? val allowScreenReaderNavigation = creationParams["allowScreenReaderNavigation"] as Boolean? @@ -103,8 +103,7 @@ class ReadiumReaderWidget( if (locatorString == null) null else Locator.fromJSON(jsonDecode(locatorString) as JSONObject) val initialPreferences = if (initPrefsMap == null) EpubPreferences() else epubPreferencesFromMap( - initPrefsMap, - null + initPrefsMap, null ) Log.d(TAG, "publication = $publication") @@ -126,8 +125,7 @@ class ReadiumReaderWidget( // This can be toggled back on via the 'allowScreenReaderNavigation' creation param. // See issue: https://notalib.atlassian.net/browse/NOTA-9828 if (allowScreenReaderNavigation != true) { - layout.importantForAccessibility = - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + layout.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS } // Remove existing fragment if any (this is to avoid crashing on restore). @@ -212,16 +210,12 @@ class ReadiumReaderWidget( private suspend fun emitOnPageChanged(locator: Locator) { try { - val locatorWithFragments = ReadiumReader.epubGetLocatorFragments(locator) - if (locatorWithFragments == null) { - Log.e(TAG, "emitOnPageChanged: window.epubPage.getVisibleRange failed!") - return - } - - channel.onPageChanged(locatorWithFragments) - ReadiumReader.emitTextLocatorUpdate(locatorWithFragments) + val emittingLocator = ReadiumReader.epubFindCurrentToc(locator) + channel.onPageChanged(emittingLocator) + ReadiumReader.emitTextLocatorUpdate(emittingLocator) + Log.d(TAG, "emitOnPageChanged: emitted $emittingLocator") } catch (e: Exception) { - Log.e(TAG, "emitOnPageChanged: window.epubPage.getVisibleRange failed! $e") + Log.e(TAG, "emitOnPageChanged: failed! $e") } } @@ -229,15 +223,6 @@ class ReadiumReaderWidget( channel.onExternalLinkActivated(url) } - private suspend fun setLocation( - locator: Locator, - isAudioBookWithText: Boolean - ) { - val json = locator.toJSON().toString() - Log.d(TAG, "::scrollToLocations: Go to locations $json") - evaluateJavascript("window.epubPage.setLocation($json, $isAudioBookWithText);") - } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { // TODO: To be safe we're doing everything on the Main thread right now. // Could probably optimize by using .IO and then change to Main @@ -247,11 +232,15 @@ class ReadiumReaderWidget( when (call.method) { "setPreferences" -> { try { - @Suppress("UNCHECKED_CAST") - val prefsMap = call.arguments as? Map ?: run { - result.error("FlutterReadium", "Failed to set preferences", "Invalid argument") - return@launch - } + @Suppress("UNCHECKED_CAST") val prefsMap = + call.arguments as? Map ?: run { + result.error( + "FlutterReadium", + "Failed to set preferences", + "Invalid argument" + ) + return@launch + } setPreferencesFromMap(prefsMap) result.success(null) @@ -268,13 +257,11 @@ class ReadiumReaderWidget( if (locatorJson.optString("type") == "") { locatorJson.put("type", " ") Log.e( - TAG, - "Got locator with empty type! This shouldn't happen. $locatorJson" + TAG, "Got locator with empty type! This shouldn't happen. $locatorJson" ) } val locator = Locator.fromJSON(locatorJson)!! ReadiumReader.epubGoToLocator(locator, animated) - setLocation(locator, isAudioBookWithText) result.success(null) } @@ -290,53 +277,12 @@ class ReadiumReaderWidget( result.success(null) } - "setLocation" -> { - val args = call.arguments as List<*> - val locatorJson = JSONObject(args[0] as String) - val isAudioBookWithText = args[1] as Boolean - val locator = Locator.fromJSON(locatorJson)!! - setLocation(locator, isAudioBookWithText) - result.success(null) - } - - "isLocatorVisible" -> { - val args = call.arguments as String - val locatorJson = JSONObject(args) - val locator = Locator.fromJSON(locatorJson)!! - var visible = locator.href == ReadiumReader.epubCurrentLocator?.href - if (visible) { - val jsonRes = - evaluateJavascript("window.epubPage.isLocatorVisible($args);") - ?: "false" - try { - visible = jsonDecode(jsonRes) as Boolean - } catch (e: Error) { - Log.e(TAG, "::isLocatorVisible - invalid response:$jsonRes - e:$e") - visible = false - } - } - result.success(visible) - } - - "getLocatorFragments" -> { - val args = call.arguments as String? - Log.d(TAG, "::====== $args") - val locatorJson = JSONObject(args!!) - Log.d(TAG, "::====== $locatorJson") - - val locator = - ReadiumReader.epubGetLocatorFragments(Locator.fromJSON(locatorJson)!!) - Log.d(TAG, "::====== $locator") - - result.success(jsonEncode(locator?.toJSON())) - } - "applyDecorations" -> { val args = call.arguments as List<*> val groupId = args[0] as String - @Suppress("UNCHECKED_CAST") - val decorationListStr = args[1] as List> + @Suppress("UNCHECKED_CAST") val decorationListStr = + args[1] as List> val decorations = decorationListStr.mapNotNull { decorationFromMap(it) } ReadiumReader.applyDecorations(decorations, groupId) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/Utils.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/Utils.kt index 59cc76a4..8cf1ea4e 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/Utils.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/Utils.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.html.cssSelector @@ -90,3 +91,12 @@ fun canScroll(locations: Locator.Locations) = fun MutableStateFlow.update(new: T) { if (this.value != new) this.value = new } + +@Throws(JSONException::class) +fun JSONArray.toList(): List { + val list = mutableListOf() + for (i in 0 until this.length()) { + list.add(this[i]) + } + return list +} diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt index 4f01acf7..1cd11fb9 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt @@ -6,7 +6,6 @@ import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.commitNow import dk.nota.flutter_readium.ReadiumReaderWidget.Companion.NAVIGATOR_FRAGMENT_TAG -import dk.nota.flutter_readium.canScroll import dk.nota.flutter_readium.fragments.EpubReaderFragment import dk.nota.flutter_readium.jsonDecode import dk.nota.flutter_readium.models.EpubReaderViewModel @@ -102,11 +101,6 @@ class EpubNavigator : BaseNavigator, EpubReaderFragment.Listener { */ private var editor: EpubPreferencesEditor? = null - /** - * Pending scroll target to be applied when the page is loaded. - */ - var pendingScrollToLocations: Locator.Locations? = null - /** * Current EPUB preferences. */ @@ -134,11 +128,6 @@ class EpubNavigator : BaseNavigator, EpubReaderFragment.Listener { } override suspend fun initNavigator() { - pendingScrollToLocations = - initialLocator?.locations?.let { locations -> - if (canScroll(locations)) locations else null - } - epubNavigator = EpubReaderFragment().apply { vm = EpubReaderViewModel().apply { navigatorFactory = EpubNavigatorFactory(publication) @@ -254,17 +243,6 @@ class EpubNavigator : BaseNavigator, EpubReaderFragment.Listener { Log.d(TAG, "::onPageLoaded") visualListener.onPageLoaded() - pendingScrollToLocations?.let { locations -> - Log.d(TAG, "::onPageLoaded - pendingScrollToLocations: $locations") - - mainScope.async { - scrollToLocations(locations, toStart = true) - } - - pendingScrollToLocations = null - - } - notifyIsReady() } @@ -399,41 +377,11 @@ class EpubNavigator : BaseNavigator, EpubReaderFragment.Listener { }.await() } - private suspend fun scrollToLocations( - locations: Locator.Locations, - toStart: Boolean - ) { - val json = locations.toJSON().toString() - Log.d(TAG, "::scrollToLocations: Go to locations $json, toStart: $toStart") - evaluateJavascript("window.epubPage.scrollToLocations($json,$isVerticalScroll,$toStart);") - } - /** * Go to a specific locator in the EPUB navigator, this scrolls to the locator position if needed. */ suspend fun goToLocator(locator: Locator, animated: Boolean) { - mainScope.async { - val locations = locator.locations - val shouldScroll = canScroll(locations) - val locatorHref = locator.href - val currentHref = currentLocator?.value?.href - val shouldGo = currentHref?.isEquivalent(locatorHref) == false - - // TODO: Figure out why we can't just use rely on Readium's own go-function to scroll - // the locator. - if (shouldGo) { - Log.d(TAG, "::goToLocator: Go to $locatorHref from $currentHref") - pendingScrollToLocations = locations - go(locator, animated) - } else if (!shouldScroll) { - Log.w(TAG, "::goToLocator: Already at $locatorHref, no scroll target, go to start") - scrollToLocations(Locator.Locations(progression = 0.0), true) - } else { - Log.d(TAG, "::goToLocator: Already at $locatorHref, scroll to position") - - scrollToLocations(locations, false) - } - }.await() + go(locator, animated) } companion object { diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TTSNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TTSNavigator.kt index 0748c538..9ad0f975 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TTSNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TTSNavigator.kt @@ -301,8 +301,10 @@ class TTSNavigator( .throttleLatest(100.milliseconds) .distinctUntilChanged() .onEach { locator -> - onCurrentLocatorChanges(locator) - state[currentTimebasedLocatorKey] = locator + val emittingLocator = + ReadiumReader.epubFindCurrentToc(locator) + onCurrentLocatorChanges(emittingLocator) + state[currentTimebasedLocatorKey] = emittingLocator } .launchIn(mainScope) .let { jobs.add(it) } From 68378e99345b5fa546fac2ad7609947a4d2f128e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Fri, 27 Feb 2026 17:01:32 +0100 Subject: [PATCH 04/19] chor: removed unused/unneeded functions from EpubPage.ts --- .../dk/nota/flutter_readium/ReadiumReader.kt | 3 +- .../navigators/EpubNavigator.kt | 24 - .../assets/_helper_scripts/package-lock.json | 121 ++- .../assets/_helper_scripts/package.json | 1 - .../assets/_helper_scripts/src/EpubPage.ts | 921 +----------------- flutter_readium/assets/helpers/epub.js | 2 +- flutter_readium/example/ios/Podfile.lock | 7 - .../Flutter/GeneratedPluginRegistrant.swift | 2 - flutter_readium/example/pubspec.lock | 88 +- .../flutter_readium/ReadiumReaderView.swift | 29 - flutter_readium/lib/reader_channel.dart | 21 +- 11 files changed, 125 insertions(+), 1094 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index 2ae31d28..f1796de4 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -765,7 +765,8 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua ) }, { language, availableVoices -> null }, - AndroidTtsPreferences())?.voices ?: setOf() + AndroidTtsPreferences() + )?.voices ?: setOf() } fun ttsGetPreferences(): FlutterTtsPreferences? { diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt index 1cd11fb9..1ee35d5d 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt @@ -332,30 +332,6 @@ class EpubNavigator : BaseNavigator, EpubReaderFragment.Listener { navigatorStarted.first { it } } - suspend fun getLocatorFragments(locator: Locator): Locator? { - val json = - evaluateJavascript("window.epubPage.getLocatorFragments(${locator.toJSON()}, $isVerticalScroll)") - try { - if (json == null || json == "null" || json == "undefined") { - Log.e( - TAG, - "getLocatorFragments: window.epubPage.getVisibleRange failed!" - ) - return null - } - val jsonLocator = jsonDecode(json) as JSONObject - val locatorWithFragments = Locator.fromJSON(jsonLocator) - - return locatorWithFragments - } catch (e: Exception) { - Log.e( - TAG, - "getLocatorFragments: window.epubPage.getVisibleRange json: $json failed! $e" - ) - } - return null - } - suspend fun firstVisibleElementLocator(): Locator? { val navigator = epubNavigator if (navigator == null) { diff --git a/flutter_readium/assets/_helper_scripts/package-lock.json b/flutter_readium/assets/_helper_scripts/package-lock.json index 63d5def3..ce089d8e 100644 --- a/flutter_readium/assets/_helper_scripts/package-lock.json +++ b/flutter_readium/assets/_helper_scripts/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "dependencies": { "animejs": "~3.2.2", - "css-selector-generator": "~3.6.9", "lit": "~3.3.0", "readium-css": "github:readium/readium-css" }, @@ -612,10 +611,11 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -707,10 +707,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -740,18 +741,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2304,10 +2319,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2337,10 +2353,11 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3244,11 +3261,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-selector-generator": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/css-selector-generator/-/css-selector-generator-3.6.9.tgz", - "integrity": "sha512-OXV+a4wlKs+8TGxTZ8g96mQOKz5QDVE52QYdYusdbcmt0XkJd6F9zkXpMZbRk3pwwRF+K2pJkj1DINpWm7Isqw==" - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -3480,10 +3492,11 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -4074,10 +4087,11 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4218,10 +4232,11 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4515,10 +4530,11 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4811,10 +4827,11 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5629,10 +5646,11 @@ } }, "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6627,12 +6645,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7735,10 +7754,11 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8271,10 +8291,11 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, diff --git a/flutter_readium/assets/_helper_scripts/package.json b/flutter_readium/assets/_helper_scripts/package.json index 597479b3..066edfa8 100644 --- a/flutter_readium/assets/_helper_scripts/package.json +++ b/flutter_readium/assets/_helper_scripts/package.json @@ -51,7 +51,6 @@ }, "dependencies": { "animejs": "~3.2.2", - "css-selector-generator": "~3.6.9", "lit": "~3.3.0", "readium-css": "github:readium/readium-css" } diff --git a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts index 78ccbfbc..3686c44e 100644 --- a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts +++ b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts @@ -1,561 +1,21 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ -import { ComicBookPage } from 'ComicBookPage'; -import { getCssSelector } from 'css-selector-generator'; import { initResponsiveTables } from './Tables'; -import { DomRange, ICurrentHeading, IHeadingElement, Locations, Locator, Readium, Rect } from 'types'; +import { Readium } from 'types'; import './EpubPage.scss'; declare const isIos: boolean; declare const isAndroid: boolean; declare const webkit: any; declare const readium: Readium; -declare const comicBookPage: ComicBookPage; declare const Android: any | null; export class EpubPage { - private readonly _headingTagNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - - private _allHeadings: IHeadingElement[] | null; - - private readonly _activeLocationId = 'activeLocation'; - - private readonly _locationTag = 'span'; - - private _documentRange = document.createRange(); - - // Sets an active location, and optionally navigates to a specific frame in a comic book - public setLocation(locator: Locator | null, isAudioBookWithText: boolean): void { - this._debugLog(locator); - - try { - if (locator == null) { - this._debugLog('No locator set'); - - return; - } - - this._removeLocation(); - - this._setLocation(locator, isAudioBookWithText); - - if (this._isComicBook()) { - const cssSelector = - locator?.locations?.cssSelector ?? locator?.locations?.domRange?.start?.cssSelector ?? locator?.locations?.domRange?.end?.cssSelector; - - if (cssSelector == null) { - this._errorLog('Css selector not set!'); - return; - } - - const duration = this._getDurationFragment(locator?.locations?.fragments); - if (duration == null) { - this._errorLog('Duration not set!'); - return; - } - - window.GotoComicFrame(cssSelector, duration * 1000); - } - - // this.debugLog(`setAll END`); - } catch (error) { - this._errorLog(error); - } - } - - // Scrolls such that the given Locations is visible. If part of the given Locations is - // already visible, only scrolls to the start of it if toStart is true. - public scrollToLocations(locations: Locations, isVerticalScroll: boolean, toStart: boolean): boolean { - try { - const range = this._processLocations(locations); - if (range != null) { - this._scrollToProcessedRange(range, isVerticalScroll, toStart); - - return true; - } - - const progression = locations.progression; - if (progression != null) { - readium?.scrollToPosition(progression); - - return true; - } - - this._debugLog(`ScrollToLocations: Unknown range`, locations); - } catch (error) { - this._errorLog(error); - } - - return false; - } - - // Checks whether a given locator is (at least partially) visible. - public isLocatorVisible(locator: Locator): boolean { - this._debugLog(locator); - - try { - const locations = locator.locations; - const selector = locations.cssSelector ?? locations.domRange?.start?.cssSelector; - - if (this._isComicBook()) { - const res = document.querySelector(selector) != null; - this._debugLog(`Comic book`, locations, { found: res, selector }); - - return res; - } - - const range = this._processLocations(locations); - if (range == null) { - this._debugLog(`isLocatorVisible: Unknown range`, locations); - return false; - } - // Checks also that the locator also contains `active` class. - // TODO: This doesn't do what we expect, if the range is visible but not active, this function will return false. - return this._isProcessedRangeVisible(range) && !!document.querySelector(`${selector} #${this._activeLocationId}`); - } catch (error) { - this._errorLog(error); - - // Use true as default to prevent showing the sync button. - return true; - } - } - - // Returns fragments for current location. - public getLocatorFragments(locator: Locator, isVerticalScroll: boolean): Locator { - try { - const cssSelector = locator?.locations?.cssSelector ?? this._findFirstVisibleCssSelector(); - if (cssSelector == null || !cssSelector?.length) { - this._debugLog('getLocatorFragments: selector not found, returning locator from args'); - - return locator; - } - - const fragments = [...this._getPageFragments(isVerticalScroll), ...this._getTocFragments(cssSelector), ...this._getPhysicalPageFragments(cssSelector)]; - - const locatorWithFragments = { - ...locator, - locations: { - cssSelector, - ...locator.locations, - fragments: [...(locator.locations?.fragments ?? []), ...fragments], - }, - }; - - return locatorWithFragments; - } catch (error) { - this._errorLog(error); - - return locator; - } - } - - private _isComicBook(): boolean { - try { - return !!comicBookPage?.isComicBook(); - } catch (_) { - return false; - } - } - - private _isTextNode(node: Node) { - // const TEXT_NODE = 3; - // const CDATA_SECTION_NODE = 4; - const nodeType = node.nodeType; - return nodeType === 3 || nodeType === 4; - } - - // private - private _clamp(value: number, min: number, max: number) { - return Math.min(Math.max(value, min), max); - } - - // Returns the `Text` node starting at the character with offset `charOffset` in node, splitting - // the text node if needed to do so. Returns `null` if charOffset is at (or past) the end of node. - private _findAndSplitOffset(node: Node, charOffset: number): Node | null { - // Using nested function to make sure node is not returned by reference. - function process(n: Node): Node | null { - // TEXT_NODE = 3; - // CDATA_SECTION_NODE = 4; - if (n.nodeType === 3 || n.nodeType === 4) { - const text = n as Text; - if (charOffset <= 0) { - return text; - } - - const length = text.length; - if (charOffset < length) { - return text.splitText(charOffset); - } - - charOffset -= length; - } - - const children = n.childNodes; - const childCount = children.length; - - for (let i = 0; i < childCount; ++i) { - const tn = process(children[i]); - - if (tn != null) { - return tn; - } - } - - return null; - } - - return process(node); - } - - // Next node following the end or closing tag of the given node. - private _nextNodeNotChild(node: HTMLElement | Node): Node | null { - return node && (node.nextSibling ?? this._nextNodeNotChild(node.parentNode)); - } - - // Next node following the start of the given node, including child nodes. - private _nextNode(node: HTMLElement | Node): Node { - return node && (node.firstChild ?? node.nextSibling ?? this._nextNodeNotChild(node.parentNode)); - } - - // Previous node before the beginning or opening tag of the given node. - private _previousNodeNotChild(node: HTMLElement | Node): Node { - return node && (node.previousSibling ?? this._previousNodeNotChild(node.parentNode)); - } - - // Previous node before the beginning of the given node, including child nodes. - private _previousNode(node: HTMLElement | Node): Node { - return node && (node.lastChild ?? node.previousSibling ?? this._previousNodeNotChild(node.parentNode)); - } - - // First non-whitespace character at or after the given node/charOffset. - private _findNonWhitespaceForward(node: HTMLElement | Node, charOffset: number) { - while (node != null) { - if (this._isTextNode(node)) { - const data = (node as Text).data; - charOffset = Math.max(charOffset, 0); - - while (charOffset < data.length) { - if (data[charOffset].trim() !== '') { - return { - node, - charOffset, - }; - } - - ++charOffset; - } - - charOffset = 0; - } - - node = this._nextNode(node); - } - } - - // First non-whitespace character at or before the given node/charOffset. - private _findNonWhitespaceBackward(node: HTMLElement | Node, charOffset?: number) { - while (node != null) { - if (this._isTextNode(node)) { - const data = (node as Text).data; - const last = data.length - 1; - charOffset = Math.min(charOffset ?? last, last); - - while (charOffset >= 0) { - if (data[charOffset].trim() !== '') { - return { - node, - charOffset, - }; - } - - --charOffset; - } - - charOffset = undefined; - } - - node = this._previousNode(node); - } - } - - // First non-whitespace character at or before the given node/charOffset. - private _findNonWhitespace(node: HTMLElement | Node, charOffset: number) { - return this._findNonWhitespaceBackward(node, charOffset) ?? this._findNonWhitespaceForward(node, charOffset); - } - - // Creates a new span specified by locator, with the style attribute given by style. Split into - // multiple spans if needed. - private _setLocation(locator: Locator, isAudioBookWithText: boolean) { - const currentLink = readium.link; - this._debugLog(`create:`, locator, currentLink); - - const locations = locator.locations; - const startCssSelector = locations.cssSelector ?? locations?.domRange?.start?.cssSelector; - - if (!startCssSelector) { - this._errorLog(`Start css selector not found`); - - return; - } - - const startParent = document.querySelector(startCssSelector); - if (!startParent) { - this._errorLog(`Start parent not found`); - - return; - } - - const endCssSelector = locations?.domRange?.end.cssSelector ?? startCssSelector; - const endParent = endCssSelector === startCssSelector ? startParent : document.querySelector(endCssSelector); - - if (!endParent) { - this._errorLog(`End parent not found`); - - return; - } - - const startOffset = locations?.domRange?.start?.charOffset; - const endOffset = locations?.domRange?.end?.charOffset; - - // highlight for audiobooks with text - if (!startOffset && !endOffset && isAudioBookWithText) { - this._wrapWithLocationElement(startParent); - - return; - } - - // Iterate over text nodes between startText and endText (if null, use end of parent). - const startNode = this._findAndSplitOffset(startParent, startOffset) ?? this._nextNodeNotChild(startParent); - const endNode = this._findAndSplitOffset(endParent, endOffset) ?? this._nextNodeNotChild(endParent); - - const texts = new Array(); - - for (let node = startNode; node && node !== endNode; node = this._nextNode(node)) { - if (this._isTextNode(node)) { - texts.push(node as Text); - } - } - - for (const text of texts) { - const locationEl = this._setLocationElement(); - locationEl.appendChild(text.cloneNode(true)); - text.replaceWith(locationEl); - } - } - - // Removes a previously-added span (but doesn't remove the contents of the span). - private _removeLocation() { - this._debugLog('Remove old location'); - - const nodes = document.querySelectorAll(`#${this._activeLocationId}`); - nodes?.forEach((node) => { - if (this._isAndroid()) { - // Ugly workaround for randomly changing layout on Android emulator. - // Leaks lots of useless s. - node.removeAttribute('id'); - return; - } - - const parent = node.parentNode; - node.replaceWith(...node.childNodes); - parent.normalize(); - }); - } - - // The screen rectangle in horizontal scrolling mode and a slightly shortened screen rectangle in - // vertical scrolling mode. - private _safeVisibleRect(isVerticalScroll: boolean): Rect { - const { innerWidth, innerHeight } = window; - - if (isVerticalScroll) { - return { - left: 0, - top: 0.05 * innerHeight, - right: innerWidth, - bottom: 0.95 * innerHeight, - }; - } - return { - left: 0, - top: 0, - right: innerWidth, - bottom: innerHeight, - }; - } - - private *_descendentTextNodes(node: Node): Generator { - if (this._isTextNode(node)) { - yield node as Text; - } else { - for (const child of node.childNodes) { - yield* this._descendentTextNodes(child); - } - } - } - - private _findTextPosition(node: Node, charOffset: number) { - // Converts a text offset in a node into something suitable for Range.setStart or Range.setEnd. - if (node == null) { - this._errorLog(`findTextPosition: no node, charOffset=${charOffset}`); - return; - } - - if (charOffset < 0 || isNaN(charOffset)) { - this._errorLog(`findTextPosition: invalid charOffset, node=${node.nodeValue}, charOffset=${charOffset}`); - return; - } - - if (charOffset === 0) { - return { - node, - charOffset, - }; - } - - for (const textNode of this._descendentTextNodes(node)) { - const length = textNode.length; - - if (charOffset <= length) { - return { - node: textNode, - charOffset, - }; - } - - charOffset -= length; - } - - this._errorLog(`findTextPosition: failed, node=${this._debugNode(node)}, charOffset=${charOffset}`); - - return; - } - - private _processDomRange(domRange: DomRange) { - const { start, end } = domRange; - const { cssSelector: startSelector, charOffset: startOffset } = start; - const { cssSelector: endSelector, charOffset: endOffset } = end != null && end !== void 0 ? end : start; - const startNode = document.querySelector(startSelector); - const startBoundary = this._findTextPosition(startNode, startOffset ?? 0); - - if (startBoundary == null) { - this._errorLog(`DomRange bad start, selector=${startSelector}`); - return; - } - - const endNode = endSelector === startSelector ? startNode : document.querySelector(endSelector); - const endBoundary = this._findTextPosition(endNode, endOffset != null && endOffset !== void 0 ? endOffset : 0); - - if (endBoundary == null) { - this._errorLog(`DomRange bad end, selector=${endSelector}`); - return; - } - - try { - this._documentRange.setStart(startBoundary.node, startBoundary.charOffset); - // this.debugLog(`range.setStart(${startBoundary.node.id ?? startBoundary.node.nodeName}, ${startBoundary.charOffset});`); - } catch (e) { - this._errorLog(`${this._debugNode(startBoundary.node)}, ${startBoundary.charOffset}`, e); - this._documentRange.setStartAfter(startBoundary.node); - } - - try { - this._documentRange.setEnd(endBoundary.node, endBoundary.charOffset); - // this.debugLog(`range.setEnd(${endBoundary.node.id ?? endBoundary.node.nodeName}, ${endBoundary.charOffset});`); - } catch (e) { - this._errorLog(`${this._debugNode(endBoundary.node)}, ${endBoundary.charOffset}`, e); - this._documentRange.setEndAfter(endBoundary.node); - } - - // Work around possible bad getClientBoundingRect data when the start/end of the range is the - // same. Browser bug? Seen on an Android device, not sure whether it happens on iOS. - // https://stackoverflow.com/questions/59767515/incorrect-positioning-of-getboundingclientrect-after-newline-character - if (this._documentRange.getClientRects().length === 0) { - const pos = this._findNonWhitespace(startBoundary.node, startBoundary.charOffset); - - if (pos == null) { - this._errorLog(`Couldn't find any non-whitespace characters in the document!'`); - return; - } - - const { node, charOffset } = pos; - this._documentRange.setStart(node, charOffset); - this._documentRange.setEnd(node, charOffset + 1); - } - - return this._documentRange; - } - - private _processCssSelector(cssSelector: string) { - const node = document.querySelector(cssSelector); - - if (node == null) { - this._errorLog(`processCssSelector: error: node not found ${cssSelector}`); - return; - } - - // Make sure node is visible on the page in order to get the range. - if (window.getComputedStyle(node).display === 'none') { - (node as HTMLElement).style.display = this._isPageBreakElement(node) ? 'flex' : 'block'; - } - - this._documentRange.selectNode(node); - return this._documentRange; - } - - private _processLocations(locations: Locations): Range | null { - if (locations == null) { - this._errorLog('location not set'); - - return; - } - - if (locations.domRange) { - return this._processDomRange(locations.domRange); - } - - const selector = locations.cssSelector ?? locations.domRange?.start?.cssSelector; - if (selector) { - return this._processCssSelector(selector); - } - } - - private _scrollToProcessedRange(range: Range, isVerticalScroll: boolean, toStart: boolean) { - if (toStart || !this._isProcessedRangeVisible(range)) { - this._scrollToBoundingClientRect(range, isVerticalScroll); - } - } - - private _scrollToBoundingClientRect(range: Range, isVerticalScroll: boolean) { - const { top, right, bottom, left } = range.getBoundingClientRect(); - - if (top === 0 && right === 0 && bottom === 0 && left === 0) { - this._debugLog(`scrollToBoundingClientRect: Scrolling to defective bounding rect, abort! `, range.getClientRects(), range.getClientRects().length); - return; - } - - const { scrollLeft, scrollWidth, scrollTop, scrollHeight } = document.scrollingElement; - - if (isVerticalScroll) { - const { top: minHeight, bottom: maxHeight } = this._safeVisibleRect(isVerticalScroll); - - if (top < minHeight || bottom > maxHeight) { - const offset = this._clamp((scrollTop + top - minHeight) / scrollHeight, 0, 1); - readium?.scrollToPosition(offset); - } - } else { - const offset = (scrollLeft + 0.5 * (left + right)) / scrollWidth; - readium?.scrollToPosition(offset); - } - } - - private _isProcessedRangeVisible(range: Range) { - const { innerWidth, innerHeight } = window; - const { top, right, bottom, left } = range.getBoundingClientRect(); - return top < innerHeight && 0 < bottom && left < innerWidth && 0 < right; - } - - private _getPageFragments(isVerticalScroll: boolean): string[] { + /** + * Get page fragments. + */ + public getPageFragments(isVerticalScroll: boolean): string[] { try { const { scrollLeft, scrollWidth } = document.scrollingElement; @@ -571,158 +31,6 @@ export class EpubPage { } } - private _getDurationFragment(fragments: string[]): number | null { - try { - const durationFragment = fragments.find((fragment) => fragment.includes('duration=')); - if (!durationFragment) { - this._errorLog('Duration fragment not found.'); - return; - } - - const durationMatch = /duration=(\d+(?:\.\d+)?)/.exec(durationFragment); - if (!durationMatch) { - this._errorLog('Invalid duration format.'); - return; - } - - this._debugLog(`Duration fragment:`, durationMatch[1]); - return parseFloat(durationMatch[1]); - } catch (error) { - this._errorLog('Could not retrieve duration fragment!'); - return; - } - } - - private _getTocFragments(selector: string): string[] { - try { - const headings = this._findPrecedingAncestorSiblingHeadings(selector); - const id = headings[0]?.id; - if (id == null) { - return []; - } - - return [`toc=${id}`]; - } catch (error) { - this._errorLog(error); - - return []; - } - } - - private _getPhysicalPageFragments(selector: string): string[] { - try { - const currentPhysicalPage = this._findCurrentPhysicalPage(selector); - - if (currentPhysicalPage == null) { - return []; - } - - return [`physicalPage=${currentPhysicalPage}`]; - } catch (error) { - this._errorLog(`Selector:${selector} -- ${error}`); - - return []; - } - } - - // TODO: Code below is from Thorium project. - // Use Intersection Observer API instead: - // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API - private _findPrecedingAncestorSiblingHeadings(selector: string | null): ICurrentHeading[] | null { - const selectorElement = document.querySelector(selector); - - // Check if the element contains any heading before finding parent or sibling heading. - const currentElement = selectorElement?.querySelectorAll(this._headingTagNames.join(','))[0] ?? selectorElement; - - if (currentElement == null) { - return; - } - - if (!this._allHeadings) { - const headingElements = Array.from(window.document.querySelectorAll(this._headingTagNames.join(','))); - for (const hElement of headingElements) { - if (hElement) { - const el = hElement; - const t = el.textContent || el.getAttribute('title') || el.getAttribute('aria-label'); - let i = el.getAttribute('id'); - if (!i) { - // common authoring pattern: parent section (or other container element) has the navigation target anchor - let cur = el; - let p: Element | null; - while ((p = cur.parentNode as Element | null) && p?.nodeType === Node.ELEMENT_NODE) { - if (p.firstElementChild !== cur) { - break; - } - - const di = p.getAttribute('id'); - if (di) { - i = di; - break; - } - - cur = p; - } - } - const heading: IHeadingElement = { - element: el, - id: i ? i : null, - level: parseInt(el.localName.substring(1), 10), - text: t, - }; - if (!this._allHeadings) { - this._allHeadings = []; - } - this._allHeadings.push(heading); - } - } - - if (!this._allHeadings) { - this._allHeadings = []; - } - } - - let arr: ICurrentHeading[] | null; - for (let i = this._allHeadings.length - 1; i >= 0; i--) { - const heading = this._allHeadings[i]; - - const c = currentElement.compareDocumentPosition(heading.element); - - // eslint-disable-next-line no-bitwise - if (c === 0 || c & Node.DOCUMENT_POSITION_PRECEDING || c & Node.DOCUMENT_POSITION_CONTAINS) { - if (!arr) { - arr = []; - } - - // Don't add the heading since the id is missing and it means that toc element does not - // points to this heading. Probably the element is wrapped in `body` or `section` element - // which will handled further below. - if (heading?.id) { - arr.push({ - id: heading.id, - level: heading.level, - text: heading.text, - }); - } - } - } - - if (arr?.length) { - return arr; - } - - // No heading found try with closes section or body - const closetSectionOrBody = selectorElement.closest('section') ?? selectorElement.closest('body'); - if (closetSectionOrBody) { - return [ - { - id: closetSectionOrBody.id, - level: 0, - text: closetSectionOrBody.innerText, - }, - ]; - } - } - private _isPageBreakElement(element: Element | null): boolean { if (element == null) { return false; @@ -761,7 +69,7 @@ export class EpubPage { return sibs; } - private _findCurrentPhysicalPage(cssSelector: string): string | null { + public findCurrentPhysicalPage(cssSelector: string): string | null { let element = document.querySelector(cssSelector); if (element == null) { @@ -797,213 +105,6 @@ export class EpubPage { } } - private _findFirstVisibleCssSelector(): string { - const selector = this._getCssSelector(this._getFirstVisibleElement()); - - return selector; - } - - private _getCssSelector(element: Element): string { - try { - const selector = getCssSelector(element, { - root: document.querySelector('body'), - }); - - // Sometimes getCssSelector returns `:root > :nth-child(2)` instead of `body` - // In such cases, replace it with `body` - const cssSelector = selector?.replace(':root > :nth-child(2)', 'body')?.trim() ?? 'body'; - - this._debugLog(cssSelector); - - return cssSelector; - } catch (error) { - this._errorLog(error); - - return 'body'; - } - } - - private _getFirstVisibleElement(): Element { - const element = this._findFirstVisibleElement(document.body); - - this._debugLog(`First visible element:`, { - tagName: element.nodeName.toLocaleLowerCase(), - id: element.id, - className: element.className, - }); - - return element; - } - - private _findFirstVisibleElement(node: Element): Element { - const nodeData = { - tagName: node.nodeName.toLocaleLowerCase(), - id: node.id, - className: node.className, - }; - - for (const child of node.children) { - const childData = { - tagName: child.nodeName.toLocaleLowerCase(), - id: child.id, - className: child.className, - }; - - if (!this._isElementVisible(child)) { - // Uncomment only when debugging. - // this._debugLog(`Not visible - continue`, childData); - - continue; - } - - if (this._shouldIgnoreElement(child)) { - this._debugLog(`Element is ignored - continue`, childData); - - continue; - } - - if (child.id.includes(`${this._activeLocationId}`)) { - this._debugLog(`Child is an active location element, return closest element with id`, { childData, nodeData }); - - return node.id ? node : this._findClosestElementWithId(child); - } - - if (child.hasChildNodes()) { - this._debugLog(`Loop into children`, childData); - - return this._findFirstVisibleElement(child); - } - - // This should not happens - if (!child.id) { - this._debugLog(`Element has no ID attribute - return closest element with id`, childData); - - return node.id ? node : this._findClosestElementWithId(child); - } - } - - this._debugLog(`return:`, nodeData); - - return node; - } - - private _findClosestElementWithId(element: Element): Element | null { - let currentElement = element.parentElement; - - while (currentElement !== null) { - if (currentElement.id) { - return currentElement; - } - currentElement = currentElement.parentElement; - } - - this._debugLog('No element with id attr found!'); - return element; - } - - // Returns first visible element in viewport. - // True `fullVisibility` will ignore the element if it starts on previous pages. - private _isElementVisible(element: Element, fullVisibility = false): boolean { - if (readium?.isFixedLayout) { - return true; - } - - if (element === document.body || element === document.documentElement) { - return true; - } else if (!document || !document.documentElement || !document.body) { - return false; - } - - const rect = element.getBoundingClientRect(); - - if (fullVisibility) { - if (this._isScrollModeEnabled()) { - return rect.top >= 0 && rect.top <= document.documentElement.clientHeight; - } - - if (rect.left >= 1) { - return true; - } - - return false; - } - - if (this._isScrollModeEnabled()) { - return rect.bottom > 0 && rect.top < window.innerHeight; - } - - return rect.right > 0 && rect.left < window.innerWidth; - } - - private _shouldIgnoreElement(element: Element): boolean { - const elStyle = window.getComputedStyle(element); - if (elStyle) { - const display = elStyle.getPropertyValue('display'); - if (display === 'none') { - return true; - } - // Cannot be relied upon, because web browser engine reports invisible when out of view in - // scrolled columns! - // const visibility = elStyle.getPropertyValue("visibility"); - // if (visibility === "hidden") { - // return false; - // } - const opacity = elStyle.getPropertyValue('opacity'); - if (opacity === '0') { - return true; - } - } - - return this._isElementEmpty(element); - } - - private _isElementEmpty(element: Element): boolean { - const nodeName = element.tagName.toLowerCase(); - if (nodeName === 'img') { - return false; - } - - return element.textContent.trim() === ''; - } - - private _wrapWithLocationElement(el: Element): void { - const parentElement = el; - - if (parentElement) { - const locationEl = this._setLocationElement(); - - while (parentElement.firstChild) { - const child = parentElement.firstChild; - parentElement.removeChild(child); - locationEl.appendChild(child); - } - - parentElement.appendChild(locationEl); - } - } - - private _setLocationElement() { - const el = document.createElement(this._locationTag); - el.id = this._activeLocationId; - - return el; - } - - private _isScrollModeEnabled() { - const style = document.documentElement.style; - return ( - style.getPropertyValue('--USER__view').trim() === 'readium-scroll-on' || - // FIXME: Will need to be removed in Readium 3.0, --USER__scroll was incorrect. - style.getPropertyValue('--USER__scroll').trim() === 'readium-scroll-on' - ); - } - - private _debugLog(...args: unknown[]) { - this._log(`=======Flutter Readium Debug=====`); - this._log(args); - this._log(`=================================`); - } - private _log(...args: unknown[]) { // Alternative for webkit in order to print logs in flutter log outputs. @@ -1024,16 +125,6 @@ export class EpubPage { console.log(JSON.stringify(args)); } - private _debugNode(node: HTMLElement | Node | null): string | undefined { - if (node instanceof Node) { - const xmlSerializer = new XMLSerializer(); - return xmlSerializer.serializeToString(node); - } else if ('innerHTML' in node || 'textContent' in node) { - const element = node as HTMLElement; - return element.innerHTML ?? element.textContent ?? '?'; - } - } - private _errorLog(...error: any) { this._log(`v===v===v===v===v===v`); this._log(`Error:`, error); diff --git a/flutter_readium/assets/helpers/epub.js b/flutter_readium/assets/helpers/epub.js index 226bfe77..85cd0b75 100644 --- a/flutter_readium/assets/helpers/epub.js +++ b/flutter_readium/assets/helpers/epub.js @@ -1 +1 @@ -var epub;(()=>{var e={324(e,t,n){"use strict";n.r(t)},679(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const o=document.createElement("tbody"),r=document.createElement("tr"),i=document.createElement("tr");r.innerHTML=" ",i.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,r.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),i.appendChild(t)}})),o.appendChild(r),o.appendChild(i),e.appendChild(o)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const o=e.rows[0];o&&o.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=o,n=Array.from(o.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const o=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",o[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}},50(e){self,e.exports=(()=>{"use strict";var e={d:(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function n(e){return"object"==typeof e&&null!==e&&e.nodeType===Node.ELEMENT_NODE}e.r(t),e.d(t,{default:()=>K,getCssSelector:()=>X});const o={NONE:"",DESCENDANT:" ",CHILD:" > "},r={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},i="CssSelectorGenerator";function l(e="unknown problem",...t){console.warn(`${i}: ${e}`,...t)}const s={selectors:[r.id,r.class,r.tag,r.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function c(e){return e instanceof RegExp}function a(e){return["string","function"].includes(typeof e)||c(e)}function d(e){return Array.isArray(e)?e.filter(a):[]}function u(e){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(e){return e instanceof Node}(e)&&t.includes(e.nodeType)}function h(e,t){if(u(e))return e.contains(t)||l("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will not work as intended."),e;const n=t.getRootNode({composed:!1});return u(n)?(n!==document&&l("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):y(t)}function f(e){return"number"==typeof e?e:Number.POSITIVE_INFINITY}function g(e=[]){const[t=[],...n]=e;return 0===n.length?t:n.reduce(((e,t)=>e.filter((e=>t.includes(e)))),t)}function m(e){return[].concat(...e)}function v(e){const t=e.map((e=>{if(c(e))return t=>e.test(t);if("function"==typeof e)return t=>{const n=e(t);return"boolean"!=typeof n?(l("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",e),!1):n};if("string"==typeof e){const t=new RegExp("^"+e.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return e=>t.test(e)}return l("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",e),()=>!1}));return e=>t.some((t=>t(e)))}function _(e,t,n){const o=Array.from(h(n,e[0]).querySelectorAll(t));return o.length===e.length&&e.every((e=>o.includes(e)))}function p(e,t){t=null!=t?t:y(e);const o=[];let r=e;for(;n(r)&&r!==t;)o.push(r),r=r.parentElement;return o}function b(e,t){return g(e.map((e=>p(e,t))))}function y(e){return e.ownerDocument.querySelector(":root")}const N=", ",S=new RegExp(["^$","\\s"].join("|")),E=new RegExp(["^$"].join("|")),L=[r.nthoftype,r.tag,r.id,r.class,r.attribute,r.nthchild],C=v(["class","id","ng-*"]);function T({name:e}){return`[${e}]`}function w({name:e,value:t}){return`[${e}='${t}']`}function x({nodeName:e,nodeValue:t}){return{name:H(e),value:H(null!=t?t:void 0)}}function P(e){const t=Array.from(e.attributes).filter((t=>function({nodeName:e,nodeValue:t},n){const o=n.tagName.toLowerCase();return!(["input","option"].includes(o)&&"value"===e||"src"===e&&(null==t?void 0:t.startsWith("data:"))||C(e))}(t,e))).map(x);return[...t.map(T),...t.map(w)]}function O(e){var t;return(null!==(t=e.getAttribute("class"))&&void 0!==t?t:"").trim().split(/\s+/).filter((e=>!E.test(e))).map((e=>`.${H(e)}`))}function A(e){var t;const n=null!==(t=e.getAttribute("id"))&&void 0!==t?t:"",o=`#${H(n)}`,r=e.getRootNode({composed:!1});return!S.test(n)&&_([e],o,r)?[o]:[]}function R(e){const t=e.parentNode;if(t){const o=Array.from(t.childNodes).filter(n).indexOf(e);if(o>-1)return[`:nth-child(${String(o+1)})`]}return[]}function I(e){return[H(e.tagName.toLowerCase())]}function $(e){const t=[...new Set(m(e.map(I)))];return 0===t.length||t.length>1?[]:[t[0]]}function M(e){const t=$([e])[0],n=e.parentElement;if(n){const o=Array.from(n.children).filter((e=>e.tagName.toLowerCase()===t)),r=o.indexOf(e);if(r>-1)return[`${t}:nth-of-type(${String(r+1)})`]}return[]}function k(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){return Array.from(function*(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){let n=0,o=F(1);for(;o.length<=e.length&&ne[t]));yield t,o=D(o,e.length-1)}}(e,{maxResults:t}))}function D(e=[],t=0){const n=e.length;if(0===n)return[];const o=[...e];o[n-1]+=1;for(let e=n-1;e>=0;e--)if(o[e]>t){if(0===e)return F(n+1);o[e-1]++,o[e]=o[e-1]+1}return o[n-1]>t?F(n+1):o}function F(e=1){return Array.from(Array(e).keys())}const q=":".charCodeAt(0).toString(16).toUpperCase(),j=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function H(e=""){return CSS?CSS.escape(e):function(e=""){return e.split("").map((e=>":"===e?`\\${q} `:j.test(e)?`\\${e}`:escape(e).replace(/%/g,"\\"))).join("")}(e)}const V={tag:$,id:function(e){return 0===e.length||e.length>1?[]:A(e[0])},class:function(e){return g(e.map(O))},attribute:function(e){return g(e.map(P))},nthchild:function(e){return g(e.map(R))},nthoftype:function(e){return g(e.map(M))}},W={tag:I,id:A,class:O,attribute:P,nthchild:R,nthoftype:M};function B(e){return e.includes(r.tag)||e.includes(r.nthoftype)?[...e]:[...e,r.tag]}function U(e={}){const t=[...L];return e[r.tag]&&e[r.nthoftype]&&t.splice(t.indexOf(r.tag),1),t.map((t=>{return(o=e)[n=t]?o[n].join(""):"";var n,o})).join("")}function Y(e,t,n="",r){const i=function(e,t){return""===t?e:function(e,t){return[...e.map((e=>t+o.DESCENDANT+e)),...e.map((e=>t+o.CHILD+e))]}(e,t)}(function(e,t,n){const o=function(e,t){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=t,l=v(n),s=v(o);return function(e){const{selectors:t,includeTag:n}=e,o=[...t];return n&&!o.includes("tag")&&o.push("tag"),o}(t).reduce(((t,n)=>{const o=function(e,t){return(0,V[t])(e)}(e,n),c=function(e=[],t,n){return e.filter((e=>n(e)||!t(e)))}(o,l,s),a=function(e=[],t){return e.sort(((e,n)=>{const o=t(e),r=t(n);return o&&!r?-1:!o&&r?1:0}))}(c,s);return t[n]=r?k(a,{maxResults:i}):a.map((e=>[e])),t}),{})}(e,n),r=function(e,t){return function(e){const{selectors:t,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=e,i=n?k(t,{maxResults:r}):t.map((e=>[e]));return o?i.map(B):i}(t).map((t=>function(e,t){const n={};return e.forEach((e=>{const o=t[e];o&&o.length>0&&(n[e]=o)})),function(e={}){let t=[];return Object.entries(e).forEach((([e,n])=>{t=n.flatMap((n=>0===t.length?[{[e]:n}]:t.map((t=>Object.assign(Object.assign({},t),{[e]:n})))))})),t}(n).map(U)}(t,e))).filter((e=>e.length>0))}(o,n),i=m(r);return[...new Set(i)]}(e,0,r),n);for(const n of i)if(_(e,n,t))return n;return null}function G(e){return{value:e,include:!1}}function z({selectors:e,operator:t}){let n=[...L];e[r.tag]&&e[r.nthoftype]&&(n=n.filter((e=>e!==r.tag)));let o="";return n.forEach((t=>{var n;(null!==(n=e[t])&&void 0!==n?n:[]).forEach((({value:e,include:t})=>{t&&(o+=e)}))})),t+o}function J(e){return[":root",...p(e).reverse().map((e=>{const t=function(e,t,n=o.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(e,t){return W[t](e)}(e,t).map(G))})),{element:e,operator:n,selectors:r}}(e,[r.nthchild],o.CHILD);return t.selectors.nthchild.forEach((e=>{e.include=!0})),t})).map(z)].join("")}function X(e,t={}){var o;const i=function(e){(e instanceof NodeList||e instanceof HTMLCollection)&&(e=Array.from(e));const t=(Array.isArray(e)?e:[e]).filter(n);return[...new Set(t)]}(e),l=function(e,t={}){const n=Object.assign(Object.assign({},s),t);return{selectors:(o=n.selectors,Array.isArray(o)?o.filter((e=>{return t=r,n=e,Object.values(t).includes(n);var t,n})):[]),whitelist:d(n.whitelist),blacklist:d(n.blacklist),root:h(n.root,e),combineWithinSelector:!!n.combineWithinSelector,combineBetweenSelectors:!!n.combineBetweenSelectors,includeTag:!!n.includeTag,maxCombinations:f(n.maxCombinations),maxCandidates:f(n.maxCandidates)};var o}(i[0],t),c=null!==(o=l.root)&&void 0!==o?o:y(i[0]);let a="",u=c;function g(){return function(e,t,n="",o){if(0===e.length)return null;const r=[e.length>1?e:[],...b(e,t).map((e=>[e]))];for(const e of r){const r=Y(e,t,n,o);if(r)return{foundElements:e,selector:r}}return null}(i,u,a,l)}let m=g();for(;m;){const{foundElements:e,selector:t}=m;if(_(i,t,c))return t;u=e[0],a=t,m=g()}return i.length>1?i.map((e=>X(e,l))).join(N):function(e){return e.map(J).join(N)}(i)}const K=X;return t})()}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var o={};(()=>{"use strict";var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(50),r=n(679);n(324);class i{constructor(){this._headingTagNames=["h1","h2","h3","h4","h5","h6"],this._activeLocationId="activeLocation",this._locationTag="span",this._documentRange=document.createRange()}setLocation(e,t){var n,o,r,i,l,s,c,a,d,u;this._debugLog(e);try{if(null==e)return void this._debugLog("No locator set");if(this._removeLocation(),this._setLocation(e,t),this._isComicBook()){const t=null!==(s=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:null===(l=null===(i=null===(r=null==e?void 0:e.locations)||void 0===r?void 0:r.domRange)||void 0===i?void 0:i.start)||void 0===l?void 0:l.cssSelector)&&void 0!==s?s:null===(d=null===(a=null===(c=null==e?void 0:e.locations)||void 0===c?void 0:c.domRange)||void 0===a?void 0:a.end)||void 0===d?void 0:d.cssSelector;if(null==t)return void this._errorLog("Css selector not set!");const h=this._getDurationFragment(null===(u=null==e?void 0:e.locations)||void 0===u?void 0:u.fragments);if(null==h)return void this._errorLog("Duration not set!");window.GotoComicFrame(t,1e3*h)}}catch(e){this._errorLog(e)}}scrollToLocations(e,t,n){try{const o=this._processLocations(e);if(null!=o)return this._scrollToProcessedRange(o,t,n),!0;const r=e.progression;if(null!=r)return null===readium||void 0===readium||readium.scrollToPosition(r),!0;this._debugLog("ScrollToLocations: Unknown range",e)}catch(e){this._errorLog(e)}return!1}isLocatorVisible(e){var t,n,o;this._debugLog(e);try{const r=e.locations,i=null!==(t=r.cssSelector)&&void 0!==t?t:null===(o=null===(n=r.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;if(this._isComicBook()){const e=null!=document.querySelector(i);return this._debugLog("Comic book",r,{found:e,selector:i}),e}const l=this._processLocations(r);return null==l?(this._debugLog("isLocatorVisible: Unknown range",r),!1):this._isProcessedRangeVisible(l)&&!!document.querySelector(`${i} #${this._activeLocationId}`)}catch(e){return this._errorLog(e),!0}}getLocatorFragments(e,t){var n,o,r,i;try{const l=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:this._findFirstVisibleCssSelector();if(null==l||!(null==l?void 0:l.length))return this._debugLog("getLocatorFragments: selector not found, returning locator from args"),e;const s=[...this._getPageFragments(t),...this._getTocFragments(l),...this._getPhysicalPageFragments(l)];return Object.assign(Object.assign({},e),{locations:Object.assign(Object.assign({cssSelector:l},e.locations),{fragments:[...null!==(i=null===(r=e.locations)||void 0===r?void 0:r.fragments)&&void 0!==i?i:[],...s]})})}catch(t){return this._errorLog(t),e}}_isComicBook(){try{return!!(null===comicBookPage||void 0===comicBookPage?void 0:comicBookPage.isComicBook())}catch(e){return!1}}_isTextNode(e){const t=e.nodeType;return 3===t||4===t}_clamp(e,t,n){return Math.min(Math.max(e,t),n)}_findAndSplitOffset(e,t){return function e(n){if(3===n.nodeType||4===n.nodeType){const e=n;if(t<=0)return e;const o=e.length;if(t=0;){if(""!==n[t].trim())return{node:e,charOffset:t};--t}t=void 0}e=this._previousNode(e)}}_findNonWhitespace(e,t){var n;return null!==(n=this._findNonWhitespaceBackward(e,t))&&void 0!==n?n:this._findNonWhitespaceForward(e,t)}_setLocation(e,t){var n,o,r,i,l,s,c,a,d,u,h;const f=readium.link;this._debugLog("create:",e,f);const g=e.locations,m=null!==(n=g.cssSelector)&&void 0!==n?n:null===(r=null===(o=null==g?void 0:g.domRange)||void 0===o?void 0:o.start)||void 0===r?void 0:r.cssSelector;if(!m)return void this._errorLog("Start css selector not found");const v=document.querySelector(m);if(!v)return void this._errorLog("Start parent not found");const _=null!==(l=null===(i=null==g?void 0:g.domRange)||void 0===i?void 0:i.end.cssSelector)&&void 0!==l?l:m,p=_===m?v:document.querySelector(_);if(!p)return void this._errorLog("End parent not found");const b=null===(c=null===(s=null==g?void 0:g.domRange)||void 0===s?void 0:s.start)||void 0===c?void 0:c.charOffset,y=null===(d=null===(a=null==g?void 0:g.domRange)||void 0===a?void 0:a.end)||void 0===d?void 0:d.charOffset;if(!b&&!y&&t)return void this._wrapWithLocationElement(v);const N=null!==(u=this._findAndSplitOffset(v,b))&&void 0!==u?u:this._nextNodeNotChild(v),S=null!==(h=this._findAndSplitOffset(p,y))&&void 0!==h?h:this._nextNodeNotChild(p),E=new Array;for(let e=N;e&&e!==S;e=this._nextNode(e))this._isTextNode(e)&&E.push(e);for(const e of E){const t=this._setLocationElement();t.appendChild(e.cloneNode(!0)),e.replaceWith(t)}}_removeLocation(){this._debugLog("Remove old location");const e=document.querySelectorAll(`#${this._activeLocationId}`);null==e||e.forEach((e=>{if(this._isAndroid())return void e.removeAttribute("id");const t=e.parentNode;e.replaceWith(...e.childNodes),t.normalize()}))}_safeVisibleRect(e){const{innerWidth:t,innerHeight:n}=window;return e?{left:0,top:.05*n,right:t,bottom:.95*n}:{left:0,top:0,right:t,bottom:n}}*_descendentTextNodes(e){if(this._isTextNode(e))yield e;else for(const t of e.childNodes)yield*this._descendentTextNodes(t)}_findTextPosition(e,t){if(null!=e)if(t<0||isNaN(t))this._errorLog(`findTextPosition: invalid charOffset, node=${e.nodeValue}, charOffset=${t}`);else{if(0===t)return{node:e,charOffset:t};for(const n of this._descendentTextNodes(e)){const e=n.length;if(t<=e)return{node:n,charOffset:t};t-=e}this._errorLog(`findTextPosition: failed, node=${this._debugNode(e)}, charOffset=${t}`)}else this._errorLog(`findTextPosition: no node, charOffset=${t}`)}_processDomRange(e){const{start:t,end:n}=e,{cssSelector:o,charOffset:r}=t,{cssSelector:i,charOffset:l}=null!=n&&void 0!==n?n:t,s=document.querySelector(o),c=this._findTextPosition(s,null!=r?r:0);if(null==c)return void this._errorLog(`DomRange bad start, selector=${o}`);const a=i===o?s:document.querySelector(i),d=this._findTextPosition(a,null!=l&&void 0!==l?l:0);if(null!=d){try{this._documentRange.setStart(c.node,c.charOffset)}catch(e){this._errorLog(`${this._debugNode(c.node)}, ${c.charOffset}`,e),this._documentRange.setStartAfter(c.node)}try{this._documentRange.setEnd(d.node,d.charOffset)}catch(e){this._errorLog(`${this._debugNode(d.node)}, ${d.charOffset}`,e),this._documentRange.setEndAfter(d.node)}if(0===this._documentRange.getClientRects().length){const e=this._findNonWhitespace(c.node,c.charOffset);if(null==e)return void this._errorLog("Couldn't find any non-whitespace characters in the document!'");const{node:t,charOffset:n}=e;this._documentRange.setStart(t,n),this._documentRange.setEnd(t,n+1)}return this._documentRange}this._errorLog(`DomRange bad end, selector=${i}`)}_processCssSelector(e){const t=document.querySelector(e);if(null!=t)return"none"===window.getComputedStyle(t).display&&(t.style.display=this._isPageBreakElement(t)?"flex":"block"),this._documentRange.selectNode(t),this._documentRange;this._errorLog(`processCssSelector: error: node not found ${e}`)}_processLocations(e){var t,n,o;if(null==e)return void this._errorLog("location not set");if(e.domRange)return this._processDomRange(e.domRange);const r=null!==(t=e.cssSelector)&&void 0!==t?t:null===(o=null===(n=e.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;return r?this._processCssSelector(r):void 0}_scrollToProcessedRange(e,t,n){!n&&this._isProcessedRangeVisible(e)||this._scrollToBoundingClientRect(e,t)}_scrollToBoundingClientRect(e,t){const{top:n,right:o,bottom:r,left:i}=e.getBoundingClientRect();if(0===n&&0===o&&0===r&&0===i)return void this._debugLog("scrollToBoundingClientRect: Scrolling to defective bounding rect, abort! ",e.getClientRects(),e.getClientRects().length);const{scrollLeft:l,scrollWidth:s,scrollTop:c,scrollHeight:a}=document.scrollingElement;if(t){const{top:e,bottom:o}=this._safeVisibleRect(t);if(no){const t=this._clamp((c+n-e)/a,0,1);null===readium||void 0===readium||readium.scrollToPosition(t)}}else{const e=(l+.5*(i+o))/s;null===readium||void 0===readium||readium.scrollToPosition(e)}}_isProcessedRangeVisible(e){const{innerWidth:t,innerHeight:n}=window,{top:o,right:r,bottom:i,left:l}=e.getBoundingClientRect();return oe.includes("duration=")));if(!t)return void this._errorLog("Duration fragment not found.");const n=/duration=(\d+(?:\.\d+)?)/.exec(t);return n?(this._debugLog("Duration fragment:",n[1]),parseFloat(n[1])):void this._errorLog("Invalid duration format.")}catch(e){return void this._errorLog("Could not retrieve duration fragment!")}}_getTocFragments(e){var t;try{const n=null===(t=this._findPrecedingAncestorSiblingHeadings(e)[0])||void 0===t?void 0:t.id;return null==n?[]:[`toc=${n}`]}catch(e){return this._errorLog(e),[]}}_getPhysicalPageFragments(e){try{const t=this._findCurrentPhysicalPage(e);return null==t?[]:[`physicalPage=${t}`]}catch(t){return this._errorLog(`Selector:${e} -- ${t}`),[]}}_findPrecedingAncestorSiblingHeadings(e){var t,n;const o=document.querySelector(e),r=null!==(t=null==o?void 0:o.querySelectorAll(this._headingTagNames.join(","))[0])&&void 0!==t?t:o;if(null==r)return;if(!this._allHeadings){const e=Array.from(window.document.querySelectorAll(this._headingTagNames.join(",")));for(const t of e)if(t){const e=t,n=e.textContent||e.getAttribute("title")||e.getAttribute("aria-label");let o=e.getAttribute("id");if(!o){let t,n=e;for(;(t=n.parentNode)&&(null==t?void 0:t.nodeType)===Node.ELEMENT_NODE&&t.firstElementChild===n;){const e=t.getAttribute("id");if(e){o=e;break}n=t}}const r={element:e,id:o||null,level:parseInt(e.localName.substring(1),10),text:n};this._allHeadings||(this._allHeadings=[]),this._allHeadings.push(r)}this._allHeadings||(this._allHeadings=[])}let i;for(let e=this._allHeadings.length-1;e>=0;e--){const t=this._allHeadings[e],n=r.compareDocumentPosition(t.element);(0===n||n&Node.DOCUMENT_POSITION_PRECEDING||n&Node.DOCUMENT_POSITION_CONTAINS)&&(i||(i=[]),(null==t?void 0:t.id)&&i.push({id:t.id,level:t.level,text:t.text}))}if(null==i?void 0:i.length)return i;const l=null!==(n=o.closest("section"))&&void 0!==n?n:o.closest("body");return l?[{id:l.id,level:0,text:l.innerText}]:void 0}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(e){var t;let n=document.querySelector(e);if(null!=n){if(this._isPageBreakElement(n))return this._getPhysicalPageIndexFromElement(n);for(;n.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(n);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(n)));t>=0;t--){const n=e[t],o=this._findPhysicalPageIndex(n);if(null!=o)return o}if(n=n.parentNode,null==n||"body"===n.nodeName.toLowerCase())return null===(t=document.querySelector("head [name='webpub:currentPage']"))||void 0===t?void 0:t.getAttribute("content")}}}_findFirstVisibleCssSelector(){return this._getCssSelector(this._getFirstVisibleElement())}_getCssSelector(e){var n,o;try{const r=(0,t.getCssSelector)(e,{root:document.querySelector("body")}),i=null!==(o=null===(n=null==r?void 0:r.replace(":root > :nth-child(2)","body"))||void 0===n?void 0:n.trim())&&void 0!==o?o:"body";return this._debugLog(i),i}catch(e){return this._errorLog(e),"body"}}_getFirstVisibleElement(){const e=this._findFirstVisibleElement(document.body);return this._debugLog("First visible element:",{tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className}),e}_findFirstVisibleElement(e){const t={tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className};for(const n of e.children){const o={tagName:n.nodeName.toLocaleLowerCase(),id:n.id,className:n.className};if(this._isElementVisible(n))if(this._shouldIgnoreElement(n))this._debugLog("Element is ignored - continue",o);else{if(n.id.includes(`${this._activeLocationId}`))return this._debugLog("Child is an active location element, return closest element with id",{childData:o,nodeData:t}),e.id?e:this._findClosestElementWithId(n);if(n.hasChildNodes())return this._debugLog("Loop into children",o),this._findFirstVisibleElement(n);if(!n.id)return this._debugLog("Element has no ID attribute - return closest element with id",o),e.id?e:this._findClosestElementWithId(n)}}return this._debugLog("return:",t),e}_findClosestElementWithId(e){let t=e.parentElement;for(;null!==t;){if(t.id)return t;t=t.parentElement}return this._debugLog("No element with id attr found!"),e}_isElementVisible(e,t=!1){if(null===readium||void 0===readium?void 0:readium.isFixedLayout)return!0;if(e===document.body||e===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;const n=e.getBoundingClientRect();return t?this._isScrollModeEnabled()?n.top>=0&&n.top<=document.documentElement.clientHeight:n.left>=1:this._isScrollModeEnabled()?n.bottom>0&&n.top0&&n.lefte instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_debugNode(e){var t,n;if(e instanceof Node){return(new XMLSerializer).serializeToString(e)}if("innerHTML"in e||"textContent"in e){const o=e;return null!==(n=null!==(t=o.innerHTML)&&void 0!==t?t:o.textContent)&&void 0!==n?n:"?"}}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function l(){window.epubPage||((0,r.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",l),window.epubPage=new i)}e.EpubPage=i,"loading"!==document.readyState?window.setTimeout(l):document.addEventListener("DOMContentLoaded",l)})(),epub=o})(); \ No newline at end of file +var epub;(()=>{var e={324(e,t,n){"use strict";n.r(t)},679(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const o=document.createElement("tbody"),r=document.createElement("tr"),i=document.createElement("tr");r.innerHTML=" ",i.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,r.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),i.appendChild(t)}})),o.appendChild(r),o.appendChild(i),e.appendChild(o)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const o=e.rows[0];o&&o.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=o,n=Array.from(o.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const o=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",o[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}},50(e){self,e.exports=(()=>{"use strict";var e={d:(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function n(e){return"object"==typeof e&&null!==e&&e.nodeType===Node.ELEMENT_NODE}e.r(t),e.d(t,{default:()=>K,getCssSelector:()=>X});const o={NONE:"",DESCENDANT:" ",CHILD:" > "},r={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},i="CssSelectorGenerator";function l(e="unknown problem",...t){console.warn(`${i}: ${e}`,...t)}const s={selectors:[r.id,r.class,r.tag,r.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function c(e){return e instanceof RegExp}function a(e){return["string","function"].includes(typeof e)||c(e)}function u(e){return Array.isArray(e)?e.filter(a):[]}function d(e){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(e){return e instanceof Node}(e)&&t.includes(e.nodeType)}function f(e,t){if(d(e))return e.contains(t)||l("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will not work as intended."),e;const n=t.getRootNode({composed:!1});return d(n)?(n!==document&&l("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):y(t)}function h(e){return"number"==typeof e?e:Number.POSITIVE_INFINITY}function g(e=[]){const[t=[],...n]=e;return 0===n.length?t:n.reduce(((e,t)=>e.filter((e=>t.includes(e)))),t)}function m(e){return[].concat(...e)}function p(e){const t=e.map((e=>{if(c(e))return t=>e.test(t);if("function"==typeof e)return t=>{const n=e(t);return"boolean"!=typeof n?(l("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",e),!1):n};if("string"==typeof e){const t=new RegExp("^"+e.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return e=>t.test(e)}return l("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",e),()=>!1}));return e=>t.some((t=>t(e)))}function _(e,t,n){const o=Array.from(f(n,e[0]).querySelectorAll(t));return o.length===e.length&&e.every((e=>o.includes(e)))}function v(e,t){t=null!=t?t:y(e);const o=[];let r=e;for(;n(r)&&r!==t;)o.push(r),r=r.parentElement;return o}function b(e,t){return g(e.map((e=>v(e,t))))}function y(e){return e.ownerDocument.querySelector(":root")}const N=", ",S=new RegExp(["^$","\\s"].join("|")),E=new RegExp(["^$"].join("|")),C=[r.nthoftype,r.tag,r.id,r.class,r.attribute,r.nthchild],L=p(["class","id","ng-*"]);function T({name:e}){return`[${e}]`}function w({name:e,value:t}){return`[${e}='${t}']`}function x({nodeName:e,nodeValue:t}){return{name:H(e),value:H(null!=t?t:void 0)}}function P(e){const t=Array.from(e.attributes).filter((t=>function({nodeName:e,nodeValue:t},n){const o=n.tagName.toLowerCase();return!(["input","option"].includes(o)&&"value"===e||"src"===e&&(null==t?void 0:t.startsWith("data:"))||L(e))}(t,e))).map(x);return[...t.map(T),...t.map(w)]}function O(e){var t;return(null!==(t=e.getAttribute("class"))&&void 0!==t?t:"").trim().split(/\s+/).filter((e=>!E.test(e))).map((e=>`.${H(e)}`))}function A(e){var t;const n=null!==(t=e.getAttribute("id"))&&void 0!==t?t:"",o=`#${H(n)}`,r=e.getRootNode({composed:!1});return!S.test(n)&&_([e],o,r)?[o]:[]}function I(e){const t=e.parentNode;if(t){const o=Array.from(t.childNodes).filter(n).indexOf(e);if(o>-1)return[`:nth-child(${String(o+1)})`]}return[]}function R(e){return[H(e.tagName.toLowerCase())]}function $(e){const t=[...new Set(m(e.map(R)))];return 0===t.length||t.length>1?[]:[t[0]]}function M(e){const t=$([e])[0],n=e.parentElement;if(n){const o=Array.from(n.children).filter((e=>e.tagName.toLowerCase()===t)),r=o.indexOf(e);if(r>-1)return[`${t}:nth-of-type(${String(r+1)})`]}return[]}function k(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){return Array.from(function*(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){let n=0,o=F(1);for(;o.length<=e.length&&ne[t]));yield t,o=D(o,e.length-1)}}(e,{maxResults:t}))}function D(e=[],t=0){const n=e.length;if(0===n)return[];const o=[...e];o[n-1]+=1;for(let e=n-1;e>=0;e--)if(o[e]>t){if(0===e)return F(n+1);o[e-1]++,o[e]=o[e-1]+1}return o[n-1]>t?F(n+1):o}function F(e=1){return Array.from(Array(e).keys())}const j=":".charCodeAt(0).toString(16).toUpperCase(),q=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function H(e=""){return CSS?CSS.escape(e):function(e=""){return e.split("").map((e=>":"===e?`\\${j} `:q.test(e)?`\\${e}`:escape(e).replace(/%/g,"\\"))).join("")}(e)}const V={tag:$,id:function(e){return 0===e.length||e.length>1?[]:A(e[0])},class:function(e){return g(e.map(O))},attribute:function(e){return g(e.map(P))},nthchild:function(e){return g(e.map(I))},nthoftype:function(e){return g(e.map(M))}},B={tag:R,id:A,class:O,attribute:P,nthchild:I,nthoftype:M};function W(e){return e.includes(r.tag)||e.includes(r.nthoftype)?[...e]:[...e,r.tag]}function U(e={}){const t=[...C];return e[r.tag]&&e[r.nthoftype]&&t.splice(t.indexOf(r.tag),1),t.map((t=>{return(o=e)[n=t]?o[n].join(""):"";var n,o})).join("")}function Y(e,t,n="",r){const i=function(e,t){return""===t?e:function(e,t){return[...e.map((e=>t+o.DESCENDANT+e)),...e.map((e=>t+o.CHILD+e))]}(e,t)}(function(e,t,n){const o=function(e,t){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=t,l=p(n),s=p(o);return function(e){const{selectors:t,includeTag:n}=e,o=[...t];return n&&!o.includes("tag")&&o.push("tag"),o}(t).reduce(((t,n)=>{const o=function(e,t){return(0,V[t])(e)}(e,n),c=function(e=[],t,n){return e.filter((e=>n(e)||!t(e)))}(o,l,s),a=function(e=[],t){return e.sort(((e,n)=>{const o=t(e),r=t(n);return o&&!r?-1:!o&&r?1:0}))}(c,s);return t[n]=r?k(a,{maxResults:i}):a.map((e=>[e])),t}),{})}(e,n),r=function(e,t){return function(e){const{selectors:t,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=e,i=n?k(t,{maxResults:r}):t.map((e=>[e]));return o?i.map(W):i}(t).map((t=>function(e,t){const n={};return e.forEach((e=>{const o=t[e];o&&o.length>0&&(n[e]=o)})),function(e={}){let t=[];return Object.entries(e).forEach((([e,n])=>{t=n.flatMap((n=>0===t.length?[{[e]:n}]:t.map((t=>Object.assign(Object.assign({},t),{[e]:n})))))})),t}(n).map(U)}(t,e))).filter((e=>e.length>0))}(o,n),i=m(r);return[...new Set(i)]}(e,0,r),n);for(const n of i)if(_(e,n,t))return n;return null}function G(e){return{value:e,include:!1}}function z({selectors:e,operator:t}){let n=[...C];e[r.tag]&&e[r.nthoftype]&&(n=n.filter((e=>e!==r.tag)));let o="";return n.forEach((t=>{var n;(null!==(n=e[t])&&void 0!==n?n:[]).forEach((({value:e,include:t})=>{t&&(o+=e)}))})),t+o}function J(e){return[":root",...v(e).reverse().map((e=>{const t=function(e,t,n=o.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(e,t){return B[t](e)}(e,t).map(G))})),{element:e,operator:n,selectors:r}}(e,[r.nthchild],o.CHILD);return t.selectors.nthchild.forEach((e=>{e.include=!0})),t})).map(z)].join("")}function X(e,t={}){var o;const i=function(e){(e instanceof NodeList||e instanceof HTMLCollection)&&(e=Array.from(e));const t=(Array.isArray(e)?e:[e]).filter(n);return[...new Set(t)]}(e),l=function(e,t={}){const n=Object.assign(Object.assign({},s),t);return{selectors:(o=n.selectors,Array.isArray(o)?o.filter((e=>{return t=r,n=e,Object.values(t).includes(n);var t,n})):[]),whitelist:u(n.whitelist),blacklist:u(n.blacklist),root:f(n.root,e),combineWithinSelector:!!n.combineWithinSelector,combineBetweenSelectors:!!n.combineBetweenSelectors,includeTag:!!n.includeTag,maxCombinations:h(n.maxCombinations),maxCandidates:h(n.maxCandidates)};var o}(i[0],t),c=null!==(o=l.root)&&void 0!==o?o:y(i[0]);let a="",d=c;function g(){return function(e,t,n="",o){if(0===e.length)return null;const r=[e.length>1?e:[],...b(e,t).map((e=>[e]))];for(const e of r){const r=Y(e,t,n,o);if(r)return{foundElements:e,selector:r}}return null}(i,d,a,l)}let m=g();for(;m;){const{foundElements:e,selector:t}=m;if(_(i,t,c))return t;d=e[0],a=t,m=g()}return i.length>1?i.map((e=>X(e,l))).join(N):function(e){return e.map(J).join(N)}(i)}const K=X;return t})()}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var o={};(()=>{"use strict";var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(50),r=n(679);n(324);class i{constructor(){this._headingTagNames=["h1","h2","h3","h4","h5","h6"],this._activeLocationId="activeLocation",this._locationTag="span",this._documentRange=document.createRange()}getAllElementIds(){return[...document.querySelectorAll("[id]")].map((e=>e.id))}scrollToLocations(e,t,n){try{const o=this._processLocations(e);if(null!=o)return this._scrollToProcessedRange(o,t,n),!0;const r=e.progression;if(null!=r)return null===readium||void 0===readium||readium.scrollToPosition(r),!0;this._debugLog("ScrollToLocations: Unknown range",e)}catch(e){this._errorLog(e)}return!1}isLocatorVisible(e){var t,n,o;this._debugLog(e);try{const r=e.locations,i=null!==(t=r.cssSelector)&&void 0!==t?t:null===(o=null===(n=r.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;if(this._isComicBook()){const e=null!=document.querySelector(i);return this._debugLog("Comic book",r,{found:e,selector:i}),e}const l=this._processLocations(r);return null==l?(this._debugLog("isLocatorVisible: Unknown range",r),!1):this._isProcessedRangeVisible(l)&&!!document.querySelector(`${i} #${this._activeLocationId}`)}catch(e){return this._errorLog(e),!0}}getLocatorFragments(e,t){var n,o,r,i;try{const l=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:this._findFirstVisibleCssSelector();if(null==l||!(null==l?void 0:l.length))return this._debugLog("getLocatorFragments: selector not found, returning locator from args"),e;const s=[...this._getPageFragments(t),...this._getTocFragments(l),...this._getPhysicalPageFragments(l)];return Object.assign(Object.assign({},e),{locations:Object.assign(Object.assign({cssSelector:l},e.locations),{fragments:[...null!==(i=null===(r=e.locations)||void 0===r?void 0:r.fragments)&&void 0!==i?i:[],...s]})})}catch(t){return this._errorLog(t),e}}_isComicBook(){try{return!!(null===comicBookPage||void 0===comicBookPage?void 0:comicBookPage.isComicBook())}catch(e){return!1}}_isTextNode(e){const t=e.nodeType;return 3===t||4===t}_clamp(e,t,n){return Math.min(Math.max(e,t),n)}_findAndSplitOffset(e,t){return function e(n){if(3===n.nodeType||4===n.nodeType){const e=n;if(t<=0)return e;const o=e.length;if(t=0;){if(""!==n[t].trim())return{node:e,charOffset:t};--t}t=void 0}e=this._previousNode(e)}}_findNonWhitespace(e,t){var n;return null!==(n=this._findNonWhitespaceBackward(e,t))&&void 0!==n?n:this._findNonWhitespaceForward(e,t)}_safeVisibleRect(e){const{innerWidth:t,innerHeight:n}=window;return e?{left:0,top:.05*n,right:t,bottom:.95*n}:{left:0,top:0,right:t,bottom:n}}*_descendentTextNodes(e){if(this._isTextNode(e))yield e;else for(const t of e.childNodes)yield*this._descendentTextNodes(t)}_findTextPosition(e,t){if(null!=e)if(t<0||isNaN(t))this._errorLog(`findTextPosition: invalid charOffset, node=${e.nodeValue}, charOffset=${t}`);else{if(0===t)return{node:e,charOffset:t};for(const n of this._descendentTextNodes(e)){const e=n.length;if(t<=e)return{node:n,charOffset:t};t-=e}this._errorLog(`findTextPosition: failed, node=${this._debugNode(e)}, charOffset=${t}`)}else this._errorLog(`findTextPosition: no node, charOffset=${t}`)}_processDomRange(e){const{start:t,end:n}=e,{cssSelector:o,charOffset:r}=t,{cssSelector:i,charOffset:l}=null!=n&&void 0!==n?n:t,s=document.querySelector(o),c=this._findTextPosition(s,null!=r?r:0);if(null==c)return void this._errorLog(`DomRange bad start, selector=${o}`);const a=i===o?s:document.querySelector(i),u=this._findTextPosition(a,null!=l&&void 0!==l?l:0);if(null!=u){try{this._documentRange.setStart(c.node,c.charOffset)}catch(e){this._errorLog(`${this._debugNode(c.node)}, ${c.charOffset}`,e),this._documentRange.setStartAfter(c.node)}try{this._documentRange.setEnd(u.node,u.charOffset)}catch(e){this._errorLog(`${this._debugNode(u.node)}, ${u.charOffset}`,e),this._documentRange.setEndAfter(u.node)}if(0===this._documentRange.getClientRects().length){const e=this._findNonWhitespace(c.node,c.charOffset);if(null==e)return void this._errorLog("Couldn't find any non-whitespace characters in the document!'");const{node:t,charOffset:n}=e;this._documentRange.setStart(t,n),this._documentRange.setEnd(t,n+1)}return this._documentRange}this._errorLog(`DomRange bad end, selector=${i}`)}_processCssSelector(e){const t=document.querySelector(e);if(null!=t)return"none"===window.getComputedStyle(t).display&&(t.style.display=this._isPageBreakElement(t)?"flex":"block"),this._documentRange.selectNode(t),this._documentRange;this._errorLog(`processCssSelector: error: node not found ${e}`)}_processLocations(e){var t,n,o;if(null==e)return void this._errorLog("location not set");if(e.domRange)return this._processDomRange(e.domRange);const r=null!==(t=e.cssSelector)&&void 0!==t?t:null===(o=null===(n=e.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;return r?this._processCssSelector(r):void 0}_scrollToProcessedRange(e,t,n){!n&&this._isProcessedRangeVisible(e)||this._scrollToBoundingClientRect(e,t)}_scrollToBoundingClientRect(e,t){const{top:n,right:o,bottom:r,left:i}=e.getBoundingClientRect();if(0===n&&0===o&&0===r&&0===i)return void this._debugLog("scrollToBoundingClientRect: Scrolling to defective bounding rect, abort! ",e.getClientRects(),e.getClientRects().length);const{scrollLeft:l,scrollWidth:s,scrollTop:c,scrollHeight:a}=document.scrollingElement;if(t){const{top:e,bottom:o}=this._safeVisibleRect(t);if(no){const t=this._clamp((c+n-e)/a,0,1);null===readium||void 0===readium||readium.scrollToPosition(t)}}else{const e=(l+.5*(i+o))/s;null===readium||void 0===readium||readium.scrollToPosition(e)}}_isProcessedRangeVisible(e){const{innerWidth:t,innerHeight:n}=window,{top:o,right:r,bottom:i,left:l}=e.getBoundingClientRect();return oe.includes("duration=")));if(!t)return void this._errorLog("Duration fragment not found.");const n=/duration=(\d+(?:\.\d+)?)/.exec(t);return n?(this._debugLog("Duration fragment:",n[1]),parseFloat(n[1])):void this._errorLog("Invalid duration format.")}catch(e){return void this._errorLog("Could not retrieve duration fragment!")}}_getTocFragments(e){var t;try{const n=null===(t=this._findPrecedingAncestorSiblingHeadings(e)[0])||void 0===t?void 0:t.id;return null==n?[]:[`toc=${n}`]}catch(e){return this._errorLog(e),[]}}_getPhysicalPageFragments(e){try{const t=this._findCurrentPhysicalPage(e);return null==t?[]:[`physicalPage=${t}`]}catch(t){return this._errorLog(`Selector:${e} -- ${t}`),[]}}_findPrecedingAncestorSiblingHeadings(e){var t,n;const o=document.querySelector(e),r=null!==(t=null==o?void 0:o.querySelectorAll(this._headingTagNames.join(","))[0])&&void 0!==t?t:o;if(null==r)return;if(!this._allHeadings){const e=Array.from(window.document.querySelectorAll(this._headingTagNames.join(",")));for(const t of e)if(t){const e=t,n=e.textContent||e.getAttribute("title")||e.getAttribute("aria-label");let o=e.getAttribute("id");if(!o){let t,n=e;for(;(t=n.parentNode)&&(null==t?void 0:t.nodeType)===Node.ELEMENT_NODE&&t.firstElementChild===n;){const e=t.getAttribute("id");if(e){o=e;break}n=t}}const r={element:e,id:o||null,level:parseInt(e.localName.substring(1),10),text:n};this._allHeadings||(this._allHeadings=[]),this._allHeadings.push(r)}this._allHeadings||(this._allHeadings=[])}let i;for(let e=this._allHeadings.length-1;e>=0;e--){const t=this._allHeadings[e],n=r.compareDocumentPosition(t.element);(0===n||n&Node.DOCUMENT_POSITION_PRECEDING||n&Node.DOCUMENT_POSITION_CONTAINS)&&(i||(i=[]),(null==t?void 0:t.id)&&i.push({id:t.id,level:t.level,text:t.text}))}if(null==i?void 0:i.length)return i;const l=null!==(n=o.closest("section"))&&void 0!==n?n:o.closest("body");return l?[{id:l.id,level:0,text:l.innerText}]:void 0}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(e){var t;let n=document.querySelector(e);if(null!=n){if(this._isPageBreakElement(n))return this._getPhysicalPageIndexFromElement(n);for(;n.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(n);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(n)));t>=0;t--){const n=e[t],o=this._findPhysicalPageIndex(n);if(null!=o)return o}if(n=n.parentNode,null==n||"body"===n.nodeName.toLowerCase())return null===(t=document.querySelector("head [name='webpub:currentPage']"))||void 0===t?void 0:t.getAttribute("content")}}}_findFirstVisibleCssSelector(){return this._getCssSelector(this._getFirstVisibleElement())}_getCssSelector(e){var n,o;try{const r=(0,t.getCssSelector)(e,{root:document.querySelector("body")}),i=null!==(o=null===(n=null==r?void 0:r.replace(":root > :nth-child(2)","body"))||void 0===n?void 0:n.trim())&&void 0!==o?o:"body";return this._debugLog(i),i}catch(e){return this._errorLog(e),"body"}}_getFirstVisibleElement(){const e=this._findFirstVisibleElement(document.body);return this._debugLog("First visible element:",{tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className}),e}_findFirstVisibleElement(e){const t={tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className};for(const n of e.children){const o={tagName:n.nodeName.toLocaleLowerCase(),id:n.id,className:n.className};if(this._isElementVisible(n))if(this._shouldIgnoreElement(n))this._debugLog("Element is ignored - continue",o);else{if(n.id.includes(`${this._activeLocationId}`))return this._debugLog("Child is an active location element, return closest element with id",{childData:o,nodeData:t}),e.id?e:this._findClosestElementWithId(n);if(n.hasChildNodes())return this._debugLog("Loop into children",o),this._findFirstVisibleElement(n);if(!n.id)return this._debugLog("Element has no ID attribute - return closest element with id",o),e.id?e:this._findClosestElementWithId(n)}}return this._debugLog("return:",t),e}_findClosestElementWithId(e){let t=e.parentElement;for(;null!==t;){if(t.id)return t;t=t.parentElement}return this._debugLog("No element with id attr found!"),e}_isElementVisible(e,t=!1){if(null===readium||void 0===readium?void 0:readium.isFixedLayout)return!0;if(e===document.body||e===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;const n=e.getBoundingClientRect();return t?this._isScrollModeEnabled()?n.top>=0&&n.top<=document.documentElement.clientHeight:n.left>=1:this._isScrollModeEnabled()?n.bottom>0&&n.top0&&n.lefte instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_debugNode(e){var t,n;if(e instanceof Node){return(new XMLSerializer).serializeToString(e)}if("innerHTML"in e||"textContent"in e){const o=e;return null!==(n=null!==(t=o.innerHTML)&&void 0!==t?t:o.textContent)&&void 0!==n?n:"?"}}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function l(){window.epubPage||((0,r.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",l),window.epubPage=new i)}e.EpubPage=i,"loading"!==document.readyState?window.setTimeout(l):document.addEventListener("DOMContentLoaded",l)})(),epub=o})(); \ No newline at end of file diff --git a/flutter_readium/example/ios/Podfile.lock b/flutter_readium/example/ios/Podfile.lock index 2409b080..e1bb3271 100644 --- a/flutter_readium/example/ios/Podfile.lock +++ b/flutter_readium/example/ios/Podfile.lock @@ -20,9 +20,6 @@ PODS: - Minizip (1.0.0) - package_info_plus (0.4.5): - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - pointer_interceptor_ios (0.0.1): - Flutter - PromiseKit (8.2.0): @@ -73,7 +70,6 @@ DEPENDENCIES: - flutter_readium (from `.symlinks/plugins/flutter_readium/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) - PromiseKit (~> 8.1) - ReadiumAdapterGCDWebServer (from `https://raw.githubusercontent.com/readium/swift-toolkit/3.7.0/Support/CocoaPods/ReadiumAdapterGCDWebServer.podspec`) @@ -104,8 +100,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" pointer_interceptor_ios: :path: ".symlinks/plugins/pointer_interceptor_ios/ios" ReadiumAdapterGCDWebServer: @@ -133,7 +127,6 @@ SPEC CHECKSUMS: integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e Minizip: 188cb3e39a1195c283ae03bf673d182596fefd0b package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 PromiseKit: 74e6ab5894856b4762fef547055c809bca91d440 ReadiumAdapterGCDWebServer: 6b7864065eabf708b8d04e6b98ad1a63fbfccf0d diff --git a/flutter_readium/example/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter_readium/example/macos/Flutter/GeneratedPluginRegistrant.swift index 0ec16401..c085c19b 100644 --- a/flutter_readium/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter_readium/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,10 @@ import Foundation import flutter_readium import package_info_plus -import path_provider_foundation import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterReadiumPlugin.register(with: registry.registrar(forPlugin: "FlutterReadiumPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/flutter_readium/example/pubspec.lock b/flutter_readium/example/pubspec.lock index ecdc13c4..b7936f31 100644 --- a/flutter_readium/example/pubspec.lock +++ b/flutter_readium/example/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: build_config - sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" characters: dependency: transitive description: @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: "direct main" description: @@ -125,10 +133,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" + sha256: "6f6b30cba0301e7b38f32bdc9a6bdae6f5921a55f0a1eb9450e1e6515645dbb2" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" dartx: dependency: transitive description: @@ -264,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.19.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" http: dependency: transitive description: @@ -305,26 +321,18 @@ packages: dependency: transitive description: name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.10.0" - json_schema: - dependency: transitive - description: - name: json_schema - sha256: f37d9c3fdfe8c9aae55fdfd5af815d24ce63c3a0f6a2c1f0982c30f43643fa1a - url: "https://pub.dev" - source: hosted - version: "5.2.2" + version: "4.11.0" json_serializable: dependency: transitive description: name: json_serializable - sha256: "93fba3ad139dab2b1ce59ecc6fdce6da46a42cdb6c4399ecda30f1e7e725760d" + sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0" url: "https://pub.dev" source: hosted - version: "6.12.0" + version: "6.13.0" leak_tracker: dependency: transitive description: @@ -389,6 +397,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -397,6 +413,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" package_config: dependency: transitive description: @@ -449,10 +473,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -565,22 +589,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rfc_6901: - dependency: transitive - description: - name: rfc_6901 - sha256: "6a43b1858dca2febaf93e15639aa6b0c49ccdfd7647775f15a499f872b018154" - url: "https://pub.dev" - source: hosted - version: "0.2.1" rxdart: dependency: "direct main" description: @@ -690,14 +698,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - uri: - dependency: transitive - description: - name: uri - sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" - url: "https://pub.dev" - source: hosted - version: "1.0.0" vector_math: dependency: transitive description: @@ -787,5 +787,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift index 58a57391..6537de8b 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift @@ -327,12 +327,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele return await readiumViewController.go(to: locator, options: NavigatorGoOptions(animated: animated)) } - private func setLocation(locator: Locator, isAudioBookWithText: Bool) async -> Result { - let json = locator.jsonString ?? "null" - - return await evaluateJavascript("window.epubPage.setLocation(\(json), \(isAudioBookWithText));") - } - private func emitOnPageChanged() { guard let locator = readiumViewController.currentLocation else { print(TAG, "emitOnPageChanged: currentLocation = nil!") @@ -353,7 +347,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele Task.detached(priority: .high) { await self.goToLocator(locator: locator, animated: animated) - let _ = await self.setLocation(locator: locator, isAudioBookWithText: isAudioBookWithText) await MainActor.run() { result(true) } @@ -381,18 +374,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele } } break - case "setLocation": - let args = call.arguments as! [Any] - print(TAG, "onMethodCall[setLocation] locator = \(args[0] as! String)") - let locator = try! Locator(jsonString: args[0] as! String, warnings: readiumBugLogger)! - let isAudioBookWithText = args[1] as? Bool ?? false - Task.detached(priority: .high) { - let _ = await self.setLocation(locator: locator, isAudioBookWithText: isAudioBookWithText) - return await MainActor.run() { - result(true) - } - } - break case "getLocatorFragments": let args = call.arguments as? String ?? "null" Task.detached(priority: .high) { @@ -409,16 +390,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele } } break - case "isLocatorVisible": - let args = call.arguments as! String - print(TAG, "onMethodCall[isLocatorVisible] locator = \(args)") - let locator = try! Locator(jsonString: args, warnings: readiumBugLogger)! - if locator.href != self.readiumViewController.currentLocation?.href { - result(false) - return - } - evaluateJSReturnResult("window.epubPage.isLocatorVisible(\(args));", result: result) - break case "setPreferences": let args = call.arguments as! [String: String] print(TAG, "onMethodCall[setPreferences] args = \(args)") diff --git a/flutter_readium/lib/reader_channel.dart b/flutter_readium/lib/reader_channel.dart index a95b8634..a090c2ec 100644 --- a/flutter_readium/lib/reader_channel.dart +++ b/flutter_readium/lib/reader_channel.dart @@ -4,16 +4,7 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter_readium/flutter_readium.dart'; -enum _ReaderChannelMethodInvoke { - applyDecorations, - go, - goLeft, - goRight, - setLocation, - isLocatorVisible, - dispose, - setPreferences, -} +enum _ReaderChannelMethodInvoke { applyDecorations, go, goLeft, goRight, dispose, setPreferences } /// Internal use only. /// Used by ReadiumReaderWidget to talk to the native widget. @@ -48,10 +39,6 @@ class ReadiumReaderChannel extends MethodChannel { return _invokeMethod(_ReaderChannelMethodInvoke.goRight, animated); } - /// Set the current location to the given [locator]. - Future setLocation(final Locator locator, final bool isAudioBookWithText) async => - _invokeMethod(_ReaderChannelMethodInvoke.setLocation, [json.encode(locator), isAudioBookWithText]); - /// Set EPUB preferences. Future setEPUBPreferences(EPUBPreferences preferences) async { await _invokeMethod(_ReaderChannelMethodInvoke.setPreferences, preferences.toJson()); @@ -62,12 +49,6 @@ class ReadiumReaderChannel extends MethodChannel { return await _invokeMethod(_ReaderChannelMethodInvoke.applyDecorations, [id, decorations.map((d) => d.toJson())]); } - /// Check if a locator is currently visible on screen. - Future isLocatorVisible(final Locator locator) => _invokeMethod( - _ReaderChannelMethodInvoke.isLocatorVisible, - json.encode(locator), - ).then((final isVisible) => isVisible!).onError((final error, final _) => true); - Future dispose() async { try { await _invokeMethod(_ReaderChannelMethodInvoke.dispose); From ba6d58b434a5fefa75ff94c31f2587190f8c3fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Fri, 27 Feb 2026 17:21:14 +0100 Subject: [PATCH 05/19] minor cleanup --- .../nota/flutter_readium/ReadiumExtensions.kt | 25 +++++++++++-------- .../dk/nota/flutter_readium/ReadiumReader.kt | 24 ++++++++---------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index 6d5500e5..fa31c78e 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -269,17 +269,20 @@ suspend fun Publication.findAllCssSelectors(href: Url): List? { return null } - val cleanHref = href.removeQuery().removeFragment() + val cleanHref = href.cleanHref() val ids = arrayListOf() - for (element in contentService.content(Locator(href = cleanHref, mediaType = MediaType.XHTML))) { + for (element in contentService.content( + Locator( + href = cleanHref, + mediaType = MediaType.XHTML + ) + )) { if (element !is Content.TextElement) { continue } - if (element.locator.href.removeQuery() - .removeFragment() != cleanHref - ) { + if (element.locator.href.cleanHref() != cleanHref) { // We iterated to the next document, stopping break } @@ -305,16 +308,13 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { return null } - val cleanHref = locator.href.removeQuery().removeFragment() - + val cleanHref = locator.href.cleanHref() for (element in contentService.content(locator)) { if (element !is Content.TextElement) { continue } - if (element.locator.href.removeQuery() - .removeFragment() != cleanHref - ) { + if (element.locator.href.cleanHref() != cleanHref) { // We iterated to the next document, stopping break } @@ -327,3 +327,8 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { return null } + +/** + * Remove query and fragment from the Url + */ +fun Url.cleanHref() = removeFragment().removeQuery() diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index f1796de4..e4f86aa1 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -177,9 +177,6 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua private var epubNavigator: EpubNavigator? = null - val epubCurrentLocator: Locator? - get() = epubNavigator?.currentLocator?.value - private var _audioPreferences: FlutterAudioPreferences = FlutterAudioPreferences() /** Current audio preferences (defaults if audio hasn't been enabled yet). */ @@ -400,10 +397,11 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua } /*** - * Maps a URL to a HTML document to a list of all the ids in the document. - * This is used to find the current ToC item. + * For EPUB profile, maps document [Url] to a list of all the cssSelectors in the document. + * + * This is used to find the current toc item. */ - private var currentPublicationContentIdsMap: MutableMap>? = null + private var currentPublicationCssSelectorMap: MutableMap>? = null /** * Sets the headers used in the HTTP requests for fetching publication resources, including @@ -586,7 +584,7 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua mainScope.async { _currentPublication?.close() _currentPublication = null - currentPublicationContentIdsMap = null + currentPublicationCssSelectorMap = null ttsNavigator?.dispose() ttsNavigator = null @@ -661,9 +659,9 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua return resultLocator } + val cleanHref = resultLocator.href.cleanHref() val toc = publication.tableOfContents.flatten().filter { - it.href.resolve().removeQuery() - .removeFragment() == resultLocator.href.removeFragment().removeQuery() + it.href.resolve().cleanHref() == cleanHref }.associateBy { contentIds.indexOf("${it.href.resolve().fragment}") } val tocItem = toc.entries.lastOrNull { it.key <= idx }?.value @@ -989,11 +987,11 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua * Get all cssSelectors for an EPUB file. */ suspend fun epubGetAllDocumentCssSelectors(href: Url): List { - val contentIdsMap = currentPublicationContentIdsMap ?: mutableMapOf() - currentPublicationContentIdsMap = contentIdsMap + val cssSelectorMap = currentPublicationCssSelectorMap ?: mutableMapOf() + currentPublicationCssSelectorMap = cssSelectorMap - val cleanHref = href.removeQuery().removeFragment() - return contentIdsMap.getOrPut(cleanHref) { + val cleanHref = href.cleanHref() + return cssSelectorMap.getOrPut(cleanHref) { currentPublication?.findAllCssSelectors( cleanHref ) ?: listOf() From 351e0589f86846dc032f2308f5bcada525eb8825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Mon, 2 Mar 2026 12:52:00 +0100 Subject: [PATCH 06/19] feat(android): add toc info to other locations for sync audiobook navigator --- .../dk/nota/flutter_readium/ReadiumExtensions.kt | 10 +++++----- .../nota/flutter_readium/models/FlutterMediaOverlay.kt | 6 +++--- .../flutter_readium/models/FlutterMediaOverlayItem.kt | 10 ++++++++-- .../navigators/SyncAudiobookNavigator.kt | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index fa31c78e..7797b799 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -153,10 +153,10 @@ suspend fun Publication.getMediaOverlays(): List? { if (!hasMediaOverlays()) return null // Flatten TOC for title lookup - val toc = tableOfContents.flatten().map { Pair(it.href.toString(), it.title) } + val toc = tableOfContents.flatten().map { Pair(it.href.toString(), it) } // Remember last matched TOC item for titles - var lastTocMatch: Pair? = null + var lastTocMatch: Pair? = null return this.readingOrder.mapNotNull { r -> r.alternates.find { a -> @@ -166,7 +166,7 @@ suspend fun Publication.getMediaOverlays(): List? { val jsonString = this.get(link)?.read()?.getOrNull()?.let { String(it) } ?: return@mapIndexedNotNull null val jsonObject = JSONObject(jsonString) - FlutterMediaOverlay.fromJson(jsonObject, index + 1, link.title ?: "") + FlutterMediaOverlay.fromJson(jsonObject, index + 1, null,link.title ?: "") } .map { mo -> val items = mo.items.map { item -> @@ -177,9 +177,9 @@ suspend fun Publication.getMediaOverlays(): List? { if (match?.second != null) { lastTocMatch = match - item.copy(title = match.second ?: "") + item.copy(title = match.second.title ?: "", tocHref = match.second.href.resolve()) } else if (lastTocMatch?.second != null && lastTocMatch.first.substringBefore("#") == item.textFile) { - item.copy(title = lastTocMatch.second ?: "") + item.copy(title = lastTocMatch.second.title ?: "", tocHref = lastTocMatch.second.href.resolve()) } else { item } diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlay.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlay.kt index 83aa0e93..b08af5b3 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlay.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlay.kt @@ -111,14 +111,14 @@ data class FlutterMediaOverlay(val items: List) : Seria } companion object { - fun fromJson(json: JSONObject, position: Int, title: String): FlutterMediaOverlay? { + fun fromJson(json: JSONObject, position: Int, tocHref: Url?, title: String): FlutterMediaOverlay? { val topNarration = json.opt("narration") as? JSONArray ?: return null val items = mutableListOf() for (i in 0 until topNarration.length()) { val itemJson = topNarration.getJSONObject(i) - FlutterMediaOverlayItem.fromJson(itemJson, position, title)?.let { items.add(it) } + FlutterMediaOverlayItem.fromJson(itemJson, position, tocHref, title)?.let { items.add(it) } - fromJson(itemJson, position, title)?.let { items.addAll(it.items) } + fromJson(itemJson, position, tocHref, title)?.let { items.addAll(it.items) } } return FlutterMediaOverlay(items) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlayItem.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlayItem.kt index 0786a589..4ce587c4 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlayItem.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/FlutterMediaOverlayItem.kt @@ -25,6 +25,11 @@ data class FlutterMediaOverlayItem( */ val position: Int, + /** + * The ToC item for this item. + */ + val tocHref: Url?, + /** * The title of the chapter or section this item belongs to */ @@ -114,6 +119,7 @@ data class FlutterMediaOverlayItem( textLocator.copy( locations = textLocator.locations.copy( fragments = listOf("t=${audioStart ?: 0.0}"), + otherLocations = textLocator.locations.otherLocations + ("toc" to tocHref.toString()) ), ) } @@ -142,11 +148,11 @@ data class FlutterMediaOverlayItem( * Creates a [FlutterMediaOverlayItem] from a JSON object. * Returns null if the JSON object does not contain valid "audio" and "text" */ - fun fromJson(json: JSONObject, position: Int, title: String): FlutterMediaOverlayItem? { + fun fromJson(json: JSONObject, position: Int, tocHref: Url?, title: String): FlutterMediaOverlayItem? { val audio = json.optString("audio") val text = json.optString("text") return if (audio != "" && text != "") { - FlutterMediaOverlayItem(audio, text, position, title) + FlutterMediaOverlayItem(audio, text, position, tocHref, title) } else { null } diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/SyncAudiobookNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/SyncAudiobookNavigator.kt index 1353b720..be9776d5 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/SyncAudiobookNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/SyncAudiobookNavigator.kt @@ -67,7 +67,7 @@ class SyncAudiobookNavigator( if (mediaOverlay == null) { Log.d( TAG, - ":onTimebasedCurrentLocatorChanges no media-overlay item found for locator=$locator, timeOffset=$timeOffset" + ":onCurrentLocatorChanges no media-overlay item found for locator=$locator, timeOffset=$timeOffset" ) return } From dc3a6bb4c4e8c059147e922417e3e6580ea0c2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Mon, 2 Mar 2026 13:32:15 +0100 Subject: [PATCH 07/19] feat(android): add toc info to other locations for pure audiobook navigator --- .../nota/flutter_readium/ReadiumExtensions.kt | 41 +++++++++++++++++-- .../dk/nota/flutter_readium/ReadiumReader.kt | 4 +- .../navigators/AudiobookNavigator.kt | 32 +++++++++++++++ .../navigators/TimebasedNavigator.kt | 21 +++++----- 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index 7797b799..b5421c8c 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -26,6 +26,9 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.filename +import java.util.Locale +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import org.readium.r2.navigator.preferences.Color as ReadiumColor private const val TAG = "ReadiumExtensions" @@ -166,7 +169,7 @@ suspend fun Publication.getMediaOverlays(): List? { val jsonString = this.get(link)?.read()?.getOrNull()?.let { String(it) } ?: return@mapIndexedNotNull null val jsonObject = JSONObject(jsonString) - FlutterMediaOverlay.fromJson(jsonObject, index + 1, null,link.title ?: "") + FlutterMediaOverlay.fromJson(jsonObject, index + 1, null, link.title ?: "") } .map { mo -> val items = mo.items.map { item -> @@ -177,9 +180,15 @@ suspend fun Publication.getMediaOverlays(): List? { if (match?.second != null) { lastTocMatch = match - item.copy(title = match.second.title ?: "", tocHref = match.second.href.resolve()) + item.copy( + title = match.second.title ?: "", + tocHref = match.second.href.resolve() + ) } else if (lastTocMatch?.second != null && lastTocMatch.first.substringBefore("#") == item.textFile) { - item.copy(title = lastTocMatch.second.title ?: "", tocHref = lastTocMatch.second.href.resolve()) + item.copy( + title = lastTocMatch.second.title ?: "", + tocHref = lastTocMatch.second.href.resolve() + ) } else { item } @@ -332,3 +341,29 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { * Remove query and fragment from the Url */ fun Url.cleanHref() = removeFragment().removeQuery() + +/** + * Remove query and fragment from the Href + */ +fun Href.cleanHref() = Href(resolve().cleanHref()) + +val Href.fragmentParameters: Map + get() = resolve() + .fragment + // Splits parameters + ?.split("&") + ?.filter { !it.startsWith("=") } + ?.map { it.split("=") } + // Only keep named parameters + ?.filter { it.size == 2 } + ?.associate { it[0].trim().lowercase(Locale.ROOT) to it[1].trim() } + ?: mapOf() + + +/** + * Media fragment, used for example in audiobooks. + * + * https://www.w3.org/TR/media-frags/ + */ +val Href.time: Duration? get() = + fragmentParameters["t"]?.toIntOrNull()?.seconds diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index e4f86aa1..d663aa22 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -40,6 +40,7 @@ import org.readium.navigator.media.tts.android.AndroidTtsPreferences import org.readium.navigator.media.tts.android.AndroidTtsSettings import org.readium.r2.navigator.Decoration import org.readium.r2.navigator.epub.EpubPreferences +import org.readium.r2.navigator.extensions.time import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Link @@ -614,12 +615,13 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua // TODO: Notify client } + @OptIn(InternalReadiumApi::class) override fun onTimebasedCurrentLocatorChanges( locator: Locator, currentReadingOrderLink: Link? ) { val duration = currentReadingOrderLink?.duration val timeOffset = - locator.locations.fragments.find { it.startsWith("t=") }?.substring(2)?.toDoubleOrNull() + locator.locations.time?.inWholeSeconds?.toDouble() ?: (duration?.let { duration -> locator.locations.progression?.let { prog -> duration * prog } }) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt index dc06a246..b2b591d2 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt @@ -7,7 +7,9 @@ import dk.nota.flutter_readium.FlutterAudioPreferences import dk.nota.flutter_readium.PluginMediaServiceFacade import dk.nota.flutter_readium.PublicationError import dk.nota.flutter_readium.ReadiumReader +import dk.nota.flutter_readium.cleanHref import dk.nota.flutter_readium.throttleLatest +import dk.nota.flutter_readium.time import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow @@ -23,9 +25,13 @@ import org.readium.adapter.exoplayer.audio.ExoPlayerNavigatorFactory import org.readium.adapter.exoplayer.audio.ExoPlayerPreferences import org.readium.adapter.exoplayer.audio.ExoPlayerSettings import org.readium.navigator.media.audio.AudioNavigator +import org.readium.r2.navigator.extensions.time import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.flatten import org.readium.r2.shared.util.getOrElse import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -225,6 +231,32 @@ open class AudiobookNavigator( } } + @OptIn(InternalReadiumApi::class) + override fun onCurrentLocatorChanges(locator: Locator) { + var emittingLocator = locator + + locator.locations.time?.let { time -> + var matchedTocItem: Link? = null + for (link in publication.tableOfContents.flatten().filter { it.href.resolve().cleanHref() == locator.href.cleanHref() }) { + val tocTime = link.href.time ?: continue + if (tocTime > time) { + continue + } + matchedTocItem = link + } + + matchedTocItem?.href?.resolve()?.let { + emittingLocator = emittingLocator.copy( + locations = emittingLocator.locations.copy( + otherLocations = emittingLocator.locations.otherLocations + ( "toc" to it ) + ) + ) + } + } + + super.onCurrentLocatorChanges(emittingLocator) + } + override fun onPlaybackStateChanged(pb: AudioNavigator.Playback) { when (pb.state) { is AudioNavigator.State.Failure<*> -> { diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TimebasedNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TimebasedNavigator.kt index 56ff9e25..6b4b8b0d 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TimebasedNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/TimebasedNavigator.kt @@ -75,7 +75,8 @@ abstract class TimebasedNavigator

( var timebasedState: TimebasedState when (pb.state) { is MediaNavigator.State.Ready -> { - timebasedState = if (pb.playWhenReady) TimebasedState.Playing else TimebasedState.Paused + timebasedState = + if (pb.playWhenReady) TimebasedState.Playing else TimebasedState.Paused } is MediaNavigator.State.Buffering -> { @@ -100,26 +101,24 @@ abstract class TimebasedNavigator

( } override fun onCurrentLocatorChanges(locator: Locator) { + var emittingLocator = locator + val readingOrderLink = publication.readingOrder.find { link -> link.href.toString() == locator.href.toString() } - if (locator.locations.position == null) { - val index = - publication.readingOrder.indexOfFirst { link -> - link == readingOrderLink - } - if (index != -1) { - val newLocator = locator.copy( + if (emittingLocator.locations.position == null) { + publication.readingOrder.indexOfFirst { link -> + link == readingOrderLink + }.takeIf { it > -1 }?.let { index -> + emittingLocator = emittingLocator.copy( locations = locator.locations.copy(position = index + 1) ) - timebaseListener.onTimebasedCurrentLocatorChanges(newLocator, readingOrderLink) - return } } - timebaseListener.onTimebasedCurrentLocatorChanges(locator, readingOrderLink) + timebaseListener.onTimebasedCurrentLocatorChanges(emittingLocator, readingOrderLink) } /** From cbb83fe1726d7dc3017e45330e1b3c7119a7fa53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Mon, 2 Mar 2026 15:09:23 +0100 Subject: [PATCH 08/19] feat(android): add page information to tex-locator --- .../flutter_readium/ReadiumReaderWidget.kt | 19 ++++++- .../flutter_readium/models/PageInformation.kt | 32 +++++++++++ .../assets/_helper_scripts/src/EpubPage.scss | 6 -- .../assets/_helper_scripts/src/EpubPage.ts | 56 ++++++++++++------- .../assets/_helper_scripts/src/types.ts | 35 +++++++----- flutter_readium/assets/helpers/epub.css | 2 +- flutter_readium/assets/helpers/epub.js | 2 +- 7 files changed, 109 insertions(+), 43 deletions(-) create mode 100644 flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt index 187bdba0..4251e61a 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentActivity import androidx.fragment.app.commitNow import dk.nota.flutter_readium.events.ReadiumReaderStatus import dk.nota.flutter_readium.fragments.EpubReaderFragment +import dk.nota.flutter_readium.models.PageInformation import dk.nota.flutter_readium.navigators.EpubNavigator import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -210,7 +211,23 @@ class ReadiumReaderWidget( private suspend fun emitOnPageChanged(locator: Locator) { try { - val emittingLocator = ReadiumReader.epubFindCurrentToc(locator) + var emittingLocator = ReadiumReader.epubFindCurrentToc(locator) + try { + evaluateJavascript("window.epubPage.getPageInformation()")?.let { + PageInformation.fromJson( + it + ) + }?.let { pageInfo -> + emittingLocator = emittingLocator.copy( + locations = emittingLocator.locations.copy( + otherLocations = emittingLocator.locations.otherLocations + pageInfo.otherLocations, + ), + ); + } + } catch (e: Error) { + Log.d(TAG, ":pageInformation error: $e") + } + channel.onPageChanged(emittingLocator) ReadiumReader.emitTextLocatorUpdate(emittingLocator) Log.d(TAG, "emitOnPageChanged: emitted $emittingLocator") diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt new file mode 100644 index 00000000..67ec65a8 --- /dev/null +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt @@ -0,0 +1,32 @@ +package dk.nota.flutter_readium.models + +import dk.nota.flutter_readium.jsonDecode +import org.json.JSONObject + +class PageInformation(val pageIndex: Long?, val totalPages: Long?, val physicalPageIndex: String?) { + val otherLocations: Map + get() { + val res = mutableMapOf() + if (pageIndex != null && totalPages != null) { + res["currentPage"] = pageIndex + res["totalPages"] = totalPages + } + + physicalPageIndex?.takeIf { it.isNotEmpty() }?.let { + res["physicalPage"] = it + } + return res; + } + + companion object { + fun fromJson(json: String): PageInformation = fromJson(jsonDecode(json) as JSONObject) + + fun fromJson(json: JSONObject): PageInformation { + val pageIndex = json.optLong("pageIndex") + val totalPages = json.optLong("totalPages") + val physicalPageIndex = json.optString("physicalPageIndex") + + return PageInformation(pageIndex, totalPages, physicalPageIndex) + } + } +} diff --git a/flutter_readium/assets/_helper_scripts/src/EpubPage.scss b/flutter_readium/assets/_helper_scripts/src/EpubPage.scss index 5de97c61..4aede2f6 100644 --- a/flutter_readium/assets/_helper_scripts/src/EpubPage.scss +++ b/flutter_readium/assets/_helper_scripts/src/EpubPage.scss @@ -10,12 +10,6 @@ font-size: 98%; } -span#activeLocation { - border-radius: 4px; - background-color: var(--USER__highlightBackgroundColor) !important; - color: var(--USER__highlightForegroundColor) !important; -} - body > *:first-child { margin-top: 50px !important; } diff --git a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts index 3686c44e..fbbb426e 100644 --- a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts +++ b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts @@ -1,34 +1,45 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ - import { initResponsiveTables } from './Tables'; -import { Readium } from 'types'; +import { PageInformation, Readium } from 'types'; import './EpubPage.scss'; declare const isIos: boolean; declare const isAndroid: boolean; declare const webkit: any; declare const readium: Readium; -declare const Android: any | null; export class EpubPage { - /** - * Get page fragments. - */ - public getPageFragments(isVerticalScroll: boolean): string[] { - try { - const { scrollLeft, scrollWidth } = document.scrollingElement; + private get _isScrollModeEnabled(): boolean { + return getComputedStyle(document.documentElement).getPropertyValue("--USER__view") === "readium-scroll-on"; + } - const { innerWidth } = window; - const pageIndex = isVerticalScroll ? null : Math.round(scrollLeft / innerWidth) + 1; - const totalPages = isVerticalScroll ? null : Math.round(scrollWidth / innerWidth); + public getPageInformation(): PageInformation { + const physicalPageIndex = this._findCurrentPhysicalPage(); - return [`page=${pageIndex}`, `totalPages=${totalPages}`]; - } catch (error) { - this._errorLog(error); + if (readium?.isReflowable) { + if (this._isScrollModeEnabled) { + return { + pageIndex: null, + totalPages: null, + physicalPageIndex, + } + } - return []; + const { scrollLeft, scrollWidth } = document.scrollingElement; + const { innerWidth } = window; + return { + pageIndex: this._isScrollModeEnabled ? null : Math.round(scrollLeft / innerWidth) + 1, + totalPages: this._isScrollModeEnabled ? null : Math.round(scrollWidth / innerWidth), + physicalPageIndex, + } } + + // Assume fixed layout has only one page, and the physical page index is determined by the current visible element. + return { + pageIndex: 1, + totalPages: 1, + physicalPageIndex, + }; } private _isPageBreakElement(element: Element | null): boolean { @@ -69,7 +80,14 @@ export class EpubPage { return sibs; } - public findCurrentPhysicalPage(cssSelector: string): string | null { + /** + * Find the current physical page index. + * + * @returns The physical page index, or null if it cannot be determined. + */ + public _findCurrentPhysicalPage(): string | null { + const cssSelector = readium.findFirstVisibleLocator()?.locations?.cssSelector; + let element = document.querySelector(cssSelector); if (element == null) { @@ -80,7 +98,7 @@ export class EpubPage { return this._getPhysicalPageIndexFromElement(element as HTMLElement); } - while (element.nodeType === Node.ELEMENT_NODE) { + while (!!element && element.nodeType === Node.ELEMENT_NODE) { const siblings = this._getAllSiblings(element); if (siblings == null) { return; diff --git a/flutter_readium/assets/_helper_scripts/src/types.ts b/flutter_readium/assets/_helper_scripts/src/types.ts index 670cc740..c39cd0bf 100644 --- a/flutter_readium/assets/_helper_scripts/src/types.ts +++ b/flutter_readium/assets/_helper_scripts/src/types.ts @@ -25,24 +25,25 @@ export interface ComicFramePosition { * Readium JS library injected by kotlin/swift-toolkit. **/ export interface Readium { - link: any; - isFixedLayout: boolean; - isReflowable: boolean; + get isFixedLayout(): boolean | undefined; + get isReflowable(): boolean | undefined; /** * @param progression // Position must be in the range [0 - 1], 0-100%. */ scrollToPosition(progression: number): void; - getColumnCountPerScreen(): void; - - isScrollModeEnabled(): boolean; - - isVerticalWritingMode(): boolean; - - // Scroll to the given TagId in document and snap. + /** + * Scroll to the given TagId in document and snap. + */ scrollToId(id: string): void; + /** + * Scrolls to the first occurrence of the given text snippet. + * + * The expected text argument is a Locator object, as defined here: + * https://readium.org/architecture/models/locators/ + */ scrollToLocator(locator: Locator): void; scrollToStart(): void; @@ -51,16 +52,14 @@ export interface Readium { scrollLeft(): void; - snapCurrentOffset(): void; - - rangeFromLocator(): Range; + scrollRight(): void; setCSSProperties(properties: Record): void; setProperty(key: string, value: string): void; removeProperty(key: string): void; - + getCurrentSelection(): CurrentSelection; registerDecorationTemplates(newStyles: Record): void; @@ -132,4 +131,10 @@ export interface CurrentSelectionRect { export interface CurrentSelection { text: CurrentSelectionText; rect: CurrentSelectionRect; -} \ No newline at end of file +} + +export interface PageInformation { + pageIndex: number | null; + totalPages: number | null; + physicalPageIndex: string | null; +} diff --git a/flutter_readium/assets/helpers/epub.css b/flutter_readium/assets/helpers/epub.css index c71da7d4..43474c5a 100644 --- a/flutter_readium/assets/helpers/epub.css +++ b/flutter_readium/assets/helpers/epub.css @@ -1 +1 @@ -[type=pagebreak]{border-top:1px solid;display:block;width:100%;line-height:100%;padding-top:8px;margin-top:40px;margin-bottom:20px;text-align:right;font-size:98%}span#activeLocation{border-radius:4px;background-color:var(--USER__highlightBackgroundColor) !important;color:var(--USER__highlightForegroundColor) !important}body>*:first-child{margin-top:50px !important}*{word-wrap:break-word}table{border-collapse:collapse;border-spacing:0;margin:15px 0;width:calc(100% - var(--RS__pageGutter)/2);word-break:break-word}table h1,table h2,table h3,table h4,table h5,table h6{margin:0}table *{font-size:1rem}table td,table th{border-collapse:collapse;border:1px solid #ccc;margin:0;padding:16px;vertical-align:top}table caption{margin-bottom:16px}table.transparent-table,table.docx-table,table.plain-table{table-layout:fixed;width:100%}table.transparent-table th,table.transparent-table td,table.docx-table th,table.docx-table td,table.plain-table th,table.plain-table td{width:auto;max-width:100%}table.transparent-table{border-width:0}table.transparent-table td,table.transparent-table th{border-width:0}table.has-first-row-headers tr:first-child{display:none}table.has-header{border:none !important}table.has-header tr{display:block;margin-bottom:25px}table.has-header tr td{border-top-width:0;display:block;width:100% !important;box-sizing:border-box}table.has-header tr td:first-child{border-top-width:1px}table.has-header tr td:not(.mobile-header):before{background-color:#7c7c7c;color:#fff;content:attr(data-th);display:block;margin:-16px -16px 5px;padding:8px 16px}table.has-header tr td.mobile-header{text-transform:uppercase;background-color:#241f20 !important}table.has-header tr td.mobile-header h6{color:#fff}table.has-header thead tr:first-child{display:none} +[type=pagebreak]{border-top:1px solid;display:block;width:100%;line-height:100%;padding-top:8px;margin-top:40px;margin-bottom:20px;text-align:right;font-size:98%}body>*:first-child{margin-top:50px !important}*{word-wrap:break-word}table{border-collapse:collapse;border-spacing:0;margin:15px 0;width:calc(100% - var(--RS__pageGutter)/2);word-break:break-word}table h1,table h2,table h3,table h4,table h5,table h6{margin:0}table *{font-size:1rem}table td,table th{border-collapse:collapse;border:1px solid #ccc;margin:0;padding:16px;vertical-align:top}table caption{margin-bottom:16px}table.transparent-table,table.docx-table,table.plain-table{table-layout:fixed;width:100%}table.transparent-table th,table.transparent-table td,table.docx-table th,table.docx-table td,table.plain-table th,table.plain-table td{width:auto;max-width:100%}table.transparent-table{border-width:0}table.transparent-table td,table.transparent-table th{border-width:0}table.has-first-row-headers tr:first-child{display:none}table.has-header{border:none !important}table.has-header tr{display:block;margin-bottom:25px}table.has-header tr td{border-top-width:0;display:block;width:100% !important;box-sizing:border-box}table.has-header tr td:first-child{border-top-width:1px}table.has-header tr td:not(.mobile-header):before{background-color:#7c7c7c;color:#fff;content:attr(data-th);display:block;margin:-16px -16px 5px;padding:8px 16px}table.has-header tr td.mobile-header{text-transform:uppercase;background-color:#241f20 !important}table.has-header tr td.mobile-header h6{color:#fff}table.has-header thead tr:first-child{display:none} diff --git a/flutter_readium/assets/helpers/epub.js b/flutter_readium/assets/helpers/epub.js index 85cd0b75..69955a43 100644 --- a/flutter_readium/assets/helpers/epub.js +++ b/flutter_readium/assets/helpers/epub.js @@ -1 +1 @@ -var epub;(()=>{var e={324(e,t,n){"use strict";n.r(t)},679(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const o=document.createElement("tbody"),r=document.createElement("tr"),i=document.createElement("tr");r.innerHTML=" ",i.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,r.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),i.appendChild(t)}})),o.appendChild(r),o.appendChild(i),e.appendChild(o)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const o=e.rows[0];o&&o.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=o,n=Array.from(o.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const o=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",o[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}},50(e){self,e.exports=(()=>{"use strict";var e={d:(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function n(e){return"object"==typeof e&&null!==e&&e.nodeType===Node.ELEMENT_NODE}e.r(t),e.d(t,{default:()=>K,getCssSelector:()=>X});const o={NONE:"",DESCENDANT:" ",CHILD:" > "},r={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},i="CssSelectorGenerator";function l(e="unknown problem",...t){console.warn(`${i}: ${e}`,...t)}const s={selectors:[r.id,r.class,r.tag,r.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function c(e){return e instanceof RegExp}function a(e){return["string","function"].includes(typeof e)||c(e)}function u(e){return Array.isArray(e)?e.filter(a):[]}function d(e){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(e){return e instanceof Node}(e)&&t.includes(e.nodeType)}function f(e,t){if(d(e))return e.contains(t)||l("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will not work as intended."),e;const n=t.getRootNode({composed:!1});return d(n)?(n!==document&&l("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):y(t)}function h(e){return"number"==typeof e?e:Number.POSITIVE_INFINITY}function g(e=[]){const[t=[],...n]=e;return 0===n.length?t:n.reduce(((e,t)=>e.filter((e=>t.includes(e)))),t)}function m(e){return[].concat(...e)}function p(e){const t=e.map((e=>{if(c(e))return t=>e.test(t);if("function"==typeof e)return t=>{const n=e(t);return"boolean"!=typeof n?(l("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",e),!1):n};if("string"==typeof e){const t=new RegExp("^"+e.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return e=>t.test(e)}return l("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",e),()=>!1}));return e=>t.some((t=>t(e)))}function _(e,t,n){const o=Array.from(f(n,e[0]).querySelectorAll(t));return o.length===e.length&&e.every((e=>o.includes(e)))}function v(e,t){t=null!=t?t:y(e);const o=[];let r=e;for(;n(r)&&r!==t;)o.push(r),r=r.parentElement;return o}function b(e,t){return g(e.map((e=>v(e,t))))}function y(e){return e.ownerDocument.querySelector(":root")}const N=", ",S=new RegExp(["^$","\\s"].join("|")),E=new RegExp(["^$"].join("|")),C=[r.nthoftype,r.tag,r.id,r.class,r.attribute,r.nthchild],L=p(["class","id","ng-*"]);function T({name:e}){return`[${e}]`}function w({name:e,value:t}){return`[${e}='${t}']`}function x({nodeName:e,nodeValue:t}){return{name:H(e),value:H(null!=t?t:void 0)}}function P(e){const t=Array.from(e.attributes).filter((t=>function({nodeName:e,nodeValue:t},n){const o=n.tagName.toLowerCase();return!(["input","option"].includes(o)&&"value"===e||"src"===e&&(null==t?void 0:t.startsWith("data:"))||L(e))}(t,e))).map(x);return[...t.map(T),...t.map(w)]}function O(e){var t;return(null!==(t=e.getAttribute("class"))&&void 0!==t?t:"").trim().split(/\s+/).filter((e=>!E.test(e))).map((e=>`.${H(e)}`))}function A(e){var t;const n=null!==(t=e.getAttribute("id"))&&void 0!==t?t:"",o=`#${H(n)}`,r=e.getRootNode({composed:!1});return!S.test(n)&&_([e],o,r)?[o]:[]}function I(e){const t=e.parentNode;if(t){const o=Array.from(t.childNodes).filter(n).indexOf(e);if(o>-1)return[`:nth-child(${String(o+1)})`]}return[]}function R(e){return[H(e.tagName.toLowerCase())]}function $(e){const t=[...new Set(m(e.map(R)))];return 0===t.length||t.length>1?[]:[t[0]]}function M(e){const t=$([e])[0],n=e.parentElement;if(n){const o=Array.from(n.children).filter((e=>e.tagName.toLowerCase()===t)),r=o.indexOf(e);if(r>-1)return[`${t}:nth-of-type(${String(r+1)})`]}return[]}function k(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){return Array.from(function*(e=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){let n=0,o=F(1);for(;o.length<=e.length&&ne[t]));yield t,o=D(o,e.length-1)}}(e,{maxResults:t}))}function D(e=[],t=0){const n=e.length;if(0===n)return[];const o=[...e];o[n-1]+=1;for(let e=n-1;e>=0;e--)if(o[e]>t){if(0===e)return F(n+1);o[e-1]++,o[e]=o[e-1]+1}return o[n-1]>t?F(n+1):o}function F(e=1){return Array.from(Array(e).keys())}const j=":".charCodeAt(0).toString(16).toUpperCase(),q=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function H(e=""){return CSS?CSS.escape(e):function(e=""){return e.split("").map((e=>":"===e?`\\${j} `:q.test(e)?`\\${e}`:escape(e).replace(/%/g,"\\"))).join("")}(e)}const V={tag:$,id:function(e){return 0===e.length||e.length>1?[]:A(e[0])},class:function(e){return g(e.map(O))},attribute:function(e){return g(e.map(P))},nthchild:function(e){return g(e.map(I))},nthoftype:function(e){return g(e.map(M))}},B={tag:R,id:A,class:O,attribute:P,nthchild:I,nthoftype:M};function W(e){return e.includes(r.tag)||e.includes(r.nthoftype)?[...e]:[...e,r.tag]}function U(e={}){const t=[...C];return e[r.tag]&&e[r.nthoftype]&&t.splice(t.indexOf(r.tag),1),t.map((t=>{return(o=e)[n=t]?o[n].join(""):"";var n,o})).join("")}function Y(e,t,n="",r){const i=function(e,t){return""===t?e:function(e,t){return[...e.map((e=>t+o.DESCENDANT+e)),...e.map((e=>t+o.CHILD+e))]}(e,t)}(function(e,t,n){const o=function(e,t){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=t,l=p(n),s=p(o);return function(e){const{selectors:t,includeTag:n}=e,o=[...t];return n&&!o.includes("tag")&&o.push("tag"),o}(t).reduce(((t,n)=>{const o=function(e,t){return(0,V[t])(e)}(e,n),c=function(e=[],t,n){return e.filter((e=>n(e)||!t(e)))}(o,l,s),a=function(e=[],t){return e.sort(((e,n)=>{const o=t(e),r=t(n);return o&&!r?-1:!o&&r?1:0}))}(c,s);return t[n]=r?k(a,{maxResults:i}):a.map((e=>[e])),t}),{})}(e,n),r=function(e,t){return function(e){const{selectors:t,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=e,i=n?k(t,{maxResults:r}):t.map((e=>[e]));return o?i.map(W):i}(t).map((t=>function(e,t){const n={};return e.forEach((e=>{const o=t[e];o&&o.length>0&&(n[e]=o)})),function(e={}){let t=[];return Object.entries(e).forEach((([e,n])=>{t=n.flatMap((n=>0===t.length?[{[e]:n}]:t.map((t=>Object.assign(Object.assign({},t),{[e]:n})))))})),t}(n).map(U)}(t,e))).filter((e=>e.length>0))}(o,n),i=m(r);return[...new Set(i)]}(e,0,r),n);for(const n of i)if(_(e,n,t))return n;return null}function G(e){return{value:e,include:!1}}function z({selectors:e,operator:t}){let n=[...C];e[r.tag]&&e[r.nthoftype]&&(n=n.filter((e=>e!==r.tag)));let o="";return n.forEach((t=>{var n;(null!==(n=e[t])&&void 0!==n?n:[]).forEach((({value:e,include:t})=>{t&&(o+=e)}))})),t+o}function J(e){return[":root",...v(e).reverse().map((e=>{const t=function(e,t,n=o.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(e,t){return B[t](e)}(e,t).map(G))})),{element:e,operator:n,selectors:r}}(e,[r.nthchild],o.CHILD);return t.selectors.nthchild.forEach((e=>{e.include=!0})),t})).map(z)].join("")}function X(e,t={}){var o;const i=function(e){(e instanceof NodeList||e instanceof HTMLCollection)&&(e=Array.from(e));const t=(Array.isArray(e)?e:[e]).filter(n);return[...new Set(t)]}(e),l=function(e,t={}){const n=Object.assign(Object.assign({},s),t);return{selectors:(o=n.selectors,Array.isArray(o)?o.filter((e=>{return t=r,n=e,Object.values(t).includes(n);var t,n})):[]),whitelist:u(n.whitelist),blacklist:u(n.blacklist),root:f(n.root,e),combineWithinSelector:!!n.combineWithinSelector,combineBetweenSelectors:!!n.combineBetweenSelectors,includeTag:!!n.includeTag,maxCombinations:h(n.maxCombinations),maxCandidates:h(n.maxCandidates)};var o}(i[0],t),c=null!==(o=l.root)&&void 0!==o?o:y(i[0]);let a="",d=c;function g(){return function(e,t,n="",o){if(0===e.length)return null;const r=[e.length>1?e:[],...b(e,t).map((e=>[e]))];for(const e of r){const r=Y(e,t,n,o);if(r)return{foundElements:e,selector:r}}return null}(i,d,a,l)}let m=g();for(;m;){const{foundElements:e,selector:t}=m;if(_(i,t,c))return t;d=e[0],a=t,m=g()}return i.length>1?i.map((e=>X(e,l))).join(N):function(e){return e.map(J).join(N)}(i)}const K=X;return t})()}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var o={};(()=>{"use strict";var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(50),r=n(679);n(324);class i{constructor(){this._headingTagNames=["h1","h2","h3","h4","h5","h6"],this._activeLocationId="activeLocation",this._locationTag="span",this._documentRange=document.createRange()}getAllElementIds(){return[...document.querySelectorAll("[id]")].map((e=>e.id))}scrollToLocations(e,t,n){try{const o=this._processLocations(e);if(null!=o)return this._scrollToProcessedRange(o,t,n),!0;const r=e.progression;if(null!=r)return null===readium||void 0===readium||readium.scrollToPosition(r),!0;this._debugLog("ScrollToLocations: Unknown range",e)}catch(e){this._errorLog(e)}return!1}isLocatorVisible(e){var t,n,o;this._debugLog(e);try{const r=e.locations,i=null!==(t=r.cssSelector)&&void 0!==t?t:null===(o=null===(n=r.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;if(this._isComicBook()){const e=null!=document.querySelector(i);return this._debugLog("Comic book",r,{found:e,selector:i}),e}const l=this._processLocations(r);return null==l?(this._debugLog("isLocatorVisible: Unknown range",r),!1):this._isProcessedRangeVisible(l)&&!!document.querySelector(`${i} #${this._activeLocationId}`)}catch(e){return this._errorLog(e),!0}}getLocatorFragments(e,t){var n,o,r,i;try{const l=null!==(o=null===(n=null==e?void 0:e.locations)||void 0===n?void 0:n.cssSelector)&&void 0!==o?o:this._findFirstVisibleCssSelector();if(null==l||!(null==l?void 0:l.length))return this._debugLog("getLocatorFragments: selector not found, returning locator from args"),e;const s=[...this._getPageFragments(t),...this._getTocFragments(l),...this._getPhysicalPageFragments(l)];return Object.assign(Object.assign({},e),{locations:Object.assign(Object.assign({cssSelector:l},e.locations),{fragments:[...null!==(i=null===(r=e.locations)||void 0===r?void 0:r.fragments)&&void 0!==i?i:[],...s]})})}catch(t){return this._errorLog(t),e}}_isComicBook(){try{return!!(null===comicBookPage||void 0===comicBookPage?void 0:comicBookPage.isComicBook())}catch(e){return!1}}_isTextNode(e){const t=e.nodeType;return 3===t||4===t}_clamp(e,t,n){return Math.min(Math.max(e,t),n)}_findAndSplitOffset(e,t){return function e(n){if(3===n.nodeType||4===n.nodeType){const e=n;if(t<=0)return e;const o=e.length;if(t=0;){if(""!==n[t].trim())return{node:e,charOffset:t};--t}t=void 0}e=this._previousNode(e)}}_findNonWhitespace(e,t){var n;return null!==(n=this._findNonWhitespaceBackward(e,t))&&void 0!==n?n:this._findNonWhitespaceForward(e,t)}_safeVisibleRect(e){const{innerWidth:t,innerHeight:n}=window;return e?{left:0,top:.05*n,right:t,bottom:.95*n}:{left:0,top:0,right:t,bottom:n}}*_descendentTextNodes(e){if(this._isTextNode(e))yield e;else for(const t of e.childNodes)yield*this._descendentTextNodes(t)}_findTextPosition(e,t){if(null!=e)if(t<0||isNaN(t))this._errorLog(`findTextPosition: invalid charOffset, node=${e.nodeValue}, charOffset=${t}`);else{if(0===t)return{node:e,charOffset:t};for(const n of this._descendentTextNodes(e)){const e=n.length;if(t<=e)return{node:n,charOffset:t};t-=e}this._errorLog(`findTextPosition: failed, node=${this._debugNode(e)}, charOffset=${t}`)}else this._errorLog(`findTextPosition: no node, charOffset=${t}`)}_processDomRange(e){const{start:t,end:n}=e,{cssSelector:o,charOffset:r}=t,{cssSelector:i,charOffset:l}=null!=n&&void 0!==n?n:t,s=document.querySelector(o),c=this._findTextPosition(s,null!=r?r:0);if(null==c)return void this._errorLog(`DomRange bad start, selector=${o}`);const a=i===o?s:document.querySelector(i),u=this._findTextPosition(a,null!=l&&void 0!==l?l:0);if(null!=u){try{this._documentRange.setStart(c.node,c.charOffset)}catch(e){this._errorLog(`${this._debugNode(c.node)}, ${c.charOffset}`,e),this._documentRange.setStartAfter(c.node)}try{this._documentRange.setEnd(u.node,u.charOffset)}catch(e){this._errorLog(`${this._debugNode(u.node)}, ${u.charOffset}`,e),this._documentRange.setEndAfter(u.node)}if(0===this._documentRange.getClientRects().length){const e=this._findNonWhitespace(c.node,c.charOffset);if(null==e)return void this._errorLog("Couldn't find any non-whitespace characters in the document!'");const{node:t,charOffset:n}=e;this._documentRange.setStart(t,n),this._documentRange.setEnd(t,n+1)}return this._documentRange}this._errorLog(`DomRange bad end, selector=${i}`)}_processCssSelector(e){const t=document.querySelector(e);if(null!=t)return"none"===window.getComputedStyle(t).display&&(t.style.display=this._isPageBreakElement(t)?"flex":"block"),this._documentRange.selectNode(t),this._documentRange;this._errorLog(`processCssSelector: error: node not found ${e}`)}_processLocations(e){var t,n,o;if(null==e)return void this._errorLog("location not set");if(e.domRange)return this._processDomRange(e.domRange);const r=null!==(t=e.cssSelector)&&void 0!==t?t:null===(o=null===(n=e.domRange)||void 0===n?void 0:n.start)||void 0===o?void 0:o.cssSelector;return r?this._processCssSelector(r):void 0}_scrollToProcessedRange(e,t,n){!n&&this._isProcessedRangeVisible(e)||this._scrollToBoundingClientRect(e,t)}_scrollToBoundingClientRect(e,t){const{top:n,right:o,bottom:r,left:i}=e.getBoundingClientRect();if(0===n&&0===o&&0===r&&0===i)return void this._debugLog("scrollToBoundingClientRect: Scrolling to defective bounding rect, abort! ",e.getClientRects(),e.getClientRects().length);const{scrollLeft:l,scrollWidth:s,scrollTop:c,scrollHeight:a}=document.scrollingElement;if(t){const{top:e,bottom:o}=this._safeVisibleRect(t);if(no){const t=this._clamp((c+n-e)/a,0,1);null===readium||void 0===readium||readium.scrollToPosition(t)}}else{const e=(l+.5*(i+o))/s;null===readium||void 0===readium||readium.scrollToPosition(e)}}_isProcessedRangeVisible(e){const{innerWidth:t,innerHeight:n}=window,{top:o,right:r,bottom:i,left:l}=e.getBoundingClientRect();return oe.includes("duration=")));if(!t)return void this._errorLog("Duration fragment not found.");const n=/duration=(\d+(?:\.\d+)?)/.exec(t);return n?(this._debugLog("Duration fragment:",n[1]),parseFloat(n[1])):void this._errorLog("Invalid duration format.")}catch(e){return void this._errorLog("Could not retrieve duration fragment!")}}_getTocFragments(e){var t;try{const n=null===(t=this._findPrecedingAncestorSiblingHeadings(e)[0])||void 0===t?void 0:t.id;return null==n?[]:[`toc=${n}`]}catch(e){return this._errorLog(e),[]}}_getPhysicalPageFragments(e){try{const t=this._findCurrentPhysicalPage(e);return null==t?[]:[`physicalPage=${t}`]}catch(t){return this._errorLog(`Selector:${e} -- ${t}`),[]}}_findPrecedingAncestorSiblingHeadings(e){var t,n;const o=document.querySelector(e),r=null!==(t=null==o?void 0:o.querySelectorAll(this._headingTagNames.join(","))[0])&&void 0!==t?t:o;if(null==r)return;if(!this._allHeadings){const e=Array.from(window.document.querySelectorAll(this._headingTagNames.join(",")));for(const t of e)if(t){const e=t,n=e.textContent||e.getAttribute("title")||e.getAttribute("aria-label");let o=e.getAttribute("id");if(!o){let t,n=e;for(;(t=n.parentNode)&&(null==t?void 0:t.nodeType)===Node.ELEMENT_NODE&&t.firstElementChild===n;){const e=t.getAttribute("id");if(e){o=e;break}n=t}}const r={element:e,id:o||null,level:parseInt(e.localName.substring(1),10),text:n};this._allHeadings||(this._allHeadings=[]),this._allHeadings.push(r)}this._allHeadings||(this._allHeadings=[])}let i;for(let e=this._allHeadings.length-1;e>=0;e--){const t=this._allHeadings[e],n=r.compareDocumentPosition(t.element);(0===n||n&Node.DOCUMENT_POSITION_PRECEDING||n&Node.DOCUMENT_POSITION_CONTAINS)&&(i||(i=[]),(null==t?void 0:t.id)&&i.push({id:t.id,level:t.level,text:t.text}))}if(null==i?void 0:i.length)return i;const l=null!==(n=o.closest("section"))&&void 0!==n?n:o.closest("body");return l?[{id:l.id,level:0,text:l.innerText}]:void 0}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(e){var t;let n=document.querySelector(e);if(null!=n){if(this._isPageBreakElement(n))return this._getPhysicalPageIndexFromElement(n);for(;n.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(n);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(n)));t>=0;t--){const n=e[t],o=this._findPhysicalPageIndex(n);if(null!=o)return o}if(n=n.parentNode,null==n||"body"===n.nodeName.toLowerCase())return null===(t=document.querySelector("head [name='webpub:currentPage']"))||void 0===t?void 0:t.getAttribute("content")}}}_findFirstVisibleCssSelector(){return this._getCssSelector(this._getFirstVisibleElement())}_getCssSelector(e){var n,o;try{const r=(0,t.getCssSelector)(e,{root:document.querySelector("body")}),i=null!==(o=null===(n=null==r?void 0:r.replace(":root > :nth-child(2)","body"))||void 0===n?void 0:n.trim())&&void 0!==o?o:"body";return this._debugLog(i),i}catch(e){return this._errorLog(e),"body"}}_getFirstVisibleElement(){const e=this._findFirstVisibleElement(document.body);return this._debugLog("First visible element:",{tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className}),e}_findFirstVisibleElement(e){const t={tagName:e.nodeName.toLocaleLowerCase(),id:e.id,className:e.className};for(const n of e.children){const o={tagName:n.nodeName.toLocaleLowerCase(),id:n.id,className:n.className};if(this._isElementVisible(n))if(this._shouldIgnoreElement(n))this._debugLog("Element is ignored - continue",o);else{if(n.id.includes(`${this._activeLocationId}`))return this._debugLog("Child is an active location element, return closest element with id",{childData:o,nodeData:t}),e.id?e:this._findClosestElementWithId(n);if(n.hasChildNodes())return this._debugLog("Loop into children",o),this._findFirstVisibleElement(n);if(!n.id)return this._debugLog("Element has no ID attribute - return closest element with id",o),e.id?e:this._findClosestElementWithId(n)}}return this._debugLog("return:",t),e}_findClosestElementWithId(e){let t=e.parentElement;for(;null!==t;){if(t.id)return t;t=t.parentElement}return this._debugLog("No element with id attr found!"),e}_isElementVisible(e,t=!1){if(null===readium||void 0===readium?void 0:readium.isFixedLayout)return!0;if(e===document.body||e===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;const n=e.getBoundingClientRect();return t?this._isScrollModeEnabled()?n.top>=0&&n.top<=document.documentElement.clientHeight:n.left>=1:this._isScrollModeEnabled()?n.bottom>0&&n.top0&&n.lefte instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_debugNode(e){var t,n;if(e instanceof Node){return(new XMLSerializer).serializeToString(e)}if("innerHTML"in e||"textContent"in e){const o=e;return null!==(n=null!==(t=o.innerHTML)&&void 0!==t?t:o.textContent)&&void 0!==n?n:"?"}}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function l(){window.epubPage||((0,r.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",l),window.epubPage=new i)}e.EpubPage=i,"loading"!==document.readyState?window.setTimeout(l):document.addEventListener("DOMContentLoaded",l)})(),epub=o})(); \ No newline at end of file +var epub;(()=>{"use strict";var e={324(e,t,n){n.r(t)},679(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const r=document.createElement("tbody"),l=document.createElement("tr"),o=document.createElement("tr");l.innerHTML=" ",o.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,l.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),o.appendChild(t)}})),r.appendChild(l),r.appendChild(o),e.appendChild(r)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const r=e.rows[0];r&&r.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=r,n=Array.from(r.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const r=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",r[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function n(r){var l=t[r];if(void 0!==l)return l.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};(()=>{var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(679);n(324);class l{get _isScrollModeEnabled(){return"readium-scroll-on"===getComputedStyle(document.documentElement).getPropertyValue("--USER__view")}getPageInformation(){const e=this._findCurrentPhysicalPage();if(null===readium||void 0===readium?void 0:readium.isReflowable){if(this._isScrollModeEnabled)return{pageIndex:null,totalPages:null,physicalPageIndex:e};const{scrollLeft:t,scrollWidth:n}=document.scrollingElement,{innerWidth:r}=window;return{pageIndex:this._isScrollModeEnabled?null:Math.round(t/r)+1,totalPages:this._isScrollModeEnabled?null:Math.round(n/r),physicalPageIndex:e}}return{pageIndex:1,totalPages:1,physicalPageIndex:e}}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(){var e,t,n;const r=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector;let l=document.querySelector(r);if(null!=l){if(this._isPageBreakElement(l))return this._getPhysicalPageIndexFromElement(l);for(;l&&l.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(l);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(l)));t>=0;t--){const n=e[t],r=this._findPhysicalPageIndex(n);if(null!=r)return r}if(l=l.parentNode,null==l||"body"===l.nodeName.toLowerCase())return null===(n=document.querySelector("head [name='webpub:currentPage']"))||void 0===n?void 0:n.getAttribute("content")}}}_log(...e){this._isIos()?null===webkit||void 0===webkit||webkit.messageHandlers.log.postMessage([].slice.call(e).map((e=>e instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function o(){window.epubPage||((0,t.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",o),window.epubPage=new l)}e.EpubPage=l,"loading"!==document.readyState?window.setTimeout(o):document.addEventListener("DOMContentLoaded",o)})(),epub=r})(); \ No newline at end of file From cacd7fdc6f38ebe56eb8d65e71554ea92f697c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Tue, 3 Mar 2026 14:12:54 +0100 Subject: [PATCH 09/19] fix: findCssSelectorForLocator already returned the first cssSelector it found --- .../nota/flutter_readium/ReadiumExtensions.kt | 20 ++++++++++++++----- .../dk/nota/flutter_readium/ReadiumReader.kt | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index b5421c8c..e21cb4ac 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -310,6 +310,7 @@ suspend fun Publication.findAllCssSelectors(href: Url): List? { * Find the cssSelector for a locator. If it already have one return it, otherwise we need to look it up. */ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { + // If our locator already has a cssSelector, use it. locator.locations.cssSelector?.takeIf { it.startsWith("#") }?.let { return it } val contentService = findService(ContentService::class) ?: run { @@ -318,6 +319,8 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { } val cleanHref = locator.href.cleanHref() + + val locatorProgress = locator.locations.progression ?: 0.0 for (element in contentService.content(locator)) { if (element !is Content.TextElement) { continue @@ -328,10 +331,16 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { break } - val cssSelector = - element.locator.locations.cssSelector?.takeIf { it.startsWith("#") } ?: continue + val progression = + element.locator.locations.progression + ?: 0.0 - return cssSelector + if (progression < locatorProgress) continue + + // Return the first cssSelector that starts with # after tha progression has been reached. + element.locator.locations.cssSelector?.takeIf { it.startsWith("#") }?.let { + return it + } } return null @@ -365,5 +374,6 @@ val Href.fragmentParameters: Map * * https://www.w3.org/TR/media-frags/ */ -val Href.time: Duration? get() = - fragmentParameters["t"]?.toIntOrNull()?.seconds +val Href.time: Duration? + get() = + fragmentParameters["t"]?.toIntOrNull()?.seconds diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index d663aa22..a0be9a0e 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -664,7 +664,7 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua val cleanHref = resultLocator.href.cleanHref() val toc = publication.tableOfContents.flatten().filter { it.href.resolve().cleanHref() == cleanHref - }.associateBy { contentIds.indexOf("${it.href.resolve().fragment}") } + }.associateBy { contentIds.indexOf("#${it.href.resolve().fragment}") } val tocItem = toc.entries.lastOrNull { it.key <= idx }?.value ?: toc.entries.firstOrNull()?.value ?: run { From 149588657877d597ccdcfd2acaaad8200610363f Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Tue, 3 Mar 2026 17:03:32 +0100 Subject: [PATCH 10/19] feat(iOS): use PageInformation and find toc link --- .../FlutterReadiumPlugin.swift | 83 +++++++++++++ .../flutter_readium/ReadiumReaderView.swift | 87 ++++--------- .../model/PageInformation.swift | 48 +++++++ .../utils/ReadiumExtensions.swift | 117 ++++++++++++++++++ 4 files changed, 271 insertions(+), 64 deletions(-) create mode 100644 flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift index 1577127b..b577ec02 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift @@ -32,6 +32,10 @@ public class FlutterReadiumPlugin: NSObject, FlutterPlugin, ReadiumShared.Warnin /// Timebased Navigator. Can be TTS, Audio or MediaOverlay implementations. internal var timebasedNavigator: FlutterTimebasedNavigator? = nil + /// For EPUB profile, maps document path to a list of all the cssSelectors in the document. + /// This is used to find the current toc item. + private var currentPublicationCssSelectorMap: [String: [String]]? + lazy var fallbackChapterTitle: LocalizedString = LocalizedString.localized([ "en": "Chapter", "da": "Kapitel", @@ -510,6 +514,85 @@ extension FlutterReadiumPlugin { currentPublication?.close() currentPublication = nil currentPublicationUrlStr = nil + currentPublicationCssSelectorMap = [:] + } + } +} + +/// Extension for finding current ToC location +extension FlutterReadiumPlugin { + + /// Find the current table of content item from a locator. + func epubFindCurrentToc(locator: Locator) async throws -> Locator { + guard let publication = currentPublication else { + debugPrint(TAG, ":epubFindCurrentToc, no currentPublication") + return locator + } + + guard let cssSelector = await publication.findCssSelectorForLocator(locator: locator) else { + debugPrint(TAG, ":epubFindCurrentToc, missing cssSelector in locator") + return locator + } + debugPrint(TAG, ":epubFindCurrentToc, found current selector: \(cssSelector)") + + let cleanHrefPath = locator.href.path + + var resultLocator = locator.copy() + resultLocator.locations.otherLocations["cssSelector"] = cssSelector + + let contentIds = try await epubGetAllDocumentCssSelectors(hrefPath: cleanHrefPath) + + guard let idx = contentIds.firstIndex(of: cssSelector) else { + debugPrint(TAG, ":epubFindCurrentToc cssSelector:\(cssSelector) not found in contentIds") + return resultLocator + } + + guard let tocLinksFlattened = try? await publication.getFlattenedToC() else { + debugPrint(TAG, ":epubFindCurrentToc failed to retrieve ToC") + return resultLocator + } + + let toc = Dictionary( + uniqueKeysWithValues: + tocLinksFlattened + .filter { RelativeURL(epubHREF: $0.href)?.path == cleanHrefPath } + .compactMap { item -> (Int, Link)? in + let fragment = RelativeURL(epubHREF: item.href)?.fragment ?? "" + guard let index = contentIds.firstIndex(of: "#\(fragment)") else { return nil } + return (index, item) + } + ) + + let tocItem = toc.filter { $0.key <= idx } + .sorted { $0.key < $1.key } + .last?.value + ?? toc.sorted { $0.key < $1.key }.first?.value + + guard let tocItem else { + debugPrint(TAG, ":epubFindCurrentToc - no tocItem found") + return resultLocator } + + debugPrint(TAG, ":epubFindCurrentToc - found tocItem: \(tocItem.title), href:\(tocItem.href)") + + resultLocator.locations.otherLocations["toc"] = tocItem.href + + return resultLocator.copy(title: tocItem.title) + } + + /// Get all cssSelectors for an EPUB file. + func epubGetAllDocumentCssSelectors(hrefPath: String) async throws -> [String] { + if currentPublicationCssSelectorMap == nil { + currentPublicationCssSelectorMap = [:] + } + + if let cached = currentPublicationCssSelectorMap?[hrefPath] { + return cached + } + + let selectors = await currentPublication?.findAllCssSelectors(hrefRelativePath: hrefPath) ?? [] + + currentPublicationCssSelectorMap?[hrefPath] = selectors + return selectors } } diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift index 6537de8b..c91a76c0 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift @@ -35,7 +35,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele private let channel: ReadiumReaderChannel private let _view: UIView private let readiumViewController: EPUBNavigatorViewController - private var isVerticalScroll = false private var hasSentReady = false var publicationIdentifier: String? @@ -252,24 +251,26 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele } private func setUserPreferences(preferences: EPUBPreferences) { - isVerticalScroll = preferences.scroll ?? false self.readiumViewController.submitPreferences(preferences) } private func emitOnPageChanged(locator: Locator) -> Void { - let json = locator.jsonString ?? "null" - print(TAG, "emitOnPageChanged:locator=\(String(describing: locator))") - Task.detached(priority: .high) { [isVerticalScroll] in - guard let locatorWithFragments = await self.getLocatorFragments(json, isVerticalScroll) else { - print(TAG, "emitOnPageChanged failed!") - return + Task.detached(priority: .high) { [locator] in + + // Map the ToC location into the locator. + var resultLocator = (try? await FlutterReadiumPlugin.instance?.epubFindCurrentToc(locator: locator)) ?? locator + // Get information about current page. + if let pageInfo = await self.getPageInformation() { + resultLocator.locations.otherLocations.merge(pageInfo.otherLocations, uniquingKeysWith: { lhs, rhs in lhs }) } + + let finalLocator = resultLocator await MainActor.run() { - self.channel.onPageChanged(locator: locatorWithFragments) + self.channel.onPageChanged(locator: finalLocator) FlutterReadiumPlugin.instance?.textLocatorStreamHandler? - .sendEvent(locatorWithFragments.jsonString) + .sendEvent(finalLocator.jsonString) } } } @@ -283,44 +284,22 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele } } - internal func getLocatorFragments(_ locatorJson: String, _ isVerticalScroll: Bool) async -> Locator? { - switch await self.evaluateJavascript("window.epubPage.getLocatorFragments(\(locatorJson), \(isVerticalScroll));") { - case .success(let jresult): - let locatorWithFragments = try! Locator(json: jresult as? Dictionary, warnings: readiumBugLogger)! - return locatorWithFragments - case .failure(let err): - print(TAG, "getLocatorFragments failed! \(err)") - return nil - } - } - - private func scrollTo(locations: Locator.Locations, toStart: Bool) async -> Void { - let json = locations.jsonString ?? "null" - print(TAG, "scrollTo: Go to locations \(json), toStart: \(toStart)") - - let _ = await evaluateJavascript("window.epubPage.scrollToLocations(\(json),\(isVerticalScroll),\(toStart));") + internal func getPageInformation() async -> PageInformation? { + switch await self.evaluateJavascript("window.epubPage.getPageInformation();") { + case .success(let jresult): + let pageInfo = PageInformation.fromJson(jresult as? Dictionary ?? Dictionary()) + return pageInfo + case .failure(let err): + print(TAG, "getPageInformation failed! \(err)") + return nil + } } func goToLocator(locator: Locator, animated: Bool) async -> Void { - let locations = locator.locations - let shouldScroll = canScroll(locations: locations) - let shouldGo = readiumViewController.currentLocation?.href != locator.href let readiumViewController = self.readiumViewController - if shouldGo { - print(TAG, "goToLocator: Go to \(locator.href)") - let goToSuccees = await readiumViewController.go(to: locator, options: NavigatorGoOptions(animated: animated)) - if (goToSuccees && shouldScroll) { - await self.scrollTo(locations: locations, toStart: false) - self.emitOnPageChanged() - } - } else { - print(TAG, "goToLocator: Already there, Scroll to \(locator.href)") - if (shouldScroll) { - await self.scrollTo(locations: locations, toStart: false) - self.emitOnPageChanged() - } - } + let goToSuccees = await readiumViewController.go(to: locator, options: NavigatorGoOptions(animated: animated)) + //self.emitOnPageChanged() } func justGoToLocator(_ locator: Locator, animated: Bool) async -> Bool { @@ -332,6 +311,7 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele print(TAG, "emitOnPageChanged: currentLocation = nil!") return } + print(TAG, "emitOnPageChanged: Calling navigator:locationDidChange.") navigator(readiumViewController, locationDidChange: locator) } @@ -374,22 +354,6 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele } } break - case "getLocatorFragments": - let args = call.arguments as? String ?? "null" - Task.detached(priority: .high) { - do { - let data = try await self.evaluateJavascript("window.epubPage.getLocatorFragments(\(args), true);").get() - await MainActor.run() { - return result(data) - } - } catch (let err) { - print(TAG, "getLocatorFragments error \(err)") - await MainActor.run() { - return result(false) - } - } - } - break case "setPreferences": let args = call.arguments as! [String: String] print(TAG, "onMethodCall[setPreferences] args = \(args)") @@ -459,8 +423,3 @@ func initUserScripts(registrar: FlutterPluginRegistrar) { /// Add simple script used by our JS to detect OS userScripts.append(WKUserScript(source: "const isAndroid=false,isIos=true;", injectionTime: .atDocumentStart, forMainFrameOnly: false)) } - -private func canScroll(locations: Locator.Locations?) -> Bool { - guard let locations = locations else { return false } - return locations.domRange != nil || locations.cssSelector != nil || locations.progression != nil -} diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift new file mode 100644 index 00000000..d69f4edd --- /dev/null +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift @@ -0,0 +1,48 @@ +import Foundation + +final class PageInformation { + + let pageIndex: Int64? + let totalPages: Int64? + let physicalPageIndex: String? + + init(pageIndex: Int64?, totalPages: Int64?, physicalPageIndex: String?) { + self.pageIndex = pageIndex + self.totalPages = totalPages + self.physicalPageIndex = physicalPageIndex + } + + var otherLocations: [String: Any] { + var res: [String: Any] = [:] + + if let pageIndex, let totalPages { + res["currentPage"] = pageIndex + res["totalPages"] = totalPages + } + + if let physicalPageIndex, + !physicalPageIndex.isEmpty { + res["physicalPage"] = physicalPageIndex + } + + return res + } + + static func fromJson(_ jsonString: String) throws -> PageInformation { + let data = Data(jsonString.utf8) + let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:] + return fromJson(object) + } + + static func fromJson(_ json: [String: Any]) -> PageInformation { + let pageIndex = json["pageIndex"] as? NSNumber + let totalPages = json["totalPages"] as? NSNumber + let physicalPageIndex = json["physicalPageIndex"] as? String + + return PageInformation( + pageIndex: pageIndex?.int64Value, + totalPages: totalPages?.int64Value, + physicalPageIndex: physicalPageIndex + ) + } +} diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift index e1f3f9f2..b50d4906 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift @@ -22,6 +22,119 @@ extension Publication { var containsMediaOverlays: Bool { self.readingOrder.contains(where: { $0.alternates.contains(where: { $0.mediaType?.matches(MediaType("application/vnd.syncnarr+json")) == true })}) } + + func getFlattenedToC() async -> [Link] { + switch await self.tableOfContents() { + case .success(let toc): + return toc.flatMap{ $0.flatten() } + case .failure(let err): + debugPrint("failed to retrieve ToC: \(err)") + return [] + } + } + + func searchInContentForQuery(_ query: String) async -> [LocatorCollection] { + guard let searchService: SearchService = findService(SearchService.self) else { + debugPrint("No SearchService available") + return [] + } + var collections: [LocatorCollection] = [] + switch await searchService.search(query: query, options: .init()) { + case .failure(let err): + switch err { + case .badQuery(let queryErr): + debugPrint("Search failed, bad query: \(queryErr)") + case .reading(let readErr): + debugPrint("Search failed, reading error: \(readErr)") + case .publicationNotSearchable: + debugPrint("Search failed, publication is not searchable") + } + case .success(let iterator): + _ = await iterator.forEach { collection in + collections.append(collection) + } + } + return collections + } + + /** + * Helper for getting all cssSelectors for a HTML document in the Publication. + */ + func findAllCssSelectors(hrefRelativePath: String) async -> [String] { + if (!self.conforms(to: Publication.Profile.epub)) { + debugPrint("This only works for EPUBs") + return [] + } + guard let contentService: ContentService = findService(ContentService.self) else { + debugPrint("No ContentService available") + return [] + } + let cleanHref = hrefRelativePath, + startLocator = Locator(href: RelativeURL(string: cleanHref)!, mediaType: MediaType.xhtml) + + let content = contentService.content(from: startLocator)?.iterator() + if (content == nil) { + debugPrint("No content iterator obtained from ContentService") + return [] + } + + var ids = [] as [String] + + while let element: ContentElement? = try? await content?.next() { + if (element == nil) { + continue + } + if (element?.locator.href.path != cleanHref) { + break + } + + if let cssSelector = element?.locator.locations.cssSelector.takeIf({ $0.hasPrefix("#") }) { + ids.append(cssSelector) + } + } + return ids + } + + /** + * Find the cssSelector for a locator. If it already have one return it, otherwise we need to look it up. + */ + func findCssSelectorForLocator(locator: Locator) async -> String? { + if locator.locations.cssSelector?.hasPrefix("#") == true { + return locator.locations.cssSelector + } + + guard let contentService: ContentService = findService(ContentService.self) else { + debugPrint("No ContentService available") + return nil + } + + let cleanPath = locator.href.path + let content = contentService.content(from: locator)?.iterator() + if (content == nil) { + debugPrint("No content iterator obtained from ContentService") + return nil + } + let locatorProgression = locator.locations.progression ?? 0.0 + var lastPassedCssSelector: String? = nil + + while let element: ContentElement? = try? await content?.next() { + debugPrint("findCssSelector: \(element?.locator.href.path ?? ""), \(element?.locator.locations.progression ?? 0.0), \(element?.locator.locations.cssSelector ?? "")") + if (element == nil || element?.locator.href.path != cleanPath) { + // End of content iterator or went past current HTML resource. + break + } + let progression = element?.locator.locations.progression ?? 0.0 + if (progression > locatorProgression && lastPassedCssSelector != nil) { + // We're past Locator's progression and previously found a cssSelector, break out and return it. + break; + } + // Save the passed CSS selector. + if let cssSelector = element?.locator.locations.cssSelector.takeIf({ $0.hasPrefix("#") }) { + lastPassedCssSelector = cssSelector.split(separator: " ").first?.lowercased() + } + } + return lastPassedCssSelector + } } extension MediaPlaybackState { @@ -54,6 +167,10 @@ extension Link { throw JSONError.parsing(Self.self) } } + + func flatten() -> [Link] { + return [self] + children.flatMap{ $0.flatten() } + } } extension Decoration { From 2b6162dfd8341bab67285983bd2ec6fe33940235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Tue, 3 Mar 2026 17:12:01 +0100 Subject: [PATCH 11/19] findCssSelectorForLocator sometimes found the wrong cssSelector --- .../nota/flutter_readium/ReadiumExtensions.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index e21cb4ac..4552f63c 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -321,6 +321,7 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { val cleanHref = locator.href.cleanHref() val locatorProgress = locator.locations.progression ?: 0.0 + var cssSelector: String? = null for (element in contentService.content(locator)) { if (element !is Content.TextElement) { continue @@ -331,19 +332,18 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { break } - val progression = - element.locator.locations.progression - ?: 0.0 + val eCssSelector = element.locator.locations.cssSelector?.takeIf { it.startsWith("#") } + if (eCssSelector == null || !eCssSelector.startsWith("#")) continue - if (progression < locatorProgress) continue - - // Return the first cssSelector that starts with # after tha progression has been reached. - element.locator.locations.cssSelector?.takeIf { it.startsWith("#") }?.let { - return it + val progression = element.locator.locations.progression ?: 0.0 + if (progression > locatorProgress && cssSelector != null) { + break } + + cssSelector = eCssSelector } - return null + return cssSelector } /** From 54038475f100714f649454f5339cedf2a28ac50d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Tue, 3 Mar 2026 17:13:01 +0100 Subject: [PATCH 12/19] fix(android): List.flatten also flattened alternates, we only want it to flatten children in the TOC --- .../dk/nota/flutter_readium/ReadiumExtensions.kt | 11 +++++++++++ .../kotlin/dk/nota/flutter_readium/ReadiumReader.kt | 5 ++--- .../flutter_readium/navigators/AudiobookNavigator.kt | 7 ++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index 4552f63c..36cd5215 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -377,3 +377,14 @@ val Href.fragmentParameters: Map val Href.time: Duration? get() = fragmentParameters["t"]?.toIntOrNull()?.seconds + +/** + * Returns a list of `Link` after flattening the `children`. + */ +fun List.flattenChildren(): List { + fun Link.flattenChildren(): List { + return listOf(this) + children.flattenChildren() + } + + return flatMap { it.flattenChildren() } +} diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index a0be9a0e..2b7f851d 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -47,7 +47,6 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml -import org.readium.r2.shared.publication.flatten import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language @@ -655,14 +654,14 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua val resultLocator = locator.copyWithLocations(otherLocations = locator.locations.otherLocations + ("cssLocator" to cssSelector)) - val contentIds = epubGetAllDocumentCssSelectors(locator.href) + val contentIds = epubGetAllDocumentCssSelectors(resultLocator.href) val idx = contentIds.indexOf(cssSelector).takeIf { it > -1 } ?: run { Log.d(TAG, ":epubFindCurrentToc cssSelector:${cssSelector} not found in contentIds") return resultLocator } val cleanHref = resultLocator.href.cleanHref() - val toc = publication.tableOfContents.flatten().filter { + val toc = publication.tableOfContents.flattenChildren().filter { it.href.resolve().cleanHref() == cleanHref }.associateBy { contentIds.indexOf("#${it.href.resolve().fragment}") } diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt index b2b591d2..53dfe978 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt @@ -8,6 +8,7 @@ import dk.nota.flutter_readium.PluginMediaServiceFacade import dk.nota.flutter_readium.PublicationError import dk.nota.flutter_readium.ReadiumReader import dk.nota.flutter_readium.cleanHref +import dk.nota.flutter_readium.flattenChildren import dk.nota.flutter_readium.throttleLatest import dk.nota.flutter_readium.time import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -31,7 +32,6 @@ import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.flatten import org.readium.r2.shared.util.getOrElse import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -237,10 +237,11 @@ open class AudiobookNavigator( locator.locations.time?.let { time -> var matchedTocItem: Link? = null - for (link in publication.tableOfContents.flatten().filter { it.href.resolve().cleanHref() == locator.href.cleanHref() }) { + val cleanHref = locator.href.cleanHref() + for (link in publication.tableOfContents.flattenChildren().filter { it.href.resolve().cleanHref() == cleanHref }) { val tocTime = link.href.time ?: continue if (tocTime > time) { - continue + break } matchedTocItem = link } From e879ec33e69509f8a24d36cc615f543bbcbd10ab Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Tue, 3 Mar 2026 18:08:23 +0100 Subject: [PATCH 13/19] chore: add AudioNavigator ToC matching --- .../FlutterReadiumPlugin.swift | 31 ++++++++++++++++- .../navigator/FlutterAudioNavigator.swift | 4 +-- .../utils/ReadiumExtensions.swift | 33 +++++++++++++++++-- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift index b577ec02..f1ff8469 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift @@ -406,7 +406,36 @@ public class FlutterReadiumPlugin: NSObject, FlutterPlugin, ReadiumShared.Warnin public func timebasedNavigator(_: any FlutterTimebasedNavigator, didChangeState state: ReadiumTimebasedState) { print(TAG, "TimebasedNavigator state: \(state)") - timebasedPlayerStateStreamHandler?.sendEvent(state.toJsonString()) + + Task.detached(priority: .high) { + // Find and enrich Locator with current ToC link + if let locator = state.currentLocator, + let time = locator.locations.time?.begin, + let pub = self.currentPublication { + let toc = await pub.getFlattenedToC() + let flattenedTocForHref = toc.filter { + $0.hrefPath == locator.href.path + } + var matchedTocItem: Link? + for tocLink in flattenedTocForHref { + guard let tocTime = tocLink.timeFragmentBegin else { + continue + } + // Save to matchedTocItem, unless timeFromFragment is past time + if tocTime > time { + break + } + matchedTocItem = tocLink + } + if let tocLinkHref = matchedTocItem?.href { + print(TAG, "Found matching TOC item: \(tocLinkHref)") + state.currentLocator?.locations.otherLocations["toc"] = tocLinkHref + } + } + Task { @MainActor [state] in + self.timebasedPlayerStateStreamHandler?.sendEvent(state.toJsonString()) + } + } } public func timebasedNavigator(_: any FlutterTimebasedNavigator, encounteredError error: any Error, withDescription description: String?) { diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterAudioNavigator.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterAudioNavigator.swift index 68f935dc..48ed76c8 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterAudioNavigator.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterAudioNavigator.swift @@ -247,8 +247,8 @@ public class FlutterAudioNavigator: FlutterTimebasedNavigator, AudioNavigatorDel /// Fetch MediaPlaybackState and convert it to TimebasedState var playerState = info.state.asTimebasedState - if (info.state == .paused && info.progress >= 1 && info.resourceIndex == self.publication.manifest.readingOrder.count - 1) { - /// If paused at progress 1 of the last resource in readingOrder, we have to assume the book has ended. + if (info.state == .paused && info.progress >= 1.0 && info.resourceIndex == self.publication.manifest.readingOrder.count - 1) { + /// If paused at progress 1 of the last resource in readingOrder, we can assume the book has ended. playerState = .ended } diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift index b50d4906..cf092035 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift @@ -26,7 +26,7 @@ extension Publication { func getFlattenedToC() async -> [Link] { switch await self.tableOfContents() { case .success(let toc): - return toc.flatMap{ $0.flatten() } + return toc.flatMap{ $0.flattened } case .failure(let err): debugPrint("failed to retrieve ToC: \(err)") return [] @@ -168,8 +168,35 @@ extension Link { } } - func flatten() -> [Link] { - return [self] + children.flatMap{ $0.flatten() } + /// Returns only the path part of the Link href. + var hrefPath: String? { + return URL(string: href)?.path + } + + /// Recursively flattens the Link and its children. + var flattened: [Link] { + return [self] + children.flatMap{ $0.flattened } + } + + /// Gets the time-fragment if part of the Link. + var timeFragment: String? { + if let url = URL(string: self.href), + let timeFragment = url.fragment?.split(separator: "&").first(where: { $0.hasPrefix("t=") }), + let timeComponent = timeFragment.split(separator: "=").last { + return String(timeComponent) + } else { + return nil + } + } + + /// Gets the Begin part of a time-fragment as Double in in the Link. + var timeFragmentBegin: Double? { + if let timeComponent = timeFragment, + let timeBegin = timeComponent.split(separator: ",").first { + return Double(timeBegin) + } else { + return nil + } } } From bae31557cfc24115f58d28f75cd4743c182b1f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Wed, 4 Mar 2026 10:33:52 +0100 Subject: [PATCH 14/19] fix(android): Using the content service to find current cssSelector didn't work Progression from the locator and progressing in the elements from the content service doesn't match, so we found the wrong cssSelector. Switching back to using the readium.findFirstVisibleLocator() instead. --- .../nota/flutter_readium/ReadiumExtensions.kt | 4 +-- .../dk/nota/flutter_readium/ReadiumReader.kt | 8 ++++- .../flutter_readium/models/PageInformation.kt | 14 ++++++-- .../assets/_helper_scripts/package-lock.json | 14 ++++---- .../assets/_helper_scripts/src/EpubPage.ts | 33 +++++++++++-------- .../assets/_helper_scripts/src/types.ts | 7 ++-- 6 files changed, 51 insertions(+), 29 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index 36cd5215..3b60a4c7 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -343,7 +343,7 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { cssSelector = eCssSelector } - return cssSelector + return cssSelector?.takeIf { it.isNotEmpty() && it.startsWith("#") } } /** @@ -354,7 +354,7 @@ fun Url.cleanHref() = removeFragment().removeQuery() /** * Remove query and fragment from the Href */ -fun Href.cleanHref() = Href(resolve().cleanHref()) +fun Href.cleanHref(): Href = Href(resolve().cleanHref()) val Href.fragmentParameters: Map get() = resolve() diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index 2b7f851d..098169f9 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -647,12 +647,18 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua return locator } + if (!publication.conformsTo(Publication.Profile.EPUB)) { + Log.d(TAG, ":epubFindCurrentToc - not an EPUB profile") + return locator + } + val cssSelector = publication.findCssSelectorForLocator(locator) ?: run { Log.e(TAG, ":epubFindCurrentToc, missing cssSelector in locator") return locator } - val resultLocator = locator.copyWithLocations(otherLocations = locator.locations.otherLocations + ("cssLocator" to cssSelector)) + val resultLocator = + locator.copyWithLocations(otherLocations = locator.locations.otherLocations + ("cssLocator" to cssSelector)) val contentIds = epubGetAllDocumentCssSelectors(resultLocator.href) val idx = contentIds.indexOf(cssSelector).takeIf { it > -1 } ?: run { diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt index 67ec65a8..0e988ffb 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt @@ -3,7 +3,12 @@ package dk.nota.flutter_readium.models import dk.nota.flutter_readium.jsonDecode import org.json.JSONObject -class PageInformation(val pageIndex: Long?, val totalPages: Long?, val physicalPageIndex: String?) { +class PageInformation( + val pageIndex: Long?, + val totalPages: Long?, + val physicalPageIndex: String?, + val cssSelector: String? +) { val otherLocations: Map get() { val res = mutableMapOf() @@ -15,6 +20,10 @@ class PageInformation(val pageIndex: Long?, val totalPages: Long?, val physicalP physicalPageIndex?.takeIf { it.isNotEmpty() }?.let { res["physicalPage"] = it } + + cssSelector?.takeIf { it.isNotEmpty() }?.let { + res["cssSelector"] = it + } return res; } @@ -25,8 +34,9 @@ class PageInformation(val pageIndex: Long?, val totalPages: Long?, val physicalP val pageIndex = json.optLong("pageIndex") val totalPages = json.optLong("totalPages") val physicalPageIndex = json.optString("physicalPageIndex") + val cssSelector = json.optString("cssSelector") - return PageInformation(pageIndex, totalPages, physicalPageIndex) + return PageInformation(pageIndex, totalPages, physicalPageIndex, cssSelector) } } } diff --git a/flutter_readium/assets/_helper_scripts/package-lock.json b/flutter_readium/assets/_helper_scripts/package-lock.json index ce089d8e..eca9d2bb 100644 --- a/flutter_readium/assets/_helper_scripts/package-lock.json +++ b/flutter_readium/assets/_helper_scripts/package-lock.json @@ -3129,10 +3129,11 @@ "dev": true }, "node_modules/copy-webpack-plugin": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", - "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", + "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", "dev": true, + "license": "MIT", "dependencies": { "glob-parent": "^6.0.1", "normalize-path": "^3.0.0", @@ -8186,16 +8187,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { diff --git a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts index fbbb426e..72bac9a3 100644 --- a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts +++ b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts @@ -15,23 +15,27 @@ export class EpubPage { public getPageInformation(): PageInformation { const physicalPageIndex = this._findCurrentPhysicalPage(); + const locator = readium.findFirstVisibleLocator(); + let pageIndex: number | null; + let totalPages: number | null; + const cssSelector = locator?.locations?.cssSelector ?? null; - if (readium?.isReflowable) { + if (readium.isReflowable) { if (this._isScrollModeEnabled) { - return { - pageIndex: null, - totalPages: null, - physicalPageIndex, - } - } - - const { scrollLeft, scrollWidth } = document.scrollingElement; - const { innerWidth } = window; - return { - pageIndex: this._isScrollModeEnabled ? null : Math.round(scrollLeft / innerWidth) + 1, - totalPages: this._isScrollModeEnabled ? null : Math.round(scrollWidth / innerWidth), - physicalPageIndex, + // Page index doesn't make sense in scroll mode. + pageIndex = null; + totalPages = null; + } else { + // Calculate page index based on scroll position and viewport width. + const { scrollLeft, scrollWidth } = document.scrollingElement; + const { innerWidth } = window; + pageIndex = Math.round(scrollLeft / innerWidth) + 1; + totalPages = Math.round(scrollWidth / innerWidth); } + } else { + // Fixed layout books is single page files. + pageIndex = 1; + totalPages = 1; } // Assume fixed layout has only one page, and the physical page index is determined by the current visible element. @@ -39,6 +43,7 @@ export class EpubPage { pageIndex: 1, totalPages: 1, physicalPageIndex, + cssSelector, }; } diff --git a/flutter_readium/assets/_helper_scripts/src/types.ts b/flutter_readium/assets/_helper_scripts/src/types.ts index c39cd0bf..5d42048e 100644 --- a/flutter_readium/assets/_helper_scripts/src/types.ts +++ b/flutter_readium/assets/_helper_scripts/src/types.ts @@ -134,7 +134,8 @@ export interface CurrentSelection { } export interface PageInformation { - pageIndex: number | null; - totalPages: number | null; - physicalPageIndex: string | null; + pageIndex?: number | null; + totalPages?: number | null; + physicalPageIndex?: string | null; + cssSelector?: string | null; } From d714618fdf8b23c4b26b43f3115bbcc370c095fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Wed, 4 Mar 2026 11:17:52 +0100 Subject: [PATCH 15/19] fix(android): remote resource to toc mapping for audiobook didn't work right --- .../dk/nota/flutter_readium/ReadiumExtensions.kt | 9 ++------- .../dk/nota/flutter_readium/ReadiumReader.kt | 1 + .../navigators/AudiobookNavigator.kt | 16 ++++++++++------ flutter_readium/assets/helpers/epub.js | 2 +- .../example/assets/pubs/free_audiobook.json | 14 ++++++++++++-- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index 3b60a4c7..db0a6c35 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -291,7 +291,7 @@ suspend fun Publication.findAllCssSelectors(href: Url): List? { continue } - if (element.locator.href.cleanHref() != cleanHref) { + if (element.locator.href.cleanHref().path != cleanHref.path) { // We iterated to the next document, stopping break } @@ -327,7 +327,7 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { continue } - if (element.locator.href.cleanHref() != cleanHref) { + if (element.locator.href.cleanHref().path != cleanHref.path) { // We iterated to the next document, stopping break } @@ -351,11 +351,6 @@ suspend fun Publication.findCssSelectorForLocator(locator: Locator): String? { */ fun Url.cleanHref() = removeFragment().removeQuery() -/** - * Remove query and fragment from the Href - */ -fun Href.cleanHref(): Href = Href(resolve().cleanHref()) - val Href.fragmentParameters: Map get() = resolve() .fragment diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index 098169f9..67ab5909 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -47,6 +47,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml +import org.readium.r2.shared.publication.services.search.search import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt index 53dfe978..f71a0619 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/AudiobookNavigator.kt @@ -71,8 +71,10 @@ open class AudiobookNavigator( DatabaseMediaMetadataFactory( publication = publication, trackCount = pub.readingOrder.size, - controlPanelInfoType = preferences.controlPanelInfoType ?: ControlPanelInfoType.STANDARD - )}) + controlPanelInfoType = preferences.controlPanelInfoType + ?: ControlPanelInfoType.STANDARD + ) + }) ) if (navigatorFactory == null) { @@ -237,8 +239,10 @@ open class AudiobookNavigator( locator.locations.time?.let { time -> var matchedTocItem: Link? = null - val cleanHref = locator.href.cleanHref() - for (link in publication.tableOfContents.flattenChildren().filter { it.href.resolve().cleanHref() == cleanHref }) { + val cleanHref = locator.href.cleanHref().path + for (link in publication.tableOfContents.flattenChildren().filter { + it.href.resolve().cleanHref().path == cleanHref + }) { val tocTime = link.href.time ?: continue if (tocTime > time) { break @@ -248,8 +252,8 @@ open class AudiobookNavigator( matchedTocItem?.href?.resolve()?.let { emittingLocator = emittingLocator.copy( - locations = emittingLocator.locations.copy( - otherLocations = emittingLocator.locations.otherLocations + ( "toc" to it ) + locations = emittingLocator.locations.copy( + otherLocations = emittingLocator.locations.otherLocations + ("toc" to it) ) ) } diff --git a/flutter_readium/assets/helpers/epub.js b/flutter_readium/assets/helpers/epub.js index 69955a43..79640c0f 100644 --- a/flutter_readium/assets/helpers/epub.js +++ b/flutter_readium/assets/helpers/epub.js @@ -1 +1 @@ -var epub;(()=>{"use strict";var e={324(e,t,n){n.r(t)},679(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const r=document.createElement("tbody"),l=document.createElement("tr"),o=document.createElement("tr");l.innerHTML=" ",o.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,l.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),o.appendChild(t)}})),r.appendChild(l),r.appendChild(o),e.appendChild(r)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const r=e.rows[0];r&&r.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=r,n=Array.from(r.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const r=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",r[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function n(r){var l=t[r];if(void 0!==l)return l.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};(()=>{var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(679);n(324);class l{get _isScrollModeEnabled(){return"readium-scroll-on"===getComputedStyle(document.documentElement).getPropertyValue("--USER__view")}getPageInformation(){const e=this._findCurrentPhysicalPage();if(null===readium||void 0===readium?void 0:readium.isReflowable){if(this._isScrollModeEnabled)return{pageIndex:null,totalPages:null,physicalPageIndex:e};const{scrollLeft:t,scrollWidth:n}=document.scrollingElement,{innerWidth:r}=window;return{pageIndex:this._isScrollModeEnabled?null:Math.round(t/r)+1,totalPages:this._isScrollModeEnabled?null:Math.round(n/r),physicalPageIndex:e}}return{pageIndex:1,totalPages:1,physicalPageIndex:e}}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(){var e,t,n;const r=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector;let l=document.querySelector(r);if(null!=l){if(this._isPageBreakElement(l))return this._getPhysicalPageIndexFromElement(l);for(;l&&l.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(l);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(l)));t>=0;t--){const n=e[t],r=this._findPhysicalPageIndex(n);if(null!=r)return r}if(l=l.parentNode,null==l||"body"===l.nodeName.toLowerCase())return null===(n=document.querySelector("head [name='webpub:currentPage']"))||void 0===n?void 0:n.getAttribute("content")}}}_log(...e){this._isIos()?null===webkit||void 0===webkit||webkit.messageHandlers.log.postMessage([].slice.call(e).map((e=>e instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function o(){window.epubPage||((0,t.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",o),window.epubPage=new l)}e.EpubPage=l,"loading"!==document.readyState?window.setTimeout(o):document.addEventListener("DOMContentLoaded",o)})(),epub=r})(); \ No newline at end of file +var epub;(()=>{"use strict";var e={324(e,t,n){n.r(t)},679(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const r=document.createElement("tbody"),l=document.createElement("tr"),o=document.createElement("tr");l.innerHTML=" ",o.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,l.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),o.appendChild(t)}})),r.appendChild(l),r.appendChild(o),e.appendChild(r)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const r=e.rows[0];r&&r.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=r,n=Array.from(r.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const r=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",r[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function n(r){var l=t[r];if(void 0!==l)return l.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};(()=>{var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(679);n(324);class l{get _isScrollModeEnabled(){return"readium-scroll-on"===getComputedStyle(document.documentElement).getPropertyValue("--USER__view")}getPageInformation(){var e,t;const n=this._findCurrentPhysicalPage(),r=readium.findFirstVisibleLocator();let l,o;const i=null!==(t=null===(e=null==r?void 0:r.locations)||void 0===e?void 0:e.cssSelector)&&void 0!==t?t:null;if(readium.isReflowable)if(this._isScrollModeEnabled)l=null,o=null;else{const{scrollLeft:e,scrollWidth:t}=document.scrollingElement,{innerWidth:n}=window;l=Math.round(e/n)+1,o=Math.round(t/n)}else l=1,o=1;return{pageIndex:1,totalPages:1,physicalPageIndex:n,cssSelector:i}}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(){var e,t,n;const r=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector;let l=document.querySelector(r);if(null!=l){if(this._isPageBreakElement(l))return this._getPhysicalPageIndexFromElement(l);for(;l&&l.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(l);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(l)));t>=0;t--){const n=e[t],r=this._findPhysicalPageIndex(n);if(null!=r)return r}if(l=l.parentNode,null==l||"body"===l.nodeName.toLowerCase())return null===(n=document.querySelector("head [name='webpub:currentPage']"))||void 0===n?void 0:n.getAttribute("content")}}}_log(...e){this._isIos()?null===webkit||void 0===webkit||webkit.messageHandlers.log.postMessage([].slice.call(e).map((e=>e instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function o(){window.epubPage||((0,t.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",o),window.epubPage=new l)}e.EpubPage=l,"loading"!==document.readyState?window.setTimeout(o):document.addEventListener("DOMContentLoaded",o)})(),epub=r})(); \ No newline at end of file diff --git a/flutter_readium/example/assets/pubs/free_audiobook.json b/flutter_readium/example/assets/pubs/free_audiobook.json index d1580af1..4dee20a8 100644 --- a/flutter_readium/example/assets/pubs/free_audiobook.json +++ b/flutter_readium/example/assets/pubs/free_audiobook.json @@ -1,7 +1,7 @@ { "readingOrder": [ { - "href": "https://merkur.beta.dbb.dk/opds2/publication/free/49965/Vilde_v_sner_og_s_re_steder_-_Del_1-00001.mp3?format=WebPubAudioOnly", + "href": "Vilde_v_sner_og_s_re_steder_-_Del_1-00001.mp3?format=WebPubAudioOnly", "duration": 1683.692, "title": "Vilde væsner og sære steder, lydfortællinger Lolland.", "type": "audio/mpeg" @@ -10,7 +10,17 @@ "toc": [ { "href": "Vilde_v_sner_og_s_re_steder_-_Del_1-00001.mp3#t=0", - "title": "Vilde væsner og sære steder, lydfortællinger Lolland." + "title": "Forside", + "children": [ + { + "href": "Vilde_v_sner_og_s_re_steder_-_Del_1-00001.mp3#t=12", + "title": "Vilde væsner og sære steder, lydfortællinger Lolland." + }, + { + "href": "Vilde_v_sner_og_s_re_steder_-_Del_1-00001.mp3#t=30", + "title": "Vilde væsner og sære steder, lydfortællinger Lolland - Del 1." + } + ] } ], "@context": "https://readium.org/webpub-manifest/context.jsonld", From fb6c94cadae7519c362fcc36969f0cb6d8185e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Wed, 4 Mar 2026 14:01:26 +0100 Subject: [PATCH 16/19] (android): Use native pageIndex and totalPages in emitOnPageChanged --- .vscode/settings.json | 1 + bin/install | 1 + .../dk/nota/flutter_readium/ReadiumReader.kt | 38 +- .../flutter_readium/ReadiumReaderWidget.kt | 16 +- .../flutter_readium/models/PageInformation.kt | 16 +- .../assets/_helper_scripts/package-lock.json | 621 ++++++++---------- .../assets/_helper_scripts/package.json | 24 +- .../assets/_helper_scripts/src/EpubPage.ts | 30 +- .../assets/_helper_scripts/src/types.ts | 4 +- flutter_readium/assets/helpers/epub.js | 2 +- flutter_readium/package-lock.json | 55 +- 11 files changed, 353 insertions(+), 455 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d45b4a7a..821cd436 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -44,6 +44,7 @@ "cSpell.words": [ "acsm", "androidx", + "animejs", "ascii", "Ascii", "charsets", diff --git a/bin/install b/bin/install index b035ff2d..c5901ffd 100755 --- a/bin/install +++ b/bin/install @@ -12,4 +12,5 @@ if [ "$(uname)" == "Darwin" ]; then fi $PROJECT_ROOT/flutter_readium/bin/build_helper_scripts.sh +$PROJECT_ROOT/build_js $PROJECT_ROOT/bin/update_web_example diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt index 67ab5909..5ce12bf6 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReader.kt @@ -47,7 +47,6 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml -import org.readium.r2.shared.publication.services.search.search import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language @@ -644,7 +643,7 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua */ suspend fun epubFindCurrentToc(locator: Locator): Locator { val publication = currentPublication ?: run { - Log.e(TAG, ":epubFindCurrentToc, no currentPublication") + Log.e(TAG, ":epubFindCurrentToc - no currentPublication") return locator } @@ -654,23 +653,33 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua } val cssSelector = publication.findCssSelectorForLocator(locator) ?: run { - Log.e(TAG, ":epubFindCurrentToc, missing cssSelector in locator") + Log.e(TAG, ":epubFindCurrentToc - missing cssSelector in locator") return locator } val resultLocator = - locator.copyWithLocations(otherLocations = locator.locations.otherLocations + ("cssLocator" to cssSelector)) + locator.copyWithLocations( + otherLocations = locator.locations.otherLocations + + ("cssSelector" to cssSelector) + ) - val contentIds = epubGetAllDocumentCssSelectors(resultLocator.href) - val idx = contentIds.indexOf(cssSelector).takeIf { it > -1 } ?: run { - Log.d(TAG, ":epubFindCurrentToc cssSelector:${cssSelector} not found in contentIds") - return resultLocator + val cleanHref = resultLocator.href.cleanHref() + val tocLinks = publication.tableOfContents.flattenChildren().filter { + it.href.resolve().cleanHref().path == cleanHref.path } - val cleanHref = resultLocator.href.cleanHref() - val toc = publication.tableOfContents.flattenChildren().filter { - it.href.resolve().cleanHref() == cleanHref - }.associateBy { contentIds.indexOf("#${it.href.resolve().fragment}") } + val documentCssSelectors = epubGetAllDocumentCssSelectors(resultLocator.href) + val idx = documentCssSelectors.indexOf(cssSelector).takeIf { it > -1 } ?: run { + // cssSelector wasn't found in the list of document cssSelectors, best effort is to assume first + Log.d( + TAG, + ":epubFindCurrentToc cssSelector:${cssSelector} not found in contentIds, assume idx = 0" + ) + 0 + } + + val toc = + tocLinks.associateBy { documentCssSelectors.indexOf("#${it.href.resolve().fragment}") } val tocItem = toc.entries.lastOrNull { it.key <= idx }?.value ?: toc.entries.firstOrNull()?.value ?: run { @@ -681,8 +690,8 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua return resultLocator.copy( title = tocItem.title ).copyWithLocations( - otherLocations = resultLocator.locations.otherLocations + ("toc" to tocItem.href.resolve() - .toString()) + otherLocations = resultLocator.locations.otherLocations + + ("toc" to tocItem.href.resolve().toString()) ) } @@ -993,6 +1002,7 @@ object ReadiumReader : TimebasedNavigator.TimebasedListener, EpubNavigator.Visua /** * Get all cssSelectors for an EPUB file. + * Note: These only includes text elements, so body, page breaks etc are not included. */ suspend fun epubGetAllDocumentCssSelectors(href: Url): List { val cssSelectorMap = currentPublicationCssSelectorMap ?: mutableMapOf() diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt index 4251e61a..5e3167cd 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt @@ -177,7 +177,8 @@ class ReadiumReaderWidget( ReadiumReader.emitReaderStatusUpdate(ReadiumReaderStatus.Ready) } - emitOnPageChanged(locator) + + emitOnPageChanged(pageIndex, totalPages,locator) } } @@ -209,9 +210,10 @@ class ReadiumReaderWidget( updatePreferences(newPreferences) } - private suspend fun emitOnPageChanged(locator: Locator) { + private suspend fun emitOnPageChanged(pageIndex: Int, totalPages: Int, locator: Locator) { try { - var emittingLocator = ReadiumReader.epubFindCurrentToc(locator) + var emittingLocator = locator + try { evaluateJavascript("window.epubPage.getPageInformation()")?.let { PageInformation.fromJson( @@ -228,6 +230,14 @@ class ReadiumReaderWidget( Log.d(TAG, ":pageInformation error: $e") } + emittingLocator = emittingLocator.copy( + locations = emittingLocator.locations.copy( + otherLocations = emittingLocator.locations.otherLocations + ("currentPage" to pageIndex) + ("totalPages" to totalPages) + ) + ) + + emittingLocator = ReadiumReader.epubFindCurrentToc(emittingLocator) + channel.onPageChanged(emittingLocator) ReadiumReader.emitTextLocatorUpdate(emittingLocator) Log.d(TAG, "emitOnPageChanged: emitted $emittingLocator") diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt index 0e988ffb..fda5ace7 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt @@ -4,20 +4,20 @@ import dk.nota.flutter_readium.jsonDecode import org.json.JSONObject class PageInformation( - val pageIndex: Long?, + val page: Long?, val totalPages: Long?, - val physicalPageIndex: String?, + val physicalPage: String?, val cssSelector: String? ) { val otherLocations: Map get() { val res = mutableMapOf() - if (pageIndex != null && totalPages != null) { - res["currentPage"] = pageIndex + if (page != null && totalPages != null) { + res["currentPage"] = page res["totalPages"] = totalPages } - physicalPageIndex?.takeIf { it.isNotEmpty() }?.let { + physicalPage?.takeIf { it.isNotEmpty() }?.let { res["physicalPage"] = it } @@ -31,12 +31,12 @@ class PageInformation( fun fromJson(json: String): PageInformation = fromJson(jsonDecode(json) as JSONObject) fun fromJson(json: JSONObject): PageInformation { - val pageIndex = json.optLong("pageIndex") + val page = json.optLong("page") val totalPages = json.optLong("totalPages") - val physicalPageIndex = json.optString("physicalPageIndex") + val physicalPage = json.optString("physicalPage").takeIf {it.isNotEmpty()} val cssSelector = json.optString("cssSelector") - return PageInformation(pageIndex, totalPages, physicalPageIndex, cssSelector) + return PageInformation(page, totalPages, physicalPage, cssSelector) } } } diff --git a/flutter_readium/assets/_helper_scripts/package-lock.json b/flutter_readium/assets/_helper_scripts/package-lock.json index eca9d2bb..64c9e309 100644 --- a/flutter_readium/assets/_helper_scripts/package-lock.json +++ b/flutter_readium/assets/_helper_scripts/package-lock.json @@ -9,18 +9,18 @@ "version": "1.0.0", "dependencies": { "animejs": "~3.2.2", - "lit": "~3.3.0", + "lit": "~3.3.2", "readium-css": "github:readium/readium-css" }, "devDependencies": { "@types/animejs": "^3.1.13", "@types/jest": "^29.5.14", - "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.32.1", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "copy-webpack-plugin": "^13.0.0", + "copy-webpack-plugin": "^14.0.0", "cross-env": "^7.0.3", - "css-loader": "^7.1.2", + "css-loader": "^7.1.4", "eslint": "^9.27.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-import": "^2.31.0", @@ -28,18 +28,18 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.4.0", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mini-css-extract-plugin": "^2.9.2", - "sass": "^1.89.0", - "sass-loader": "^16.0.5", - "terser-webpack-plugin": "^5.3.14", + "mini-css-extract-plugin": "^2.10.0", + "sass": "^1.97.3", + "sass-loader": "^16.0.7", + "terser-webpack-plugin": "^5.3.17", "ts-jest": "^29.3.4", - "ts-loader": "^9.5.2", + "ts-loader": "^9.5.4", "ts-node": "^10.9.2", - "typescript": "^5.8.3", - "webpack": "^5.105.0", + "typescript": "^5.9.3", + "webpack": "^5.105.4", "webpack-cli": "^6.0.1", "webpack-merge": "^6.0.1" } @@ -559,10 +559,11 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -577,10 +578,11 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1263,41 +1265,6 @@ "@lit-labs/ssr-dom-shim": "^1.2.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -1857,20 +1824,20 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1880,22 +1847,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1905,18 +1873,41 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1926,16 +1917,35 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1945,15 +1955,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1963,19 +1974,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1985,19 +1998,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2007,18 +2021,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2029,12 +2044,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2250,9 +2266,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -3129,20 +3145,20 @@ "dev": true }, "node_modules/copy-webpack-plugin": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", - "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", + "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", "dev": true, "license": "MIT", "dependencies": { "glob-parent": "^6.0.1", "normalize-path": "^3.0.0", "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2", + "serialize-javascript": "^7.0.3", "tinyglobby": "^0.2.12" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "funding": { "type": "opencollective", @@ -3212,19 +3228,20 @@ } }, "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.33", + "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" + "semver": "^7.6.3" }, "engines": { "node": ">= 18.12.0" @@ -3234,7 +3251,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "peerDependenciesMeta": { @@ -3376,12 +3393,13 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3672,9 +3690,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4398,34 +4416,6 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4453,15 +4443,6 @@ "node": ">= 4.9.1" } }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4887,12 +4868,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -5024,10 +4999,11 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "version": "5.6.6", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", + "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", "dev": true, + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -5132,10 +5108,11 @@ } }, "node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -6420,9 +6397,10 @@ "dev": true }, "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", @@ -6573,15 +6551,6 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6626,10 +6595,11 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz", + "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==", "dev": true, + "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" @@ -6646,21 +6616,44 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -6671,10 +6664,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", @@ -7383,35 +7377,6 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7568,39 +7533,6 @@ "node": ">=10" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -7619,26 +7551,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -7663,10 +7575,11 @@ "dev": true }, "node_modules/sass": { - "version": "1.89.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz", - "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -7683,10 +7596,11 @@ } }, "node_modules/sass-loader": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", - "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.7.tgz", + "integrity": "sha512-w6q+fRHourZ+e+xA1kcsF27iGM6jdB8teexYCfdUw0sYgcDNeZESnDNT9sUmmPm3ooziwUJXGwZJSTF3kOdBfA==", "dev": true, + "license": "MIT", "dependencies": { "neo-async": "^2.6.2" }, @@ -7698,7 +7612,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", @@ -7790,10 +7704,11 @@ "dev": true }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -7802,12 +7717,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, - "dependencies": { - "randombytes": "^2.1.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" } }, "node_modules/set-function-length": { @@ -8304,13 +8220,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -8320,10 +8237,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -8334,10 +8255,11 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -8391,10 +8313,11 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -8452,10 +8375,11 @@ } }, "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", @@ -8669,10 +8593,11 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8838,9 +8763,9 @@ } }, "node_modules/webpack": { - "version": "5.105.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", - "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", "dependencies": { @@ -8850,11 +8775,11 @@ "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -8866,9 +8791,9 @@ "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", + "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -8953,9 +8878,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { diff --git a/flutter_readium/assets/_helper_scripts/package.json b/flutter_readium/assets/_helper_scripts/package.json index 066edfa8..be92c5ae 100644 --- a/flutter_readium/assets/_helper_scripts/package.json +++ b/flutter_readium/assets/_helper_scripts/package.json @@ -12,12 +12,12 @@ "devDependencies": { "@types/animejs": "^3.1.13", "@types/jest": "^29.5.14", - "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.32.1", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "copy-webpack-plugin": "^13.0.0", + "copy-webpack-plugin": "^14.0.0", "cross-env": "^7.0.3", - "css-loader": "^7.1.2", + "css-loader": "^7.1.4", "eslint": "^9.27.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-import": "^2.31.0", @@ -25,18 +25,18 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.4.0", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mini-css-extract-plugin": "^2.9.2", - "sass": "^1.89.0", - "sass-loader": "^16.0.5", - "terser-webpack-plugin": "^5.3.14", + "mini-css-extract-plugin": "^2.10.0", + "sass": "^1.97.3", + "sass-loader": "^16.0.7", + "terser-webpack-plugin": "^5.3.17", "ts-jest": "^29.3.4", - "ts-loader": "^9.5.2", + "ts-loader": "^9.5.4", "ts-node": "^10.9.2", - "typescript": "^5.8.3", - "webpack": "^5.105.0", + "typescript": "^5.9.3", + "webpack": "^5.105.4", "webpack-cli": "^6.0.1", "webpack-merge": "^6.0.1" }, @@ -51,7 +51,7 @@ }, "dependencies": { "animejs": "~3.2.2", - "lit": "~3.3.0", + "lit": "~3.3.2", "readium-css": "github:readium/readium-css" } } diff --git a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts index 72bac9a3..c1fc9a57 100644 --- a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts +++ b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts @@ -14,35 +14,35 @@ export class EpubPage { } public getPageInformation(): PageInformation { - const physicalPageIndex = this._findCurrentPhysicalPage(); + const physicalPage = this._findCurrentPhysicalPage(); const locator = readium.findFirstVisibleLocator(); - let pageIndex: number | null; + let page: number | null; let totalPages: number | null; const cssSelector = locator?.locations?.cssSelector ?? null; if (readium.isReflowable) { if (this._isScrollModeEnabled) { // Page index doesn't make sense in scroll mode. - pageIndex = null; + page = null; totalPages = null; } else { // Calculate page index based on scroll position and viewport width. const { scrollLeft, scrollWidth } = document.scrollingElement; const { innerWidth } = window; - pageIndex = Math.round(scrollLeft / innerWidth) + 1; + page = Math.round(scrollLeft / innerWidth) + 1; totalPages = Math.round(scrollWidth / innerWidth); } } else { // Fixed layout books is single page files. - pageIndex = 1; + page = 1; totalPages = 1; } // Assume fixed layout has only one page, and the physical page index is determined by the current visible element. return { - pageIndex: 1, - totalPages: 1, - physicalPageIndex, + page, + totalPages, + physicalPage, cssSelector, }; } @@ -52,7 +52,7 @@ export class EpubPage { return false; } - return element.getAttribute('type') === 'pagebreak'; + return element.getAttributeNS("http://www.idpf.org/2007/ops", "type") === 'pagebreak' || element.getAttribute('type') === 'pagebreak'; } private _getPhysicalPageIndexFromElement(element: HTMLElement): string | null { @@ -66,13 +66,7 @@ export class EpubPage { return this._getPhysicalPageIndexFromElement(element as HTMLElement); } - const pageBreakElement = element?.querySelector('.page-normal, .page-front, .page-special'); - - if (pageBreakElement == null) { - return null; - } - - return this._getPhysicalPageIndexFromElement(pageBreakElement as HTMLElement); + return null; } private _getAllSiblings(elem: ChildNode): HTMLElement[] | null { @@ -90,11 +84,10 @@ export class EpubPage { * * @returns The physical page index, or null if it cannot be determined. */ - public _findCurrentPhysicalPage(): string | null { + private _findCurrentPhysicalPage(): string | null { const cssSelector = readium.findFirstVisibleLocator()?.locations?.cssSelector; let element = document.querySelector(cssSelector); - if (element == null) { return; } @@ -114,7 +107,6 @@ export class EpubPage { const e = siblings[i]; const pageBreakIndex = this._findPhysicalPageIndex(e); - if (pageBreakIndex != null) { return pageBreakIndex; } diff --git a/flutter_readium/assets/_helper_scripts/src/types.ts b/flutter_readium/assets/_helper_scripts/src/types.ts index 5d42048e..34b55e19 100644 --- a/flutter_readium/assets/_helper_scripts/src/types.ts +++ b/flutter_readium/assets/_helper_scripts/src/types.ts @@ -134,8 +134,8 @@ export interface CurrentSelection { } export interface PageInformation { - pageIndex?: number | null; + page?: number | null; totalPages?: number | null; - physicalPageIndex?: string | null; + physicalPage?: string | null; cssSelector?: string | null; } diff --git a/flutter_readium/assets/helpers/epub.js b/flutter_readium/assets/helpers/epub.js index 79640c0f..524a407f 100644 --- a/flutter_readium/assets/helpers/epub.js +++ b/flutter_readium/assets/helpers/epub.js @@ -1 +1 @@ -var epub;(()=>{"use strict";var e={324(e,t,n){n.r(t)},679(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const r=document.createElement("tbody"),l=document.createElement("tr"),o=document.createElement("tr");l.innerHTML=" ",o.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,l.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),o.appendChild(t)}})),r.appendChild(l),r.appendChild(o),e.appendChild(r)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const r=e.rows[0];r&&r.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=r,n=Array.from(r.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const r=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",r[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function n(r){var l=t[r];if(void 0!==l)return l.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};(()=>{var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=n(679);n(324);class l{get _isScrollModeEnabled(){return"readium-scroll-on"===getComputedStyle(document.documentElement).getPropertyValue("--USER__view")}getPageInformation(){var e,t;const n=this._findCurrentPhysicalPage(),r=readium.findFirstVisibleLocator();let l,o;const i=null!==(t=null===(e=null==r?void 0:r.locations)||void 0===e?void 0:e.cssSelector)&&void 0!==t?t:null;if(readium.isReflowable)if(this._isScrollModeEnabled)l=null,o=null;else{const{scrollLeft:e,scrollWidth:t}=document.scrollingElement,{innerWidth:n}=window;l=Math.round(e/n)+1,o=Math.round(t/n)}else l=1,o=1;return{pageIndex:1,totalPages:1,physicalPageIndex:n,cssSelector:i}}_isPageBreakElement(e){return null!=e&&"pagebreak"===e.getAttribute("type")}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){if(!(null!=e&&e instanceof Element))return null;if(this._isPageBreakElement(e))return this._getPhysicalPageIndexFromElement(e);const t=null==e?void 0:e.querySelector(".page-normal, .page-front, .page-special");return null==t?null:this._getPhysicalPageIndexFromElement(t)}_getAllSiblings(e){var t;const n=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&n.push(e)}while(e=null==e?void 0:e.nextSibling);return n}_findCurrentPhysicalPage(){var e,t,n;const r=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector;let l=document.querySelector(r);if(null!=l){if(this._isPageBreakElement(l))return this._getPhysicalPageIndexFromElement(l);for(;l&&l.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(l);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(l)));t>=0;t--){const n=e[t],r=this._findPhysicalPageIndex(n);if(null!=r)return r}if(l=l.parentNode,null==l||"body"===l.nodeName.toLowerCase())return null===(n=document.querySelector("head [name='webpub:currentPage']"))||void 0===n?void 0:n.getAttribute("content")}}}_log(...e){this._isIos()?null===webkit||void 0===webkit||webkit.messageHandlers.log.postMessage([].slice.call(e).map((e=>e instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function o(){window.epubPage||((0,t.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",o),window.epubPage=new l)}e.EpubPage=l,"loading"!==document.readyState?window.setTimeout(o):document.addEventListener("DOMContentLoaded",o)})(),epub=r})(); \ No newline at end of file +var epub;(()=>{"use strict";var e={324(e,t,r){r.r(t)},679(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,r=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:r++}))})),t>r)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,r=e.querySelectorAll("tbody tr");if(1===t&&Array.from(r).every((e=>2===e.querySelectorAll("td").length))){const t=r[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,r)=>e.querySelector(`h${r+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,r=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const n=document.createElement("tbody"),l=document.createElement("tr"),o=document.createElement("tr");l.innerHTML=" ",o.innerHTML=`${t}`,r.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,l.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),o.appendChild(t)}})),n.appendChild(l),n.appendChild(o),e.appendChild(n)}(e)}!function(e){let t=null,r=[];if(e.tHead)t=e.tHead.rows[0],r=Array.from(t.cells);else{const n=e.rows[0];n&&n.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=n,r=Array.from(n.cells),e.classList.add("has-first-row-headers"))}if(r.length){e.classList.add("has-header");const n=r.map((e=>{var t;let r=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return r.length&&(r+=":"),r}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",n[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function r(n){var l=t[n];if(void 0!==l)return l.exports;var o=t[n]={exports:{}};return e[n](o,o.exports,r),o.exports}r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=r(679);r(324);class l{get _isScrollModeEnabled(){return"readium-scroll-on"===getComputedStyle(document.documentElement).getPropertyValue("--USER__view")}getPageInformation(){var e,t;const r=this._findCurrentPhysicalPage(),n=readium.findFirstVisibleLocator();let l,o;const i=null!==(t=null===(e=null==n?void 0:n.locations)||void 0===e?void 0:e.cssSelector)&&void 0!==t?t:null;if(readium.isReflowable)if(this._isScrollModeEnabled)l=null,o=null;else{const{scrollLeft:e,scrollWidth:t}=document.scrollingElement,{innerWidth:r}=window;l=Math.round(e/r)+1,o=Math.round(t/r)}else l=1,o=1;return{page:l,totalPages:o,physicalPage:r,cssSelector:i}}_isPageBreakElement(e){return null!=e&&("pagebreak"===e.getAttributeNS("http://www.idpf.org/2007/ops","type")||"pagebreak"===e.getAttribute("type"))}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){return null!=e&&e instanceof Element&&this._isPageBreakElement(e)?this._getPhysicalPageIndexFromElement(e):null}_getAllSiblings(e){var t;const r=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&r.push(e)}while(e=null==e?void 0:e.nextSibling);return r}_findCurrentPhysicalPage(){var e,t,r;const n=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector;let l=document.querySelector(n);if(null!=l){if(this._isPageBreakElement(l))return this._getPhysicalPageIndexFromElement(l);for(;l&&l.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(l);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(l)));t>=0;t--){const r=e[t],n=this._findPhysicalPageIndex(r);if(null!=n)return n}if(l=l.parentNode,null==l||"body"===l.nodeName.toLowerCase())return null===(r=document.querySelector("head [name='webpub:currentPage']"))||void 0===r?void 0:r.getAttribute("content")}}}_log(...e){this._isIos()?null===webkit||void 0===webkit||webkit.messageHandlers.log.postMessage([].slice.call(e).map((e=>e instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function o(){window.epubPage||((0,t.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",o),window.epubPage=new l)}e.EpubPage=l,"loading"!==document.readyState?window.setTimeout(o):document.addEventListener("DOMContentLoaded",o)})(),epub=n})(); \ No newline at end of file diff --git a/flutter_readium/package-lock.json b/flutter_readium/package-lock.json index f8051f66..29b3ea0d 100644 --- a/flutter_readium/package-lock.json +++ b/flutter_readium/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "flutter_readium", "dependencies": { "@readium/navigator": "^2.2.4", "@readium/navigator-html-injectables": "^2.2.1", @@ -415,9 +416,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -1399,16 +1400,6 @@ "dev": true, "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -1473,27 +1464,6 @@ "node": ">=8" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -1527,16 +1497,6 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -1691,16 +1651,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { From 7a8e03a167723b2b3951116e50f52cf472dd21d5 Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Thu, 5 Mar 2026 11:46:25 +0100 Subject: [PATCH 17/19] refactor(iOS): improve ToC identification code --- .../FlutterReadiumPlugin.swift | 47 ++++++----------- .../flutter_readium/ReadiumReaderView.swift | 11 ++-- .../model/PageInformation.swift | 13 ++++- .../utils/ReadiumExtensions.swift | 50 +++++++++---------- 4 files changed, 58 insertions(+), 63 deletions(-) diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift index f1ff8469..c1b59f54 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/FlutterReadiumPlugin.swift @@ -406,7 +406,7 @@ public class FlutterReadiumPlugin: NSObject, FlutterPlugin, ReadiumShared.Warnin public func timebasedNavigator(_: any FlutterTimebasedNavigator, didChangeState state: ReadiumTimebasedState) { print(TAG, "TimebasedNavigator state: \(state)") - + Task.detached(priority: .high) { // Find and enrich Locator with current ToC link if let locator = state.currentLocator, @@ -552,38 +552,31 @@ extension FlutterReadiumPlugin { extension FlutterReadiumPlugin { /// Find the current table of content item from a locator. - func epubFindCurrentToc(locator: Locator) async throws -> Locator { + func currentTocLinkFromLocator(_ locator: Locator) async throws -> Link? { + let start = CFAbsoluteTimeGetCurrent() guard let publication = currentPublication else { - debugPrint(TAG, ":epubFindCurrentToc, no currentPublication") - return locator + debugPrint(TAG, ":currentTocLinkFromLocator, no currentPublication") + return nil } guard let cssSelector = await publication.findCssSelectorForLocator(locator: locator) else { - debugPrint(TAG, ":epubFindCurrentToc, missing cssSelector in locator") - return locator + debugPrint(TAG, ":epubFindCurrentToc, could not find cssSelector from locator") + return nil } - debugPrint(TAG, ":epubFindCurrentToc, found current selector: \(cssSelector)") let cleanHrefPath = locator.href.path - var resultLocator = locator.copy() - resultLocator.locations.otherLocations["cssSelector"] = cssSelector - let contentIds = try await epubGetAllDocumentCssSelectors(hrefPath: cleanHrefPath) - guard let idx = contentIds.firstIndex(of: cssSelector) else { - debugPrint(TAG, ":epubFindCurrentToc cssSelector:\(cssSelector) not found in contentIds") - return resultLocator - } - - guard let tocLinksFlattened = try? await publication.getFlattenedToC() else { - debugPrint(TAG, ":epubFindCurrentToc failed to retrieve ToC") - return resultLocator + var idx = contentIds.firstIndex(of: cssSelector) + if idx == nil { + debugPrint(TAG, ":currentTocLinkFromLocator cssSelector:\(cssSelector) not found in current href, assuming 0") + idx = 0 } let toc = Dictionary( uniqueKeysWithValues: - tocLinksFlattened + await publication.getFlattenedToC() .filter { RelativeURL(epubHREF: $0.href)?.path == cleanHrefPath } .compactMap { item -> (Int, Link)? in let fragment = RelativeURL(epubHREF: item.href)?.fragment ?? "" @@ -592,21 +585,11 @@ extension FlutterReadiumPlugin { } ) - let tocItem = toc.filter { $0.key <= idx } + let tocItem = (toc.filter { $0.key <= idx! } .sorted { $0.key < $1.key } .last?.value - ?? toc.sorted { $0.key < $1.key }.first?.value - - guard let tocItem else { - debugPrint(TAG, ":epubFindCurrentToc - no tocItem found") - return resultLocator - } - - debugPrint(TAG, ":epubFindCurrentToc - found tocItem: \(tocItem.title), href:\(tocItem.href)") - - resultLocator.locations.otherLocations["toc"] = tocItem.href - - return resultLocator.copy(title: tocItem.title) + ?? toc.sorted { $0.key < $1.key }.first?.value) + return tocItem } /// Get all cssSelectors for an EPUB file. diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift index c91a76c0..230d01d6 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/ReadiumReaderView.swift @@ -258,14 +258,17 @@ public class ReadiumReaderView: NSObject, FlutterPlatformView, EPUBNavigatorDele print(TAG, "emitOnPageChanged:locator=\(String(describing: locator))") Task.detached(priority: .high) { [locator] in - - // Map the ToC location into the locator. - var resultLocator = (try? await FlutterReadiumPlugin.instance?.epubFindCurrentToc(locator: locator)) ?? locator - // Get information about current page. + /// Enrich Locator with PageInformation and ToC. + var resultLocator = locator if let pageInfo = await self.getPageInformation() { resultLocator.locations.otherLocations.merge(pageInfo.otherLocations, uniquingKeysWith: { lhs, rhs in lhs }) } + if let tocLink = try? await FlutterReadiumPlugin.instance?.currentTocLinkFromLocator(resultLocator) { + resultLocator.title = tocLink.title + resultLocator.locations.otherLocations["toc"] = tocLink.href + } + /// Immutable ref, so that we can use it on the main thread let finalLocator = resultLocator await MainActor.run() { self.channel.onPageChanged(locator: finalLocator) diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift index d69f4edd..5891be7b 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/PageInformation.swift @@ -5,11 +5,13 @@ final class PageInformation { let pageIndex: Int64? let totalPages: Int64? let physicalPageIndex: String? + let cssSelector: String? - init(pageIndex: Int64?, totalPages: Int64?, physicalPageIndex: String?) { + init(pageIndex: Int64?, totalPages: Int64?, physicalPageIndex: String?, cssSelector: String?) { self.pageIndex = pageIndex self.totalPages = totalPages self.physicalPageIndex = physicalPageIndex + self.cssSelector = cssSelector } var otherLocations: [String: Any] { @@ -25,6 +27,11 @@ final class PageInformation { res["physicalPage"] = physicalPageIndex } + if let cssSelector, + !cssSelector.isEmpty { + res["cssSelector"] = cssSelector + } + return res } @@ -38,11 +45,13 @@ final class PageInformation { let pageIndex = json["pageIndex"] as? NSNumber let totalPages = json["totalPages"] as? NSNumber let physicalPageIndex = json["physicalPageIndex"] as? String + let cssSelector = json["cssSelector"] as? String return PageInformation( pageIndex: pageIndex?.int64Value, totalPages: totalPages?.int64Value, - physicalPageIndex: physicalPageIndex + physicalPageIndex: physicalPageIndex, + cssSelector: cssSelector ) } } diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift index cf092035..5d034a4e 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift @@ -72,35 +72,39 @@ extension Publication { let cleanHref = hrefRelativePath, startLocator = Locator(href: RelativeURL(string: cleanHref)!, mediaType: MediaType.xhtml) - let content = contentService.content(from: startLocator)?.iterator() - if (content == nil) { + guard let content = contentService.content(from: startLocator)?.iterator() else { debugPrint("No content iterator obtained from ContentService") return [] } var ids = [] as [String] - while let element: ContentElement? = try? await content?.next() { - if (element == nil) { - continue - } - if (element?.locator.href.path != cleanHref) { - break - } - - if let cssSelector = element?.locator.locations.cssSelector.takeIf({ $0.hasPrefix("#") }) { - ids.append(cssSelector) + do { + while let element = try await content.next() { + if (element.locator.href.path != cleanHref) { + break + } + + if let cssSelector = element.locator.locations.cssSelector.takeIf({ $0.hasPrefix("#") }) { + ids.append(cssSelector) + debugPrint("findAllCssSelectors: \(element.locator.href.path),id: \(cssSelector)") + } } + } catch (let err) { + debugPrint("ContentService failed to fetch next element: \(err)") } return ids } /** * Find the cssSelector for a locator. If it already have one return it, otherwise we need to look it up. + * We find it by rewinding in the ContentService from current Locator, until we hit an #id selector. */ func findCssSelectorForLocator(locator: Locator) async -> String? { if locator.locations.cssSelector?.hasPrefix("#") == true { return locator.locations.cssSelector + } else { + debugPrint("findCssSelectorForLocator: there's work to do!") } guard let contentService: ContentService = findService(ContentService.self) else { @@ -115,25 +119,21 @@ extension Publication { return nil } let locatorProgression = locator.locations.progression ?? 0.0 - var lastPassedCssSelector: String? = nil - while let element: ContentElement? = try? await content?.next() { - debugPrint("findCssSelector: \(element?.locator.href.path ?? ""), \(element?.locator.locations.progression ?? 0.0), \(element?.locator.locations.cssSelector ?? "")") - if (element == nil || element?.locator.href.path != cleanPath) { - // End of content iterator or went past current HTML resource. + while let element: ContentElement? = try? await content?.previous() { + if (element == nil) { + // Nore more content to rewind through. break } - let progression = element?.locator.locations.progression ?? 0.0 - if (progression > locatorProgression && lastPassedCssSelector != nil) { - // We're past Locator's progression and previously found a cssSelector, break out and return it. - break; - } - // Save the passed CSS selector. + // Return first content element with an #id cssSelector if let cssSelector = element?.locator.locations.cssSelector.takeIf({ $0.hasPrefix("#") }) { - lastPassedCssSelector = cssSelector.split(separator: " ").first?.lowercased() + debugPrint("findCssSelector: \(element?.locator.href.path ?? ""), \(element?.locator.locations.progression ?? 0.0), \(element?.locator.locations.cssSelector ?? "")") + return cssSelector.split(separator: " ").first?.lowercased() + } else { + debugPrint("findCssSelector: skip \(element?.locator.locations.cssSelector ?? "")") } } - return lastPassedCssSelector + return nil } } From 8e3e43dffbc768fd3879ecde3cdd77e56317ed2a Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Thu, 5 Mar 2026 15:08:10 +0100 Subject: [PATCH 18/19] feat: add ToC references in emitted Locator for MediaOverlay books --- .../model/FlutterMediaOverlay.swift | 31 ++++++++++---- .../FlutterMediaOverlayNavigator.swift | 12 +----- .../utils/ReadiumExtensions.swift | 41 +++++++++++++++++++ 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/FlutterMediaOverlay.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/FlutterMediaOverlay.swift index 1c6846a8..e0e669d5 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/FlutterMediaOverlay.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/model/FlutterMediaOverlay.swift @@ -56,16 +56,16 @@ struct FlutterMediaOverlay { return nil } - static func fromJson(_ json: [String: Any], atPosition position: Int) -> FlutterMediaOverlay? { + static func fromJson(_ json: [String: Any], atPosition position: Int, atTocHref: String? = nil) -> FlutterMediaOverlay? { guard let topNarration = json["narration"] as? [[String: Any]] else { return nil } var acc: [FlutterMediaOverlayItem] = [] for obj in topNarration { - if let item = FlutterMediaOverlayItem.fromJson(obj, atPosition: position) { + if let item = FlutterMediaOverlayItem.fromJson(obj, atPosition: position, atTocHref: atTocHref) { acc.append(item) } // recurse if nested containers also have "narration" - if let nested = FlutterMediaOverlay.fromJson(obj, atPosition: position) { + if let nested = FlutterMediaOverlay.fromJson(obj, atPosition: position, atTocHref: atTocHref) { acc.append(contentsOf: nested.items) } } @@ -94,15 +94,20 @@ struct FlutterMediaOverlayItem { let textFile: String let textId: String - init(audio: String, text: String, position: Int) { + let tocTitle: String? + let tocHref: String? + + init(audio: String, text: String, position: Int, tocTitle: String? = nil, tocHref: String? = nil) { self.audio = audio self.text = text self.position = position + self.tocTitle = tocTitle + self.tocHref = tocHref self.audioFile = audio.split(separator: "#", maxSplits: 1).first.map(String.init) ?? audio - self.audioFragment = audio.split(separator: "#", maxSplits: 1).last.map(String.init) ?? "" + self.audioFragment = audio.split(separator: "#", maxSplits: 1).getOrNil(1).map(String.init) ?? "" self.audioTime = audioFragment.hasPrefix("t=") ? String(audioFragment.dropFirst(2)) : nil self.textFile = text.split(separator: "#", maxSplits: 1).first.map(String.init) ?? "" - self.textId = text.split(separator: "#", maxSplits: 1).last.map(String.init) ?? "" + self.textId = text.split(separator: "#", maxSplits: 1).getOrNil(1).map(String.init) ?? "" self.audioMediaType = switch (audioFile.split(separator: ".").last) { case "opus" : MediaType.opus @@ -120,6 +125,10 @@ struct FlutterMediaOverlayItem { } } + func copyWith(tocTitle: String?, tocHref: String?) -> FlutterMediaOverlayItem { + return FlutterMediaOverlayItem(audio: audio, text: text, position: position, tocTitle: tocTitle, tocHref: tocHref) + } + static func == (lhs: FlutterMediaOverlayItem, rhs: FlutterMediaOverlayItem) -> Bool { return lhs.audio == rhs.audio && lhs.text == rhs.text && lhs.position == rhs.position } @@ -146,12 +155,16 @@ struct FlutterMediaOverlayItem { var locator = Locator( href: href, mediaType: MediaType.xhtml, + title: tocTitle, locations: .init( fragments: frag.map { ["#\($0)"] } ?? [], ) ) if (frag != nil) { - locator.locations.otherLocations = ["cssSelector": "#\(frag!)"] + locator.locations.otherLocations["cssSelector"] = "#\(frag!)" + } + if (tocHref != nil) { + locator.locations.otherLocations["toc"] = tocHref } return locator } @@ -185,11 +198,11 @@ struct FlutterMediaOverlayItem { } // MARK: JSON - static func fromJson(_ json: [String: Any], atPosition position: Int) -> FlutterMediaOverlayItem? { + static func fromJson(_ json: [String: Any], atPosition position: Int, atTocHref: String?) -> FlutterMediaOverlayItem? { guard let audio = json["audio"] as? String, !audio.isEmpty, let text = json["text"] as? String, !text.isEmpty else { return nil } - return FlutterMediaOverlayItem(audio: audio, text: text, position: position) + return FlutterMediaOverlayItem(audio: audio, text: text, position: position, tocHref: atTocHref) } } diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterMediaOverlayNavigator.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterMediaOverlayNavigator.swift index 44c73fca..f770fe11 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterMediaOverlayNavigator.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/navigator/FlutterMediaOverlayNavigator.swift @@ -24,16 +24,8 @@ public class FlutterMediaOverlayNavigator : FlutterAudioNavigator public override func initNavigator() async -> Void { debugPrint(OTAG, "Publication with Synchronized Narration reading-order found!") - let narrationLinks = publication.readingOrder.compactMap { - var link = $0.alternates.filterByMediaType(MediaType("application/vnd.syncnarr+json")!).first - link?.title = $0.title - return link - } - let narrationJson = await narrationLinks.asyncCompactMap { try? await publication.get($0)?.readAsJSONObject().get() } - let mediaOverlays = narrationJson.enumerated().compactMap({ idx, json in FlutterMediaOverlay.fromJson(json, atPosition: idx) }) - - // Assert that we did not lose any MediaOverlays during JSON deserialization. - assert(mediaOverlays.count == narrationLinks.count) + let narrationLinks = publication.narrationLinks + let mediaOverlays = await publication.getMediaOverlays() let audioReadingOrder = mediaOverlays.enumerated().map { (idx, narr) in narrationLinks.getOrNil(idx).map { diff --git a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift index 5d034a4e..1a1cfe98 100644 --- a/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift +++ b/flutter_readium/ios/flutter_readium/Sources/flutter_readium/utils/ReadiumExtensions.swift @@ -23,6 +23,47 @@ extension Publication { self.readingOrder.contains(where: { $0.alternates.contains(where: { $0.mediaType?.matches(MediaType("application/vnd.syncnarr+json")) == true })}) } + var narrationLinks: [Link] { + return self.readingOrder.compactMap { + var link = $0.alternates.filterByMediaType(MediaType("application/vnd.syncnarr+json")!).first + link?.title = $0.title + return link + } + } + + func getMediaOverlays() async -> [FlutterMediaOverlay] { + if (!containsMediaOverlays) { + return [] + } + + let narrationLinks = self.narrationLinks + + let toc: [(String, Link)] = (await getFlattenedToC()).map { ($0.href, $0) } + var lastTocMatch: (String, Link)? = nil + + let narrationJson = await narrationLinks.asyncCompactMap { try? await self.get($0)?.readAsJSONObject().get() } + let mediaOverlays = narrationJson.enumerated().compactMap({ idx, json in + FlutterMediaOverlay.fromJson(json, atPosition: idx, atTocHref: nil) + }).map({ + let items = $0.items.map { item in + // Find best matching title from ToC (via text URL) + if let match = toc.first(where: { tocItem in tocItem.0 == item.text }) { + lastTocMatch = match + return item.copyWith(tocTitle: match.1.title, tocHref: match.1.href) + } else if (lastTocMatch?.1 != nil && lastTocMatch?.0.substringBeforeLast("#") == item.textFile) { + return item.copyWith(tocTitle: lastTocMatch?.1.title, tocHref: lastTocMatch?.1.href) + } + return item + } + return FlutterMediaOverlay(items: items) + }) + + // Assert that we did not lose any MediaOverlays during JSON deserialization. + assert(mediaOverlays.count == narrationLinks.count) + + return mediaOverlays + } + func getFlattenedToC() async -> [Link] { switch await self.tableOfContents() { case .success(let toc): From 79d122224c55944467c9ec60ab18b45f4b96f49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Thu, 5 Mar 2026 18:24:31 +0100 Subject: [PATCH 19/19] Improve find cssSelector and tocId from javascript --- bin/install | 2 +- .../nota/flutter_readium/ReadiumExtensions.kt | 2 +- .../flutter_readium/ReadiumReaderWidget.kt | 4 +- .../flutter_readium/models/PageInformation.kt | 36 +- .../navigators/EpubNavigator.kt | 16 +- .../assets/_helper_scripts/src/EpubPage.ts | 406 +++++++++++++----- .../src/License.readium-swift-toolkit | 31 ++ .../assets/_helper_scripts/src/types.ts | 3 +- .../assets/_helper_scripts/webpack.config.ts | 7 - .../assets/_helper_scripts/webpack.prod.ts | 2 + flutter_readium/assets/helpers/.gitkeep | 0 flutter_readium/assets/helpers/epub.js | 2 +- flutter_readium/package-lock.json | 1 - 13 files changed, 379 insertions(+), 133 deletions(-) create mode 100644 flutter_readium/assets/_helper_scripts/src/License.readium-swift-toolkit delete mode 100644 flutter_readium/assets/helpers/.gitkeep diff --git a/bin/install b/bin/install index c5901ffd..0fc9c430 100755 --- a/bin/install +++ b/bin/install @@ -12,5 +12,5 @@ if [ "$(uname)" == "Darwin" ]; then fi $PROJECT_ROOT/flutter_readium/bin/build_helper_scripts.sh -$PROJECT_ROOT/build_js +$PROJECT_ROOT/bin/build_js $PROJECT_ROOT/bin/update_web_example diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt index db0a6c35..fc6ceadb 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumExtensions.kt @@ -259,7 +259,7 @@ fun Locator.getTextId(): String? { fun Locator.copyWithTimeFragment(time: Double): Locator { return copy( locations = locations.copy( - fragments = listOf("${time}") + fragments = listOf("t=${time}") ) ) } diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt index 5e3167cd..e8bfa38a 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/ReadiumReaderWidget.kt @@ -178,7 +178,7 @@ class ReadiumReaderWidget( ReadiumReader.emitReaderStatusUpdate(ReadiumReaderStatus.Ready) } - emitOnPageChanged(pageIndex, totalPages,locator) + emitOnPageChanged(pageIndex, totalPages, locator) } } @@ -217,7 +217,7 @@ class ReadiumReaderWidget( try { evaluateJavascript("window.epubPage.getPageInformation()")?.let { PageInformation.fromJson( - it + it, locator.href ) }?.let { pageInfo -> emittingLocator = emittingLocator.copy( diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt index fda5ace7..d7d7263c 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/models/PageInformation.kt @@ -1,21 +1,21 @@ package dk.nota.flutter_readium.models +import dk.nota.flutter_readium.cleanHref import dk.nota.flutter_readium.jsonDecode import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.util.Url class PageInformation( - val page: Long?, - val totalPages: Long?, val physicalPage: String?, - val cssSelector: String? + val cssSelector: String?, + val href: String, + val tocId: String? ) { val otherLocations: Map get() { val res = mutableMapOf() - if (page != null && totalPages != null) { - res["currentPage"] = page - res["totalPages"] = totalPages - } physicalPage?.takeIf { it.isNotEmpty() }?.let { res["physicalPage"] = it @@ -24,19 +24,31 @@ class PageInformation( cssSelector?.takeIf { it.isNotEmpty() }?.let { res["cssSelector"] = it } + + tocId?.takeIf { it.isNotEmpty() }?.let { + res["tocId"] = it + } return res; } companion object { - fun fromJson(json: String): PageInformation = fromJson(jsonDecode(json) as JSONObject) + fun fromJson(json: String, href: Url): PageInformation = + fromJson(jsonDecode(json) as JSONObject, href) - fun fromJson(json: JSONObject): PageInformation { + @OptIn(InternalReadiumApi::class) + fun fromJson(json: JSONObject, href: Url): PageInformation { val page = json.optLong("page") val totalPages = json.optLong("totalPages") - val physicalPage = json.optString("physicalPage").takeIf {it.isNotEmpty()} - val cssSelector = json.optString("cssSelector") + val physicalPage = json.optString("physicalPage").takeIf { it.isNotEmpty() } + val cssSelector = json.optNullableString("cssSelector") + val tocId = json.optNullableString("tocId")?.takeIf { it.isNotEmpty() } - return PageInformation(page, totalPages, physicalPage, cssSelector) + return PageInformation( + physicalPage, + cssSelector, + href.cleanHref().toString(), + tocId + ) } } } diff --git a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt index 1ee35d5d..07051e5d 100644 --- a/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt +++ b/flutter_readium/android/src/main/kotlin/dk/nota/flutter_readium/navigators/EpubNavigator.kt @@ -6,8 +6,9 @@ import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.commitNow import dk.nota.flutter_readium.ReadiumReaderWidget.Companion.NAVIGATOR_FRAGMENT_TAG +import dk.nota.flutter_readium.flattenChildren import dk.nota.flutter_readium.fragments.EpubReaderFragment -import dk.nota.flutter_readium.jsonDecode +import dk.nota.flutter_readium.jsonEncode import dk.nota.flutter_readium.models.EpubReaderViewModel import dk.nota.flutter_readium.throttleLatest import dk.nota.flutter_readium.withScope @@ -239,8 +240,21 @@ class EpubNavigator : BaseNavigator, EpubReaderFragment.Listener { } } + fun updateTocInJavascript() { + mainScope.launch { + val tocIds = + publication.tableOfContents.flattenChildren().map { it.href.resolve().fragment } + + for (chunk in tocIds.chunked(1000)) { + evaluateJavascript("window.epubPage.registerToc(${jsonEncode(chunk)})") + } + } + } + override fun onPageLoaded() { Log.d(TAG, "::onPageLoaded") + updateTocInJavascript() + visualListener.onPageLoaded() notifyIsReady() diff --git a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts index c1fc9a57..8fd98bbb 100644 --- a/flutter_readium/assets/_helper_scripts/src/EpubPage.ts +++ b/flutter_readium/assets/_helper_scripts/src/EpubPage.ts @@ -3,164 +3,360 @@ import { initResponsiveTables } from './Tables'; import { PageInformation, Readium } from 'types'; import './EpubPage.scss'; -declare const isIos: boolean; -declare const isAndroid: boolean; -declare const webkit: any; declare const readium: Readium; export class EpubPage { - private get _isScrollModeEnabled(): boolean { - return getComputedStyle(document.documentElement).getPropertyValue("--USER__view") === "readium-scroll-on"; + get #isScrollModeEnabled(): boolean { + return readium.isReflowable === true && getComputedStyle(document.documentElement).getPropertyValue('--USER__view')?.trim() === 'readium-scroll-on"'; } + /** + * List of all ids from the publication's Table of Contents, in lowercase for easier comparison. + * This is used to find the nearest ToC element to the current reading position. + */ + #tocIds: string[] = []; + + /** + * Register all the ToC ids for the publication. Should be called in the EpubNavigator's onPageLoaded() callback. + */ + public registerToc(ids: string[]) { + this.#tocIds = this.#tocIds.concat(ids.map((id) => document.getElementById(id)?.id?.toLocaleLowerCase()).filter((id): id is string => id != null)); + console.error(`Registered ToC ids: ${this.#tocIds.join(", ")}`); + } + + /** + * Find current page information, including physical page, css selector of the current position, and the nearest ToC element id. + */ public getPageInformation(): PageInformation { - const physicalPage = this._findCurrentPhysicalPage(); - const locator = readium.findFirstVisibleLocator(); - let page: number | null; - let totalPages: number | null; - const cssSelector = locator?.locations?.cssSelector ?? null; - - if (readium.isReflowable) { - if (this._isScrollModeEnabled) { - // Page index doesn't make sense in scroll mode. - page = null; - totalPages = null; - } else { - // Calculate page index based on scroll position and viewport width. - const { scrollLeft, scrollWidth } = document.scrollingElement; - const { innerWidth } = window; - page = Math.round(scrollLeft / innerWidth) + 1; - totalPages = Math.round(scrollWidth / innerWidth); - } - } else { - // Fixed layout books is single page files. - page = 1; - totalPages = 1; - } + const physicalPage = this.#findCurrentPhysicalPage(); + const cssSelector = this.#findCssSelector(); + const tocSelector = this.#findTocId(cssSelector); - // Assume fixed layout has only one page, and the physical page index is determined by the current visible element. return { - page, - totalPages, physicalPage, cssSelector, + tocSelector, }; } - private _isPageBreakElement(element: Element | null): boolean { - if (element == null) { - return false; + /** + * Find the nearest cssSelector that is an id. + * + * @param cssSelector + * @returns cssSelector that is guaranteed to be an id, or null if no element can be found. + */ + #findCssSelector(): string | null { + const firstVisibleCssSelector = this.#findFirstVisibleCssSelector(); + const cssSelector = readium.findFirstVisibleLocator()?.locations?.cssSelector ?? null; + if (cssSelector == null) { + return null; } - return element.getAttributeNS("http://www.idpf.org/2007/ops", "type") === 'pagebreak' || element.getAttribute('type') === 'pagebreak'; - } + let selectorElement = document.querySelector(cssSelector) as HTMLElement; + if (selectorElement == null) { + if (firstVisibleCssSelector) { + return firstVisibleCssSelector; + } - private _getPhysicalPageIndexFromElement(element: HTMLElement): string | null { - return element?.getAttribute('title') ?? element?.innerText.trim(); + return null; + } + + if (selectorElement.id) { + return `#${selectorElement.id}`; + } + + if (firstVisibleCssSelector) { + return firstVisibleCssSelector; + } + + // Some locators land inside injected spans + if (selectorElement.nodeType !== Node.ELEMENT_NODE) { + selectorElement = selectorElement.parentElement; + } + + // 1. Closest ancestor with ID + const ancestor = selectorElement.closest('[id]'); + if (ancestor) { + return `#${ancestor.id}`; + }; + + // 2. Nearest element with ID, either preceding or following the current element in the document order. + const precedingElementXPath = 'preceding::*[@id][1]'; + const followingElementXPath = 'following::*[@id][1]'; + + for (const xpath of [precedingElementXPath, followingElementXPath]) { + const result = document.evaluate( + xpath, + selectorElement, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ); + + if (result.singleNodeValue instanceof Element) { + return `#${result.singleNodeValue.id}`; + } + } } - private _findPhysicalPageIndex(element: Element | null): string | null { - if (element == null || !(element instanceof Element)) { + /** + * Find the nearest cssSelector that is visible, starting from the first visible element and ending at the given cssSelector. + */ + #findFirstVisibleCssSelector(): string | null { + const firstVisibleElement = this.#findFirstVisibleElement(); + const lastVisibleElement = this.#findLastVisibleElement(); + + if (firstVisibleElement == null || lastVisibleElement == null) { return null; - } else if (this._isPageBreakElement(element)) { - return this._getPhysicalPageIndexFromElement(element as HTMLElement); } - return null; - } + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT + ); + + walker.currentNode = firstVisibleElement; + let node: Node = firstVisibleElement; + + while (node) { + if (node instanceof Element && node.id) return `#${node.id}`; - private _getAllSiblings(elem: ChildNode): HTMLElement[] | null { - const sibs: HTMLElement[] = []; - elem = elem?.parentNode?.firstChild as HTMLElement; - do { - if (elem?.nodeType === 3) continue; // text node - sibs.push(elem as HTMLElement); - } while ((elem = elem?.nextSibling as HTMLElement)); - return sibs; + if (node === lastVisibleElement) break; + + node = walker.nextNode(); + } } /** - * Find the current physical page index. - * - * @returns The physical page index, or null if it cannot be determined. + * Find the preceding Table of Contents element id. + * @param cssSelector + * @returns */ - private _findCurrentPhysicalPage(): string | null { - const cssSelector = readium.findFirstVisibleLocator()?.locations?.cssSelector; + #findTocId(cssSelector: string | null): string | null { + if (this.#tocIds == null || this.#tocIds.length === 0 || cssSelector == null) { + return null; + } - let element = document.querySelector(cssSelector); - if (element == null) { - return; + // First check if any of the registered ToC ids are currently visible and return the first one found. + for (const tocId of this.#tocIds) { + const tocElement = document.getElementById(tocId); + if (this.#isElementVisible(tocElement)) { + return `#${tocId}`; + } } - if (this._isPageBreakElement(element)) { - return this._getPhysicalPageIndexFromElement(element as HTMLElement); + // Then find the nearest ToC id to the current cssSelector, either preceding or following in the document order. + const selectorElement = document.querySelector(cssSelector); + if (selectorElement == null) { + return null; } - while (!!element && element.nodeType === Node.ELEMENT_NODE) { - const siblings = this._getAllSiblings(element); - if (siblings == null) { - return; - } - const currentIndex = siblings.findIndex((e) => e?.isEqualNode(element)); + // If the current element itself is a ToC element, return it immediately. + if (selectorElement.id && this.#tocIds.includes(selectorElement.id.toLocaleLowerCase())) { + return `#${selectorElement.id}`; + } - for (let i = currentIndex; i >= 0; i--) { - const e = siblings[i]; + // Find the preceding ToC element. + const predicate = this.#tocIds.map((id) => `@id="${id}"`).join(" or "); - const pageBreakIndex = this._findPhysicalPageIndex(e); - if (pageBreakIndex != null) { - return pageBreakIndex; - } + const precedingElementXPath = `preceding::*[${predicate}][1]`; + const result = document.evaluate( + precedingElementXPath, + selectorElement, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ); + + if (result.singleNodeValue instanceof Element) { + return `#${result.singleNodeValue.id}`; + } + + // This might be a special case, where we start just before the first ToC element. + let firstTocElement: Element; + for (const tocId of this.#tocIds) { + const tocElement = document.getElementById(tocId); + if (tocElement) { + firstTocElement = tocElement; + break; } + } + + if (firstTocElement == null) { + // Really shouldn't happen. + return null; + } - element = element.parentNode as HTMLElement; + // Walk backwards from the first to see if the find the current selector element. + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT + ); - if (element == null || element.nodeName.toLowerCase() === 'body') { - return document.querySelector("head [name='webpub:currentPage']")?.getAttribute('content'); + walker.currentNode = firstTocElement; + let node: Node = firstTocElement; + + while (node) { + if (node instanceof Element && node === selectorElement) { + // First ToC element is the current one. + return `#${firstTocElement.id}`; } + + node = walker.previousNode(); } } - private _log(...args: unknown[]) { - // Alternative for webkit in order to print logs in flutter log outputs. - - if (this._isIos()) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - webkit?.messageHandlers.log.postMessage( - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - [].slice - .call(args) - .map((x: unknown) => (x instanceof String ? `${x}` : `${JSON.stringify(x)}`)) - .join(', '), - ); + /** + * Is the given element a page break element, based on EPUB specification or common practices? + * @param element + * @returns + */ + #isPageBreakElement(element: Element | null): boolean { + if (element == null) { + return false; + } + + return element.getAttributeNS("http://www.idpf.org/2007/ops", "type") === 'pagebreak' || element.getAttribute('type') === 'pagebreak' || element.getAttribute('epub:type') === 'pagebreak'; + } + + /** + * Get the physical page text from the given element, if it is a page break element. + * @param element + * @returns + */ + #getPhysicalPageText(element: HTMLElement): string | null { + if (!this.#isPageBreakElement(element)) { + return null; + } + return element?.getAttribute('title') ?? element?.innerText.trim(); + } + + /** + * Find the current physical page index. + * + * @returns The physical page index, or null if it cannot be determined. + */ + #findCurrentPhysicalPage(): string | null { + let element = this.#findFirstVisibleElement(); + if (!(element instanceof HTMLElement)) { return; } - // eslint-disable-next-line no-console - console.log(JSON.stringify(args)); + if (this.#isPageBreakElement(element)) { + return this.#getPhysicalPageText(element); + } + + const result = document.evaluate( + 'preceding::*[@epub:type="pagebreak" or @type="pagebreak" or @role="doc-pagebreak" or contains(@class,"pagebreak")][1]', + element, + (prefix: string) => { + if (prefix === "epub") { + return "http://www.idpf.org/2007/ops"; + } + return null; + }, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ); + + if (result.singleNodeValue instanceof Element && this.#isPageBreakElement(result.singleNodeValue)) { + return this.#getPhysicalPageText(result.singleNodeValue as HTMLElement); + } } - private _errorLog(...error: any) { - this._log(`v===v===v===v===v===v`); - this._log(`Error:`, error); - this._log(`Stack:`, error?.stack ?? new Error().stack.replace('\n', '->').replace('_errorLog', '')); - this._log(`^===^===^===^===^===^`); + /** + * Find the first visible element in the document. + * @returns The first visible element, or null if none is found. + */ + #findFirstVisibleElement(): Element | null { + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT + ); + + walker.currentNode = document.body.firstElementChild ?? document.body; + + let node = walker.currentNode; + while (node) { + if (node instanceof Element && this.#isElementVisible(node)) { + return node; + } + + node = walker.nextNode(); + } } - private _isIos(): boolean { - try { - return isIos; - } catch (error) { - return false; + /** + * Find the last visible element in the document. + * @returns The last visible element, or null if none is found. + */ + #findLastVisibleElement() { + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT + ); + + walker.currentNode = document.body.lastElementChild ?? document.body; + + let node = walker.currentNode; + while (node) { + if (node instanceof Element && this.#isElementVisible(node)) { + return node; + } + + node = walker.previousNode(); } } - private _isAndroid(): boolean { - try { - return isAndroid; - } catch (error) { + // Functions below was copied from Swift-toolkit - see License.readium-swift-toolkit for details. + #isElementVisible(element: Element | null): boolean { + if (this.#shouldIgnoreElement(element)) { return false; } + + if (readium.isFixedLayout) return true; + + if (element === document.body || element === document.documentElement) { + return true; + } + + if (!document || !document.documentElement || !document.body) { + return false; + } + + const rect = element.getBoundingClientRect(); + if (this.#isScrollModeEnabled) { + return rect.bottom > 0 && rect.top < window.innerHeight; + } else { + return rect.right > 0 && rect.left < window.innerWidth; + } + } + + #shouldIgnoreElement(element: Element | null): boolean { + if (element == null) { + return true; + } + + const elStyle = getComputedStyle(element); + if (elStyle) { + const display = elStyle.getPropertyValue("display"); + if (display != "block") { + return true; + } + // Cannot be relied upon, because web browser engine reports invisible when out of view in + // scrolled columns! + // const visibility = elStyle.getPropertyValue("visibility"); + // if (visibility === "hidden") { + // return false; + // } + const opacity = elStyle.getPropertyValue("opacity"); + if (opacity === "0") { + return true; + } + } + + return false; } } diff --git a/flutter_readium/assets/_helper_scripts/src/License.readium-swift-toolkit b/flutter_readium/assets/_helper_scripts/src/License.readium-swift-toolkit new file mode 100644 index 00000000..01c2b32d --- /dev/null +++ b/flutter_readium/assets/_helper_scripts/src/License.readium-swift-toolkit @@ -0,0 +1,31 @@ +Applies to Readium javascript functions. + +BSD 3-Clause License + +Copyright (c) 2017, Readium +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flutter_readium/assets/_helper_scripts/src/types.ts b/flutter_readium/assets/_helper_scripts/src/types.ts index 34b55e19..caca4bf3 100644 --- a/flutter_readium/assets/_helper_scripts/src/types.ts +++ b/flutter_readium/assets/_helper_scripts/src/types.ts @@ -134,8 +134,7 @@ export interface CurrentSelection { } export interface PageInformation { - page?: number | null; - totalPages?: number | null; physicalPage?: string | null; cssSelector?: string | null; + tocSelector?: string | null; } diff --git a/flutter_readium/assets/_helper_scripts/webpack.config.ts b/flutter_readium/assets/_helper_scripts/webpack.config.ts index 434e41c2..964e2867 100644 --- a/flutter_readium/assets/_helper_scripts/webpack.config.ts +++ b/flutter_readium/assets/_helper_scripts/webpack.config.ts @@ -22,13 +22,6 @@ export default function (): webpack.Configuration { clean: true, }, plugins: [ - new CopyPlugin({ - patterns: [ - { - from: 'public/.gitkeep', - }, - ], - }), new webpack.ProgressPlugin(), new MiniCssExtractPlugin({ filename: '[name].css' }), ], diff --git a/flutter_readium/assets/_helper_scripts/webpack.prod.ts b/flutter_readium/assets/_helper_scripts/webpack.prod.ts index 6b239007..4005fd0f 100644 --- a/flutter_readium/assets/_helper_scripts/webpack.prod.ts +++ b/flutter_readium/assets/_helper_scripts/webpack.prod.ts @@ -3,8 +3,10 @@ import * as webpack from 'webpack'; export default { mode: 'production', + // devtool: 'source-map', optimization: { minimizer: [new TerserPlugin()], + minimize: true, splitChunks: { cacheGroups: { diff --git a/flutter_readium/assets/helpers/.gitkeep b/flutter_readium/assets/helpers/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/flutter_readium/assets/helpers/epub.js b/flutter_readium/assets/helpers/epub.js index 524a407f..1e91769c 100644 --- a/flutter_readium/assets/helpers/epub.js +++ b/flutter_readium/assets/helpers/epub.js @@ -1 +1 @@ -var epub;(()=>{"use strict";var e={324(e,t,r){r.r(t)},679(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.initResponsiveTables=function(){let e=Array.from(document.querySelectorAll("table"));if(e=e.filter((e=>!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,r=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:r++}))})),t>r)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,r=e.querySelectorAll("tbody tr");if(1===t&&Array.from(r).every((e=>2===e.querySelectorAll("td").length))){const t=r[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,r)=>e.querySelector(`h${r+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,r=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const n=document.createElement("tbody"),l=document.createElement("tr"),o=document.createElement("tr");l.innerHTML=" ",o.innerHTML=`${t}`,r.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,l.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),o.appendChild(t)}})),n.appendChild(l),n.appendChild(o),e.appendChild(n)}(e)}!function(e){let t=null,r=[];if(e.tHead)t=e.tHead.rows[0],r=Array.from(t.cells);else{const n=e.rows[0];n&&n.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=n,r=Array.from(n.cells),e.classList.add("has-first-row-headers"))}if(r.length){e.classList.add("has-header");const n=r.map((e=>{var t;let r=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return r.length&&(r+=":"),r}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",n[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function r(n){var l=t[n];if(void 0!==l)return l.exports;var o=t[n]={exports:{}};return e[n](o,o.exports,r),o.exports}r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.EpubPage=void 0;const t=r(679);r(324);class l{get _isScrollModeEnabled(){return"readium-scroll-on"===getComputedStyle(document.documentElement).getPropertyValue("--USER__view")}getPageInformation(){var e,t;const r=this._findCurrentPhysicalPage(),n=readium.findFirstVisibleLocator();let l,o;const i=null!==(t=null===(e=null==n?void 0:n.locations)||void 0===e?void 0:e.cssSelector)&&void 0!==t?t:null;if(readium.isReflowable)if(this._isScrollModeEnabled)l=null,o=null;else{const{scrollLeft:e,scrollWidth:t}=document.scrollingElement,{innerWidth:r}=window;l=Math.round(e/r)+1,o=Math.round(t/r)}else l=1,o=1;return{page:l,totalPages:o,physicalPage:r,cssSelector:i}}_isPageBreakElement(e){return null!=e&&("pagebreak"===e.getAttributeNS("http://www.idpf.org/2007/ops","type")||"pagebreak"===e.getAttribute("type"))}_getPhysicalPageIndexFromElement(e){var t;return null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim()}_findPhysicalPageIndex(e){return null!=e&&e instanceof Element&&this._isPageBreakElement(e)?this._getPhysicalPageIndexFromElement(e):null}_getAllSiblings(e){var t;const r=[];e=null===(t=null==e?void 0:e.parentNode)||void 0===t?void 0:t.firstChild;do{3!==(null==e?void 0:e.nodeType)&&r.push(e)}while(e=null==e?void 0:e.nextSibling);return r}_findCurrentPhysicalPage(){var e,t,r;const n=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector;let l=document.querySelector(n);if(null!=l){if(this._isPageBreakElement(l))return this._getPhysicalPageIndexFromElement(l);for(;l&&l.nodeType===Node.ELEMENT_NODE;){const e=this._getAllSiblings(l);if(null==e)return;for(let t=e.findIndex((e=>null==e?void 0:e.isEqualNode(l)));t>=0;t--){const r=e[t],n=this._findPhysicalPageIndex(r);if(null!=n)return n}if(l=l.parentNode,null==l||"body"===l.nodeName.toLowerCase())return null===(r=document.querySelector("head [name='webpub:currentPage']"))||void 0===r?void 0:r.getAttribute("content")}}}_log(...e){this._isIos()?null===webkit||void 0===webkit||webkit.messageHandlers.log.postMessage([].slice.call(e).map((e=>e instanceof String?`${e}`:`${JSON.stringify(e)}`)).join(", ")):console.log(JSON.stringify(e))}_errorLog(...e){var t;this._log("v===v===v===v===v===v"),this._log("Error:",e),this._log("Stack:",null!==(t=null==e?void 0:e.stack)&&void 0!==t?t:(new Error).stack.replace("\n","->").replace("_errorLog","")),this._log("^===^===^===^===^===^")}_isIos(){try{return isIos}catch(e){return!1}}_isAndroid(){try{return isAndroid}catch(e){return!1}}}function o(){window.epubPage||((0,t.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",o),window.epubPage=new l)}e.EpubPage=l,"loading"!==document.readyState?window.setTimeout(o):document.addEventListener("DOMContentLoaded",o)})(),epub=n})(); \ No newline at end of file +var epub;(()=>{"use strict";var e={324(e,t,n){n.r(t)},295(e,t,n){var r,o,l,i,a,d,c,u,s,f,m,h,p,y=this&&this.__classPrivateFieldGet||function(e,t,n,r){if("a"===n&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?r:"a"===n?r.call(e):r?r.value:t.get(e)},E=this&&this.__classPrivateFieldSet||function(e,t,n,r,o){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!o:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===r?o.call(e,n):o?o.value=n:t.set(e,n),n};Object.defineProperty(t,"__esModule",{value:!0}),t.EpubPage=void 0;const b=n(679);n(324);class g{constructor(){r.add(this),l.set(this,[])}registerToc(e){E(this,l,y(this,l,"f").concat(e.map((e=>{var t,n;return null===(n=null===(t=document.getElementById(e))||void 0===t?void 0:t.id)||void 0===n?void 0:n.toLocaleLowerCase()})).filter((e=>null!=e))),"f"),console.error(`Registered ToC ids: ${y(this,l,"f").join(", ")}`)}getPageInformation(){const e=y(this,r,"m",s).call(this),t=y(this,r,"m",i).call(this);return{physicalPage:e,cssSelector:t,tocSelector:y(this,r,"m",d).call(this,t)}}}function v(){window.epubPage||((0,b.initResponsiveTables)(),document.removeEventListener("DOMContentLoaded",v),window.epubPage=new g)}t.EpubPage=g,l=new WeakMap,r=new WeakSet,o=function(){var e;return!0===readium.isReflowable&&'readium-scroll-on"'===(null===(e=getComputedStyle(document.documentElement).getPropertyValue("--USER__view"))||void 0===e?void 0:e.trim())},i=function(){var e,t,n;const o=y(this,r,"m",a).call(this),l=null!==(n=null===(t=null===(e=readium.findFirstVisibleLocator())||void 0===e?void 0:e.locations)||void 0===t?void 0:t.cssSelector)&&void 0!==n?n:null;if(null==l)return null;let i=document.querySelector(l);if(null==i)return o||null;if(i.id)return`#${i.id}`;if(o)return o;i.nodeType!==Node.ELEMENT_NODE&&(i=i.parentElement);const d=i.closest("[id]");if(d)return`#${d.id}`;for(const e of["preceding::*[@id][1]","following::*[@id][1]"]){const t=document.evaluate(e,i,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null);if(t.singleNodeValue instanceof Element)return`#${t.singleNodeValue.id}`}},a=function(){const e=y(this,r,"m",f).call(this),t=y(this,r,"m",m).call(this);if(null==e||null==t)return null;const n=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT);n.currentNode=e;let o=e;for(;o;){if(o instanceof Element&&o.id)return`#${o.id}`;if(o===t)break;o=n.nextNode()}},d=function(e){if(null==y(this,l,"f")||0===y(this,l,"f").length||null==e)return null;for(const e of y(this,l,"f")){const t=document.getElementById(e);if(y(this,r,"m",h).call(this,t))return`#${e}`}const t=document.querySelector(e);if(null==t)return null;if(t.id&&y(this,l,"f").includes(t.id.toLocaleLowerCase()))return`#${t.id}`;const n=`preceding::*[${y(this,l,"f").map((e=>`@id="${e}"`)).join(" or ")}][1]`,o=document.evaluate(n,t,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null);if(o.singleNodeValue instanceof Element)return`#${o.singleNodeValue.id}`;let i;for(const e of y(this,l,"f")){const t=document.getElementById(e);if(t){i=t;break}}if(null==i)return null;const a=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT);a.currentNode=i;let d=i;for(;d;){if(d instanceof Element&&d===t)return`#${i.id}`;d=a.previousNode()}},c=function(e){return null!=e&&("pagebreak"===e.getAttributeNS("http://www.idpf.org/2007/ops","type")||"pagebreak"===e.getAttribute("type")||"pagebreak"===e.getAttribute("epub:type"))},u=function(e){var t;return y(this,r,"m",c).call(this,e)?null!==(t=null==e?void 0:e.getAttribute("title"))&&void 0!==t?t:null==e?void 0:e.innerText.trim():null},s=function(){let e=y(this,r,"m",f).call(this);if(!(e instanceof HTMLElement))return;if(y(this,r,"m",c).call(this,e))return y(this,r,"m",u).call(this,e);const t=document.evaluate('preceding::*[@epub:type="pagebreak" or @type="pagebreak" or @role="doc-pagebreak" or contains(@class,"pagebreak")][1]',e,(e=>"epub"===e?"http://www.idpf.org/2007/ops":null),XPathResult.FIRST_ORDERED_NODE_TYPE,null);return t.singleNodeValue instanceof Element&&y(this,r,"m",c).call(this,t.singleNodeValue)?y(this,r,"m",u).call(this,t.singleNodeValue):void 0},f=function(){var e;const t=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT);t.currentNode=null!==(e=document.body.firstElementChild)&&void 0!==e?e:document.body;let n=t.currentNode;for(;n;){if(n instanceof Element&&y(this,r,"m",h).call(this,n))return n;n=t.nextNode()}},m=function(){var e;const t=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT);t.currentNode=null!==(e=document.body.lastElementChild)&&void 0!==e?e:document.body;let n=t.currentNode;for(;n;){if(n instanceof Element&&y(this,r,"m",h).call(this,n))return n;n=t.previousNode()}},h=function(e){if(y(this,r,"m",p).call(this,e))return!1;if(readium.isFixedLayout)return!0;if(e===document.body||e===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;const t=e.getBoundingClientRect();return y(this,r,"a",o)?t.bottom>0&&t.top0&&t.left!e.classList.contains("docx-table")&&!e.classList.contains("transparent-table")&&!e.classList.contains("plain-table"))),!e||0===e.length)return;e.forEach((e=>{if(function(e){let t=0,n=0;if(e.querySelectorAll("tr").forEach((e=>{e.querySelectorAll("td").forEach((e=>{""===e.textContent.trim()?t++:n++}))})),t>n)return e.classList.add("plain-table"),!0;return!1}(e))return;const t=e.querySelectorAll("thead th").length,n=e.querySelectorAll("tbody tr");if(1===t&&Array.from(n).every((e=>2===e.querySelectorAll("td").length))){const t=n[0].querySelectorAll("td");if(Array.from(t).every((e=>e.querySelector("strong")||Array.from({length:6},((t,n)=>e.querySelector(`h${n+1}`))).some((e=>null!==e)))))return void e.classList.add("plain-table");!function(e){const t=e.querySelector("thead th").textContent,n=Array.from(e.querySelectorAll("tbody td"));e.innerHTML="";const r=document.createElement("tbody"),o=document.createElement("tr"),l=document.createElement("tr");o.innerHTML=" ",l.innerHTML=`${t}`,n.forEach(((e,t)=>{if(t%2==0){const t=document.createElement("td");t.innerHTML=`

${e.textContent}

`,o.appendChild(t)}else{const t=document.createElement("td");Array.from(e.childNodes).forEach((e=>t.appendChild(e.cloneNode(!0)))),l.appendChild(t)}})),r.appendChild(o),r.appendChild(l),e.appendChild(r)}(e)}!function(e){let t=null,n=[];if(e.tHead)t=e.tHead.rows[0],n=Array.from(t.cells);else{const r=e.rows[0];r&&r.querySelector("th, h1, h2, h3, h4, h5, h6, strong")&&(t=r,n=Array.from(r.cells),e.classList.add("has-first-row-headers"))}if(n.length){e.classList.add("has-header");const r=n.map((e=>{var t;let n=(null===(t=e.textContent)||void 0===t?void 0:t.trim())||"";return n.length&&(n+=":"),n}));Array.from(e.rows).filter((e=>e!==t)).forEach((e=>{if(Array.from(e.cells).forEach(((e,t)=>{e.setAttribute("data-th",r[t]||"")})),!e.cells[0].getAttribute("data-th")){e.cells[0].classList.add("mobile-header");const t=document.createElement("h6");t.textContent=e.cells[0].textContent,e.cells[0].textContent="",e.cells[0].appendChild(t)}}))}}(e)}))}}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var l=t[r]={exports:{}};return e[r].call(l.exports,l,l.exports,n),l.exports}n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r=n(295);epub=r})(); \ No newline at end of file diff --git a/flutter_readium/package-lock.json b/flutter_readium/package-lock.json index 29b3ea0d..3a334d4f 100644 --- a/flutter_readium/package-lock.json +++ b/flutter_readium/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "flutter_readium", "dependencies": { "@readium/navigator": "^2.2.4", "@readium/navigator-html-injectables": "^2.2.1",