From d4b9ee5cabc9580b79f485a3fd1e178f0d2f24c8 Mon Sep 17 00:00:00 2001
From: k6G52m4Dz75W <74605402+k6G52m4Dz75W@users.noreply.github.com>
Date: Wed, 9 Sep 2026 22:00:30 +0800
Subject: [PATCH 01/20] feat(settings): show web engine and version on the
About page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reader features vary per engine (@layer/:has floors, execCommand,
clipboard behavior), so bug reports need the engine name and build, not
just the app version. Add a Web-engine row under the app version in
Settings → About, parsed from the UA per Tauri platform:
- Windows: WebView2 (Edg/ token = real Evergreen build)
- Android: system WebView (Chrome/ token, ; wv) marker)
- macOS/iOS: WebKit via the Version/ token (follows the system release,
unlike readest's frozen AppleWebKit/605.1.15 parse)
- Linux: WebKitGTK without a version — the UA carries only frozen
tokens; the real one is the system libwebkit2gtk package
- plain vite dev in a browser: generic Chrome/Edge/Firefox/Safari labels
Labels localized for all 7 locales (settings.webviewEngine).
---
.../src/components/settings/AboutSettings.tsx | 7 ++
packages/app/src/lib/webview-info.ts | 87 +++++++++++++++++++
.../core/src/i18n/locales/en/settings.json | 1 +
.../core/src/i18n/locales/es/settings.json | 1 +
.../core/src/i18n/locales/fr/settings.json | 1 +
.../core/src/i18n/locales/ja/settings.json | 1 +
.../core/src/i18n/locales/ko/settings.json | 1 +
.../core/src/i18n/locales/zh-TW/settings.json | 1 +
.../core/src/i18n/locales/zh/settings.json | 1 +
9 files changed, 101 insertions(+)
create mode 100644 packages/app/src/lib/webview-info.ts
diff --git a/packages/app/src/components/settings/AboutSettings.tsx b/packages/app/src/components/settings/AboutSettings.tsx
index 8407de6e2..fd2d4268e 100644
--- a/packages/app/src/components/settings/AboutSettings.tsx
+++ b/packages/app/src/components/settings/AboutSettings.tsx
@@ -18,6 +18,7 @@ import {
resetStatus,
subscribeToUpdates,
} from "@/lib/updater";
+import { formatWebviewInfo } from "@/lib/webview-info";
import { getVersion } from "@tauri-apps/api/app";
import {
AlertCircle,
@@ -37,6 +38,8 @@ import {
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
+const WEBVIEW_LABEL = formatWebviewInfo();
+
const TECH_STACK = [
{ name: "Tauri", descKey: "settings.techStackTauri", icon: Shield },
{ name: "React", descKey: "settings.techStackReact", icon: Code2 },
@@ -147,6 +150,10 @@ export function AboutSettings() {
+
diff --git a/packages/app/src/components/settings/FeedbackSettings.tsx b/packages/app/src/components/settings/FeedbackSettings.tsx
index c16b4e91a..824895219 100644
--- a/packages/app/src/components/settings/FeedbackSettings.tsx
+++ b/packages/app/src/components/settings/FeedbackSettings.tsx
@@ -104,7 +104,7 @@ export function FeedbackSettings() {
webview: webview || undefined,
locale: i18n.language || navigator.language,
});
- }, [appVersion, webview]);
+ }, [appVersion, webview, i18n.language]);
const loadRecords = useCallback(async (refreshStatus = false) => {
const history = await getFeedbackHistory();
diff --git a/packages/app/src/lib/webview-info.ts b/packages/app/src/lib/webview-info.ts
index ab328b6c4..88641271e 100644
--- a/packages/app/src/lib/webview-info.ts
+++ b/packages/app/src/lib/webview-info.ts
@@ -67,5 +67,5 @@ export async function getWebviewLabel(): Promise {
const { engine, version } = getWebviewInfo();
if (!engine) return "";
const fullVersion = (await getFullVersionFromClientHints(engine)) || version;
- return formatWebviewInfo({ engine, version: fullVersion || version });
+ return formatWebviewInfo({ engine, version: fullVersion });
}
diff --git a/packages/core/src/utils/webview-info.ts b/packages/core/src/utils/webview-info.ts
index 3e7992980..4e8145dc7 100644
--- a/packages/core/src/utils/webview-info.ts
+++ b/packages/core/src/utils/webview-info.ts
@@ -89,3 +89,12 @@ export function parseWebviewInfo(ua: string, inAppShell = true): WebviewInfo {
export function formatWebviewInfo(info: WebviewInfo): string {
return info.engine ? (info.version ? `${info.engine} ${info.version}` : info.engine) : "";
}
+
+/**
+ * The two-line version info pasted into bug reports — the pair (app version +
+ * web engine build) is what the issue template needs. Shared by the desktop
+ * About card and the mobile About screen so the format cannot drift.
+ */
+export function buildVersionInfo(appVersion: string, webviewLabel: string): string {
+ return [`ReadAny ${appVersion}`, webviewLabel].filter(Boolean).join("\n");
+}
diff --git a/packages/feedback-worker/src/index.ts b/packages/feedback-worker/src/index.ts
index 90a25cf40..5517672ff 100644
--- a/packages/feedback-worker/src/index.ts
+++ b/packages/feedback-worker/src/index.ts
@@ -299,7 +299,7 @@ function buildIssueBody(
const clean = (value: unknown): string =>
String(value ?? "unknown")
.replace(/[\r\n\t]+/g, " ")
- .replace(/[`[\]<>!]/g, "")
+ .replace(/[`[\]<>!@]/g, "")
.slice(0, 300);
const details = [
`### Type\n${TYPE_LABELS[payload.type]}`,
From 54e4048e7ce058a3ff2cef3f9e0664baf0752e53 Mon Sep 17 00:00:00 2001
From: k6G52m4Dz75W <74605402+k6G52m4Dz75W@users.noreply.github.com>
Date: Fri, 11 Sep 2026 20:58:33 +0800
Subject: [PATCH 17/20] chore(mobile-dev): regenerate reader.html (justify
engine + fullVersion UA report)
---
packages/app-expo/assets/reader/reader.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/app-expo/assets/reader/reader.html b/packages/app-expo/assets/reader/reader.html
index 43b01a6a8..c3e15b765 100644
--- a/packages/app-expo/assets/reader/reader.html
+++ b/packages/app-expo/assets/reader/reader.html
@@ -5631,7 +5631,7 @@
0% { opacity: 1; }
100% { opacity: 0; }
}
- `,C.head.appendChild(B)}a&&r>0&&setTimeout(()=>{u.classList.add("foliate-arrow-fadeout"),setTimeout(()=>{u.parentNode&&u.parentNode.removeChild(u)},1e3)},r)}return c.append(u),c}static copyImage([e],t={}){let{src:A}=t,s=Ii("image"),{left:n,top:a,height:r,width:o}=e;return s.setAttribute("href",A),s.setAttribute("x",n),s.setAttribute("y",a),s.setAttribute("height",r),s.setAttribute("width",o),s}};Dc=new WeakMap,Od=new WeakMap,Vo=new WeakMap,Rs=new WeakMap,Wd=new WeakMap,ka=new WeakMap;var tU=i=>{let e=0,t=A=>{if(A.id=e++,A.subitems)for(let s of A.subitems)t(s)};for(let A of i)t(A);return i},ek=i=>i.flatMap(e=>e.subitems?.length?[e,ek(e.subitems)].flat():e),Vd=class{async init({toc:e,ids:t,splitHref:A,getFragment:s}){tU(e);let n=ek(e),a=new Map;for(let[o,c]of n.entries()){let[g,l]=await A(c?.href)??[],I={fragment:l,item:c};a.has(g)?a.get(g).items.push(I):a.set(g,{prev:n[o-1],items:[I]})}let r=new Map;for(let[o,c]of t.entries())a.has(c)?r.set(c,a.get(c)):r.set(c,r.get(t[o-1]));this.ids=t,this.map=r,this.getFragment=s}getProgress(e,t){if(!this.ids)return;let A=this.ids[e],s=this.map.get(A);if(!s)return null;let{prev:n,items:a}=s;if(!a)return n;if(!t||a.length===1&&!a[0].fragment)return a[0].item;let r=t.startContainer.getRootNode();for(let[o,{fragment:c}]of a.entries()){let g=this.getFragment(r,c);if(g&&t.comparePoint(g,0)>0)return a[o-1]?.item??n}return a[a.length-1].item}},W1,tk,O1=class{constructor(e,t,A){D(this,W1);this.sizes=e.map(s=>s.linear!="no"&&s.size>0?s.size:0),this.sizePerLoc=t,this.sizePerTimeUnit=A,this.sizeTotal=this.sizes.reduce((s,n)=>s+n,0),this.sectionFractions=x(this,W1,tk).call(this)}getProgress(e,t,A=0){let{sizes:s,sizePerLoc:n,sizePerTimeUnit:a,sizeTotal:r}=this,o=s[e]??0,g=s.slice(0,e).reduce((u,f)=>u+f,0)+t*o,l=g+A*o,I=r-g,d=(1-t)*o;return{fraction:l/r,section:{current:e,total:s.length},location:{current:Math.floor(g/n),next:Math.floor(l/n),total:Math.ceil(r/n)},time:{section:d/a,total:I/a}}}getSection(e){if(e<=0)return[0,0];if(e>=1)return[this.sizes.length-1,1];e=e+Number.EPSILON;let{sizeTotal:t}=this,A=this.sectionFractions.findIndex(n=>n>e)-1;if(A<0)return[0,0];for(;!this.sizes[A];)A++;let s=(e-this.sectionFractions[A])/(this.sizes[A]/t);return[A,s]}};W1=new WeakSet,tk=function(){let{sizeTotal:e}=this,t=[0],A=0;for(let s of this.sizes)t.push((A+=s)/e);return t};var iU=(i,e)=>{let t=[];for(let A=e.currentNode;A;A=e.nextNode()){let s=i.comparePoint(A,0);if(s===0)t.push(A);else if(s>0)break}return t},AU=(i,e)=>{let t=[];for(let A=e.nextNode();A;A=e.nextNode())t.push(A);return t},sU=NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT|NodeFilter.SHOW_CDATA_SECTION,nU=i=>{if(i.nodeType===1){let e=i.tagName.toLowerCase();return e==="script"||e==="style"||e==="rt"||e==="rp"?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_SKIP}return NodeFilter.FILTER_ACCEPT},tm=function*(i,e,t){let A=i.commonAncestorContainer??i.body??i,s=document.createTreeWalker(A,sU,{acceptNode:t||nU}),a=(i.commonAncestorContainer?iU:AU)(i,s),r=a.map(c=>c.nodeValue),o=(c,g,l,I)=>{let d=document.createRange();return d.setStart(a[c],g),d.setEnd(a[l],I),d};for(let c of e(r,o))yield c};var M1="foliate-search:",hL="foliate-tts:",HK=async i=>{let e=new Uint8Array(await i.slice(0,4).arrayBuffer());return e[0]===80&&e[1]===75&&e[2]===3&&e[3]===4},JK=async i=>{let e=new Uint8Array(await i.slice(0,5).arrayBuffer());return e[0]===37&&e[1]===80&&e[2]===68&&e[3]===70&&e[4]===45},YK=({name:i,type:e})=>e==="application/vnd.comicbook+zip"||i.endsWith(".cbz"),PK=({name:i,type:e})=>e==="application/x-fictionbook+xml"||i.endsWith(".fb2"),_K=({name:i,type:e})=>e==="application/x-zip-compressed-fb2"||i.endsWith(".fb2.zip")||i.endsWith(".fbz"),KK=async i=>{let{configure:e,ZipReader:t,BlobReader:A,TextWriter:s,BlobWriter:n}=await Promise.resolve().then(()=>(wm(),tF));e({useWebWorkers:!1});let r=await new t(new A(i)).getEntries(),o=new Map(r.map(d=>[d.filename,d])),c=d=>(u,...f)=>o.has(u)?d(o.get(u),...f):null,g=c(d=>d.getData(new s)),l=c((d,u)=>d.getData(new n(u)));return{entries:r,loadText:g,loadBlob:l,getSize:d=>o.get(d)?.uncompressedSize??0}},IL=async i=>i.isFile?i:(await Promise.all(Array.from(await new Promise((e,t)=>i.createReader().readEntries(A=>e(A),A=>t(A))),IL))).flat(),qK=async i=>{let e=await IL(i),t=await Promise.all(e.map(g=>new Promise((l,I)=>g.file(d=>l([d,g.fullPath]),d=>I(d))))),A=new Map(t.map(([g,l])=>[l.replace(`${i.fullPath}/`,""),g])),s=new TextDecoder,n=g=>g?s.decode(g):null,a=g=>A.get(g)?.arrayBuffer()??null;return{loadText:async g=>n(await a(g)),loadBlob:g=>A.get(g),getSize:g=>A.get(g)?.size??0}},MS=class extends Error{},GS=class extends Error{},vS=class extends Error{},OK=async i=>{let e=await fetch(i);if(!e.ok)throw new MS(`${e.status} ${e.statusText}`,{cause:e});return new File([await e.blob()],new URL(e.url).pathname)},TS=async i=>{typeof i=="string"&&(i=await OK(i));let e;if(i.isDirectory){let t=await qK(i),{EPUB:A}=await Promise.resolve().then(()=>(IE(),Gm));e=await new A(t).init()}else if(i.size)if(await HK(i)){let t=await KK(i);if(YK(i)){let{makeComicBook:A}=await Promise.resolve().then(()=>(lF(),gF));e=A(t,i)}else if(_K(i)){let{makeFB2:A}=await Promise.resolve().then(()=>(Tm(),Um)),{entries:s}=t,n=s.find(r=>r.filename.endsWith(".fb2")),a=await t.loadBlob((n??s[0]).filename);e=await A(a)}else{let{EPUB:A}=await Promise.resolve().then(()=>(IE(),Gm));e=await new A(t).init()}}else if(await JK(i)){let{makePDF:t}=await Promise.resolve().then(()=>($D(),nv));e=await t(i)}else{let{isMOBI:t,MOBI:A}=await Promise.resolve().then(()=>(Cv(),fv));if(await t(i)){let s=await Promise.resolve().then(()=>(bv(),wv));e=await new A({unzlib:s.unzlibSync}).open(i)}else if(PK(i)){let{makeFB2:s}=await Promise.resolve().then(()=>(Tm(),Um));e=await s(i)}}else throw new GS("File not found");if(!e)throw new vS("File type not supported");return e},Jd,Vl,v1,Kn,HS=class HS{constructor(e,t,A={}){D(this,Jd);D(this,Vl);D(this,v1);D(this,Kn);m(this,Vl,e),m(this,v1,t),m(this,Kn,A),h(this,Kn).hidden&&this.hide(),h(this,Vl).addEventListener("mousemove",({screenX:s,screenY:n})=>{s===h(this,Kn).x&&n===h(this,Kn).y||(h(this,Kn).x=s,h(this,Kn).y=n,this.show(),h(this,Jd)&&clearTimeout(h(this,Jd)),t()&&m(this,Jd,setTimeout(this.hide.bind(this),1e3)))},!1)}cloneFor(e){return new HS(e,h(this,v1),h(this,Kn))}hide(){h(this,Vl).style.cursor="none",h(this,Kn).hidden=!0}show(){h(this,Vl).style.removeProperty("cursor"),h(this,Kn).hidden=!1}};Jd=new WeakMap,Vl=new WeakMap,v1=new WeakMap,Kn=new WeakMap;var LS=HS,qn,Fs,US=class extends EventTarget{constructor(){super(...arguments);D(this,qn,[]);D(this,Fs,-1)}pushState(t){let A=h(this,qn)[h(this,Fs)];A===t||A?.fraction&&A.fraction===t.fraction||(h(this,qn)[++hi(this,Fs)._]=t,h(this,qn).length=h(this,Fs)+1,this.dispatchEvent(new Event("index-change")))}replaceState(t){let A=h(this,Fs);h(this,qn)[A]=t}back(){let t=h(this,Fs);if(t<=0)return;let A={state:h(this,qn)[t-1]};m(this,Fs,t-1),this.dispatchEvent(new CustomEvent("popstate",{detail:A})),this.dispatchEvent(new Event("index-change"))}forward(){let t=h(this,Fs);if(t>=h(this,qn).length-1)return;let A={state:h(this,qn)[t+1]};m(this,Fs,t+1),this.dispatchEvent(new CustomEvent("popstate",{detail:A})),this.dispatchEvent(new Event("index-change"))}get canGoBack(){return h(this,Fs)>0}get canGoForward(){return h(this,Fs){if(!i)return{};try{let e=Intl.getCanonicalLocales(i)[0],t=new Intl.Locale(e),A=["zh","ja","kr"].includes(t.language),s=(t.getTextInfo?.()??t.textInfo)?.direction;return{canonical:e,locale:t,isCJK:A,direction:s}}catch(e){return console.warn(e),{}}},Px,qo,Da,bc,Oo,mr,_x,Yt,xr,dL,uL,fL,Yx,CL,BL,EL,G1=class extends HTMLElement{constructor(){super();D(this,Yt);D(this,Px,this.attachShadow({mode:"closed"}));D(this,qo);D(this,Da);D(this,bc);D(this,Oo,new Map);D(this,mr,{type:"outline",options:{}});D(this,_x,new LS(this,()=>this.hasAttribute("autohide-cursor")));pe(this,"isFixedLayout",!1);pe(this,"lastLocation");pe(this,"history",new US);this.history.addEventListener("popstate",({detail:t})=>{let A=this.resolveNavigation(t.state);this.renderer.goTo(A)})}async open(t){if((typeof t=="string"||typeof t.arrayBuffer=="function"||t.isDirectory)&&(t=await TS(t)),this.book=t,this.language=WK(t.metadata?.language),t.splitTOCHref&&t.getTOCFragment){let A=t.sections.map(a=>a.id);m(this,qo,new O1(t.sections,1500,1600));let s=t.splitTOCHref.bind(t),n=t.getTOCFragment.bind(t);m(this,Da,new Vd),await h(this,Da).init({toc:t.toc??[],ids:A,splitHref:s,getFragment:n}),m(this,bc,new Vd),await h(this,bc).init({toc:t.pageList??[],ids:A,splitHref:s,getFragment:n})}if(this.isFixedLayout=this.book.rendition?.layout==="pre-paginated",this.isFixedLayout?(await Promise.resolve().then(()=>(Fv(),kv)),this.renderer=document.createElement("foliate-fxl")):(await Promise.resolve().then(()=>(zv(),Zv)),this.renderer=document.createElement("foliate-paginator")),this.renderer.setAttribute("exportparts","head,foot,filter"),this.renderer.addEventListener("load",A=>x(this,Yt,uL).call(this,A.detail)),this.renderer.addEventListener("relocate",A=>x(this,Yt,dL).call(this,A.detail)),this.renderer.addEventListener("create-overlayer",A=>A.detail.attach(x(this,Yt,CL).call(this,A.detail))),this.renderer.open(t),h(this,Px).append(this.renderer),t.sections.some(A=>A.mediaOverlay)){let A=t.media.activeClass,s=t.media.playbackActiveClass;this.mediaOverlay=t.getMediaOverlay();let n;this.mediaOverlay.addEventListener("highlight",a=>{let r=this.resolveNavigation(a.detail.text);this.renderer.goTo(r).then(()=>{let{doc:o}=this.renderer.getContents().find(g=>g.index=r.index),c=r.anchor(o);c.classList.add(A),s&&c.ownerDocument.documentElement.classList.add(s),n=new WeakRef(c)})}),this.mediaOverlay.addEventListener("unhighlight",()=>{let a=n?.deref();a&&(a.classList.remove(A),s&&a.ownerDocument.documentElement.classList.remove(s))})}}close(){this.renderer?.destroy(),this.renderer?.remove(),m(this,qo,null),m(this,Da,null),m(this,bc,null),m(this,Oo,new Map),this.lastLocation=null,this.history.clear(),this.tts=null,this.mediaOverlay=null}goToTextStart(){return this.goTo(this.book.landmarks?.find(t=>t.type.includes("bodymatter")||t.type.includes("text"))?.href??this.book.sections.findIndex(t=>t.linear!=="no"))}async init({lastLocation:t,showTextStart:A}){let s=t?this.resolveNavigation(t):null;s?(await this.renderer.goTo(s),this.history.pushState(t)):A?await this.goToTextStart():(this.history.pushState(0),await this.next())}async addAnnotation(t,A){let{value:s,indicatorType:n="outline",indicatorOptions:a={}}=t;if(s.startsWith(M1)){let I=s.replace(M1,""),{index:d,anchor:u}=await this.resolveNavigation(I),f=x(this,Yt,Yx).call(this,d);if(f){let{overlayer:C,doc:B}=f;if(A){C.remove(s),C.remove(`${s}::underline`),C.remove(`${s}::tooltip`);return}let E=B?u(B):u,Q;n==="arrow"?Q=jo.arrow:Q=jo.outline,C.add(s,E,Q,a)}return}let r=s.startsWith(hL)?s.replace(hL,""):s,{index:o,anchor:c}=await this.resolveNavigation(r),g=x(this,Yt,Yx).call(this,o);if(g){let{overlayer:I,doc:d}=g;if(I.remove(s),I.remove(`${s}::underline`),I.remove(`${s}::tooltip`),A&&x(this,Yt,xr).call(this,"delete-annotation",{value:s,doc:d}),!A){let u=d?c(d):c,f=(C,B,E)=>{let Q=E?`${s}::${E}`:s;I.add(Q,u,C,B)};x(this,Yt,xr).call(this,"draw-annotation",{draw:f,annotation:t,doc:d,range:u})}}let l=h(this,Da).getProgress(o)?.label??"";return{index:o,label:l}}deleteAnnotation(t){return this.addAnnotation(t,!0)}async showAnnotation(t){let{value:A}=t,s=await this.goTo(A);if(s){let{index:n,anchor:a}=s,{doc:r}=x(this,Yt,Yx).call(this,n),o=a(r);x(this,Yt,xr).call(this,"show-annotation",{value:A,index:n,range:o})}}getCFI(t,A){let s=this.book.sections[t].cfi??Kd.fromIndex(t);return A?Zx(s,$x(A)):s}resolveCFI(t){if(this.book.resolveCFI)return this.book.resolveCFI(t);let A=Vn(t);return{index:Kd.toIndex((A.parent??A).shift()),anchor:a=>_d(a,A)}}resolveNavigation(t){try{if(typeof t=="number")return{index:t};if(typeof t.fraction=="number"){let[A,s]=h(this,qo).getSection(t.fraction);return{index:A,anchor:s}}return jl.test(t)?this.resolveCFI(t):this.book.resolveHref(t)}catch(A){console.error(A),console.error(`Could not resolve target ${t}`)}}async goTo(t){t=decodeURIComponent(t);let A=this.resolveNavigation(t);try{return await this.renderer.goTo(A),this.history.pushState(t),A}catch(s){console.error(s),console.error(`Could not go to ${t}`)}}async goToFraction(t){let[A,s]=h(this,qo).getSection(t);await this.renderer.goTo({index:A,anchor:s}),this.history.pushState({fraction:t})}async select(t){try{let A=await this.resolveNavigation(t);await this.renderer.goTo({...A,select:!0}),this.history.pushState(t)}catch(A){console.error(A),console.error(`Could not go to ${t}`)}}deselect(){for(let{doc:t}of this.renderer.getContents())t.defaultView.getSelection().removeAllRanges()}getSectionFractions(){return(h(this,qo)?.sectionFractions??[]).map(t=>t+Number.EPSILON)}getProgressOf(t,A){let s=h(this,Da)?.getProgress(t,A),n=h(this,bc)?.getProgress(t,A);return{tocItem:s,pageItem:n}}async getTOCItemOf(t){try{let{index:A,anchor:s}=await this.resolveNavigation(t),n=await this.book.sections[A].createDocument(),a=s(n),r=a instanceof Range,o=r?a:n.createRange();return r||o.selectNodeContents(a),h(this,Da).getProgress(A,o)}catch(A){console.error(A),console.error(`Could not get ${t}`)}}async prev(t){await this.renderer.prev(t)}async next(t){await this.renderer.next(t)}goLeft(){return this.book.dir==="rtl"?this.next():this.prev()}goRight(){return this.book.dir==="rtl"?this.prev():this.next()}async*search(t){this.clearSearch();let{searchMatcher:A}=await Promise.resolve().then(()=>(AL(),iL)),{query:s,index:n}=t,a=A(tm,{defaultLocale:this.language,...t}),r=n!=null?x(this,Yt,BL).call(this,a,s,n):x(this,Yt,EL).call(this,a,s),o=[];h(this,Oo).set(n,o);for await(let c of r)if(c.subitems){let g=c.subitems.map(({cfi:l})=>({value:M1+l}));h(this,Oo).set(c.index,g);for(let l of g){let I={...l,indicatorType:h(this,mr).type,indicatorOptions:h(this,mr).options};this.addAnnotation(I)}yield{label:h(this,Da).getProgress(c.index)?.label??"",subitems:c.subitems}}else{if(c.cfi){let g={value:M1+c.cfi};o.push(g);let l={...g,indicatorType:h(this,mr).type,indicatorOptions:h(this,mr).options};this.addAnnotation(l)}yield c}yield"done"}clearSearch(){for(let t of h(this,Oo).values())for(let A of t)this.deleteAnnotation(A);h(this,Oo).clear()}setSearchIndicator(t="outline",A={}){m(this,mr,{type:t,options:A})}async initTTS(t="word",A,s){let n=this.renderer.getContents(),a=this.renderer.primaryIndex,r=n.find(l=>l.index===a)??n[0],o=r?.doc,c=r?.index??0;if(!o)return;if(this.tts&&this.tts.doc===o){A&&(this.tts.highlight=A);return}let{TTS:g}=await Promise.resolve().then(()=>(lL(),gL));this.tts=new g(o,tm,s||null,A||(l=>this.renderer.scrollToAnchor(l,!0)),l=>this.getCFI(c,l),t)}startMediaOverlay(){let{index:t}=this.renderer.getContents()[0];return this.mediaOverlay.start(t)}};Px=new WeakMap,qo=new WeakMap,Da=new WeakMap,bc=new WeakMap,Oo=new WeakMap,mr=new WeakMap,_x=new WeakMap,Yt=new WeakSet,xr=function(t,A,s){return this.dispatchEvent(new CustomEvent(t,{detail:A,cancelable:s}))},dL=function({reason:t,range:A,index:s,fraction:n,size:a}){let r=h(this,qo)?.getProgress(s,n,a)??{},o=h(this,Da)?.getProgress(s,A),c=h(this,bc)?.getProgress(s,A),g=this.getCFI(s,A);this.lastLocation={...r,tocItem:o,pageItem:c,cfi:g,range:A},(t==="snap"||t==="page"||t==="scroll")&&this.history.replaceState(g),x(this,Yt,xr).call(this,"relocate",this.lastLocation)},uL=function({doc:t,index:A}){var s,n;(s=t.documentElement).lang||(s.lang=this.language.canonical??""),this.language.isCJK||(n=t.documentElement).dir||(n.dir=this.language.direction??""),x(this,Yt,fL).call(this,t,A),h(this,_x).cloneFor(t.documentElement),x(this,Yt,xr).call(this,"load",{doc:t,index:A})},fL=function(t,A){let{book:s}=this,n=s.sections[A];t.addEventListener("click",a=>{let r=a.target.closest("a[href]");if(!r)return;a.preventDefault();let o=r.getAttribute("href"),c=n?.resolveHref?.(o)??o;s?.isExternal?.(c)?Promise.resolve(x(this,Yt,xr).call(this,"external-link",{a:r,href:c},!0)).then(g=>g?globalThis.open(c,"_blank"):null).catch(g=>console.error(g)):Promise.resolve(x(this,Yt,xr).call(this,"link",{a:r,href:c},!0)).then(g=>g?this.goTo(c):null).catch(g=>console.error(g))})},Yx=function(t){return this.renderer.getContents().find(A=>A.index===t&&A.overlayer)},CL=function({doc:t,index:A}){let s=new jo;t.addEventListener("click",a=>{let[r,o]=s.hitTest(a);r&&!r.startsWith(M1)&&x(this,Yt,xr).call(this,"show-annotation",{value:r,index:A,range:o})},!1);let n=h(this,Oo).get(A);if(n)for(let a of n){let r={...a,indicatorType:h(this,mr).type,indicatorOptions:h(this,mr).options};this.addAnnotation(r)}return x(this,Yt,xr).call(this,"create-overlay",{index:A}),s},BL=async function*(t,A,s){let n=await this.book.sections[s].createDocument();for(let{range:a,excerpt:r}of t(n,A))yield{cfi:this.getCFI(s,a),excerpt:r}},EL=async function*(t,A){let{sections:s}=this.book;for(let[n,{createDocument:a}]of s.entries()){if(!a)continue;let r=await a(),o=Array.from(t(r,A),({range:g,excerpt:l})=>({cfi:this.getCFI(n,g),excerpt:l}));yield{progress:(n+1)/s.length},o.length&&(yield{index:n,subitems:o})}};customElements.get("foliate-view")||customElements.define("foliate-view",G1);q1();wm();IE();$D();window.makeBook=TS;window.Overlayer=jo;window.CFI=qd;window._zipJs={configure:aE,ZipReader:Au,BlobReader:th,TextWriter:iu,BlobWriter:ih};window._EPUB=nu;window._makePDFFromURL=ZD;window._extractPDFChapters=zD;customElements.get("foliate-view")||customElements.define("foliate-view",G1);window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:"foliate-loaded"}));})();
+ `,C.head.appendChild(B)}a&&r>0&&setTimeout(()=>{u.classList.add("foliate-arrow-fadeout"),setTimeout(()=>{u.parentNode&&u.parentNode.removeChild(u)},1e3)},r)}return c.append(u),c}static copyImage([e],t={}){let{src:A}=t,s=Ii("image"),{left:n,top:a,height:r,width:o}=e;return s.setAttribute("href",A),s.setAttribute("x",n),s.setAttribute("y",a),s.setAttribute("height",r),s.setAttribute("width",o),s}};Dc=new WeakMap,Od=new WeakMap,Vo=new WeakMap,Rs=new WeakMap,Wd=new WeakMap,ka=new WeakMap;var tU=i=>{let e=0,t=A=>{if(A.id=e++,A.subitems)for(let s of A.subitems)t(s)};for(let A of i)t(A);return i},ek=i=>i.flatMap(e=>e.subitems?.length?[e,ek(e.subitems)].flat():e),Vd=class{async init({toc:e,ids:t,splitHref:A,getFragment:s}){tU(e);let n=ek(e),a=new Map;for(let[o,c]of n.entries()){let[g,l]=await A(c?.href)??[],I={fragment:l,item:c};a.has(g)?a.get(g).items.push(I):a.set(g,{prev:n[o-1],items:[I]})}let r=new Map;for(let[o,c]of t.entries())a.has(c)?r.set(c,a.get(c)):r.set(c,r.get(t[o-1]));this.ids=t,this.map=r,this.getFragment=s}getProgress(e,t){if(!this.ids)return;let A=this.ids[e],s=this.map.get(A);if(!s)return null;let{prev:n,items:a}=s;if(!a)return n;if(!t||a.length===1&&!a[0].fragment)return a[0].item;let r=t.startContainer.getRootNode();for(let[o,{fragment:c}]of a.entries()){let g=this.getFragment(r,c);if(g&&t.comparePoint(g,0)>0)return a[o-1]?.item??n}return a[a.length-1].item}},W1,tk,O1=class{constructor(e,t,A){D(this,W1);this.sizes=e.map(s=>s.linear!="no"&&s.size>0?s.size:0),this.sizePerLoc=t,this.sizePerTimeUnit=A,this.sizeTotal=this.sizes.reduce((s,n)=>s+n,0),this.sectionFractions=x(this,W1,tk).call(this)}getProgress(e,t,A=0){let{sizes:s,sizePerLoc:n,sizePerTimeUnit:a,sizeTotal:r}=this,o=s[e]??0,g=s.slice(0,e).reduce((u,f)=>u+f,0)+t*o,l=g+A*o,I=r-g,d=(1-t)*o;return{fraction:l/r,section:{current:e,total:s.length},location:{current:Math.floor(g/n),next:Math.floor(l/n),total:Math.ceil(r/n)},time:{section:d/a,total:I/a}}}getSection(e){if(e<=0)return[0,0];if(e>=1)return[this.sizes.length-1,1];e=e+Number.EPSILON;let{sizeTotal:t}=this,A=this.sectionFractions.findIndex(n=>n>e)-1;if(A<0)return[0,0];for(;!this.sizes[A];)A++;let s=(e-this.sectionFractions[A])/(this.sizes[A]/t);return[A,s]}};W1=new WeakSet,tk=function(){let{sizeTotal:e}=this,t=[0],A=0;for(let s of this.sizes)t.push((A+=s)/e);return t};var iU=(i,e)=>{let t=[];for(let A=e.currentNode;A;A=e.nextNode()){let s=i.comparePoint(A,0);if(s===0)t.push(A);else if(s>0)break}return t},AU=(i,e)=>{let t=[];for(let A=e.nextNode();A;A=e.nextNode())t.push(A);return t},sU=NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT|NodeFilter.SHOW_CDATA_SECTION,nU=i=>{if(i.nodeType===1){let e=i.tagName.toLowerCase();return e==="script"||e==="style"||e==="rt"||e==="rp"?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_SKIP}return NodeFilter.FILTER_ACCEPT},tm=function*(i,e,t){let A=i.commonAncestorContainer??i.body??i,s=document.createTreeWalker(A,sU,{acceptNode:t||nU}),a=(i.commonAncestorContainer?iU:AU)(i,s),r=a.map(c=>c.nodeValue),o=(c,g,l,I)=>{let d=document.createRange();return d.setStart(a[c],g),d.setEnd(a[l],I),d};for(let c of e(r,o))yield c};var M1="foliate-search:",hL="foliate-tts:",HK=async i=>{let e=new Uint8Array(await i.slice(0,4).arrayBuffer());return e[0]===80&&e[1]===75&&e[2]===3&&e[3]===4},JK=async i=>{let e=new Uint8Array(await i.slice(0,5).arrayBuffer());return e[0]===37&&e[1]===80&&e[2]===68&&e[3]===70&&e[4]===45},YK=({name:i,type:e})=>e==="application/vnd.comicbook+zip"||i.endsWith(".cbz"),PK=({name:i,type:e})=>e==="application/x-fictionbook+xml"||i.endsWith(".fb2"),_K=({name:i,type:e})=>e==="application/x-zip-compressed-fb2"||i.endsWith(".fb2.zip")||i.endsWith(".fbz"),KK=async i=>{let{configure:e,ZipReader:t,BlobReader:A,TextWriter:s,BlobWriter:n}=await Promise.resolve().then(()=>(wm(),tF));e({useWebWorkers:!1});let r=await new t(new A(i)).getEntries(),o=new Map(r.map(d=>[d.filename,d])),c=d=>(u,...f)=>o.has(u)?d(o.get(u),...f):null,g=c(d=>d.getData(new s)),l=c((d,u)=>d.getData(new n(u)));return{entries:r,loadText:g,loadBlob:l,getSize:d=>o.get(d)?.uncompressedSize??0}},IL=async i=>i.isFile?i:(await Promise.all(Array.from(await new Promise((e,t)=>i.createReader().readEntries(A=>e(A),A=>t(A))),IL))).flat(),qK=async i=>{let e=await IL(i),t=await Promise.all(e.map(g=>new Promise((l,I)=>g.file(d=>l([d,g.fullPath]),d=>I(d))))),A=new Map(t.map(([g,l])=>[l.replace(`${i.fullPath}/`,""),g])),s=new TextDecoder,n=g=>g?s.decode(g):null,a=g=>A.get(g)?.arrayBuffer()??null;return{loadText:async g=>n(await a(g)),loadBlob:g=>A.get(g),getSize:g=>A.get(g)?.size??0}},MS=class extends Error{},GS=class extends Error{},vS=class extends Error{},OK=async i=>{let e=await fetch(i);if(!e.ok)throw new MS(`${e.status} ${e.statusText}`,{cause:e});return new File([await e.blob()],new URL(e.url).pathname)},TS=async i=>{typeof i=="string"&&(i=await OK(i));let e;if(i.isDirectory){let t=await qK(i),{EPUB:A}=await Promise.resolve().then(()=>(IE(),Gm));e=await new A(t).init()}else if(i.size)if(await HK(i)){let t=await KK(i);if(YK(i)){let{makeComicBook:A}=await Promise.resolve().then(()=>(lF(),gF));e=A(t,i)}else if(_K(i)){let{makeFB2:A}=await Promise.resolve().then(()=>(Tm(),Um)),{entries:s}=t,n=s.find(r=>r.filename.endsWith(".fb2")),a=await t.loadBlob((n??s[0]).filename);e=await A(a)}else{let{EPUB:A}=await Promise.resolve().then(()=>(IE(),Gm));e=await new A(t).init()}}else if(await JK(i)){let{makePDF:t}=await Promise.resolve().then(()=>($D(),nv));e=await t(i)}else{let{isMOBI:t,MOBI:A}=await Promise.resolve().then(()=>(Cv(),fv));if(await t(i)){let s=await Promise.resolve().then(()=>(bv(),wv));e=await new A({unzlib:s.unzlibSync}).open(i)}else if(PK(i)){let{makeFB2:s}=await Promise.resolve().then(()=>(Tm(),Um));e=await s(i)}}else throw new GS("File not found");if(!e)throw new vS("File type not supported");return e},Jd,Vl,v1,Kn,HS=class HS{constructor(e,t,A={}){D(this,Jd);D(this,Vl);D(this,v1);D(this,Kn);m(this,Vl,e),m(this,v1,t),m(this,Kn,A),h(this,Kn).hidden&&this.hide(),h(this,Vl).addEventListener("mousemove",({screenX:s,screenY:n})=>{s===h(this,Kn).x&&n===h(this,Kn).y||(h(this,Kn).x=s,h(this,Kn).y=n,this.show(),h(this,Jd)&&clearTimeout(h(this,Jd)),t()&&m(this,Jd,setTimeout(this.hide.bind(this),1e3)))},!1)}cloneFor(e){return new HS(e,h(this,v1),h(this,Kn))}hide(){h(this,Vl).style.cursor="none",h(this,Kn).hidden=!0}show(){h(this,Vl).style.removeProperty("cursor"),h(this,Kn).hidden=!1}};Jd=new WeakMap,Vl=new WeakMap,v1=new WeakMap,Kn=new WeakMap;var LS=HS,qn,Fs,US=class extends EventTarget{constructor(){super(...arguments);D(this,qn,[]);D(this,Fs,-1)}pushState(t){let A=h(this,qn)[h(this,Fs)];A===t||A?.fraction&&A.fraction===t.fraction||(h(this,qn)[++hi(this,Fs)._]=t,h(this,qn).length=h(this,Fs)+1,this.dispatchEvent(new Event("index-change")))}replaceState(t){let A=h(this,Fs);h(this,qn)[A]=t}back(){let t=h(this,Fs);if(t<=0)return;let A={state:h(this,qn)[t-1]};m(this,Fs,t-1),this.dispatchEvent(new CustomEvent("popstate",{detail:A})),this.dispatchEvent(new Event("index-change"))}forward(){let t=h(this,Fs);if(t>=h(this,qn).length-1)return;let A={state:h(this,qn)[t+1]};m(this,Fs,t+1),this.dispatchEvent(new CustomEvent("popstate",{detail:A})),this.dispatchEvent(new Event("index-change"))}get canGoBack(){return h(this,Fs)>0}get canGoForward(){return h(this,Fs){if(!i)return{};try{let e=Intl.getCanonicalLocales(i)[0],t=new Intl.Locale(e),A=["zh","ja","kr"].includes(t.language),s=(t.getTextInfo?.()??t.textInfo)?.direction;return{canonical:e,locale:t,isCJK:A,direction:s}}catch(e){return console.warn(e),{}}},Px,qo,Da,bc,Oo,mr,_x,Yt,xr,dL,uL,fL,Yx,CL,BL,EL,G1=class extends HTMLElement{constructor(){super();D(this,Yt);D(this,Px,this.attachShadow({mode:"closed"}));D(this,qo);D(this,Da);D(this,bc);D(this,Oo,new Map);D(this,mr,{type:"outline",options:{}});D(this,_x,new LS(this,()=>this.hasAttribute("autohide-cursor")));pe(this,"isFixedLayout",!1);pe(this,"lastLocation");pe(this,"history",new US);this.history.addEventListener("popstate",({detail:t})=>{let A=this.resolveNavigation(t.state);this.renderer.goTo(A)})}async open(t){if((typeof t=="string"||typeof t.arrayBuffer=="function"||t.isDirectory)&&(t=await TS(t)),this.book=t,this.language=WK(t.metadata?.language),t.splitTOCHref&&t.getTOCFragment){let A=t.sections.map(a=>a.id);m(this,qo,new O1(t.sections,1500,1600));let s=t.splitTOCHref.bind(t),n=t.getTOCFragment.bind(t);m(this,Da,new Vd),await h(this,Da).init({toc:t.toc??[],ids:A,splitHref:s,getFragment:n}),m(this,bc,new Vd),await h(this,bc).init({toc:t.pageList??[],ids:A,splitHref:s,getFragment:n})}if(this.isFixedLayout=this.book.rendition?.layout==="pre-paginated",this.isFixedLayout?(await Promise.resolve().then(()=>(Fv(),kv)),this.renderer=document.createElement("foliate-fxl")):(await Promise.resolve().then(()=>(zv(),Zv)),this.renderer=document.createElement("foliate-paginator")),this.renderer.setAttribute("exportparts","head,foot,filter"),this.renderer.addEventListener("load",A=>x(this,Yt,uL).call(this,A.detail)),this.renderer.addEventListener("relocate",A=>x(this,Yt,dL).call(this,A.detail)),this.renderer.addEventListener("create-overlayer",A=>A.detail.attach(x(this,Yt,CL).call(this,A.detail))),this.renderer.open(t),h(this,Px).append(this.renderer),t.sections.some(A=>A.mediaOverlay)){let A=t.media.activeClass,s=t.media.playbackActiveClass;this.mediaOverlay=t.getMediaOverlay();let n;this.mediaOverlay.addEventListener("highlight",a=>{let r=this.resolveNavigation(a.detail.text);this.renderer.goTo(r).then(()=>{let{doc:o}=this.renderer.getContents().find(g=>g.index=r.index),c=r.anchor(o);c.classList.add(A),s&&c.ownerDocument.documentElement.classList.add(s),n=new WeakRef(c)})}),this.mediaOverlay.addEventListener("unhighlight",()=>{let a=n?.deref();a&&(a.classList.remove(A),s&&a.ownerDocument.documentElement.classList.remove(s))})}}close(){this.renderer?.destroy(),this.renderer?.remove(),m(this,qo,null),m(this,Da,null),m(this,bc,null),m(this,Oo,new Map),this.lastLocation=null,this.history.clear(),this.tts=null,this.mediaOverlay=null}goToTextStart(){return this.goTo(this.book.landmarks?.find(t=>t.type.includes("bodymatter")||t.type.includes("text"))?.href??this.book.sections.findIndex(t=>t.linear!=="no"))}async init({lastLocation:t,showTextStart:A}){let s=t?this.resolveNavigation(t):null;s?(await this.renderer.goTo(s),this.history.pushState(t)):A?await this.goToTextStart():(this.history.pushState(0),await this.next())}async addAnnotation(t,A){let{value:s,indicatorType:n="outline",indicatorOptions:a={}}=t;if(s.startsWith(M1)){let I=s.replace(M1,""),{index:d,anchor:u}=await this.resolveNavigation(I),f=x(this,Yt,Yx).call(this,d);if(f){let{overlayer:C,doc:B}=f;if(A){C.remove(s),C.remove(`${s}::underline`),C.remove(`${s}::tooltip`);return}let E=B?u(B):u,Q;n==="arrow"?Q=jo.arrow:Q=jo.outline,C.add(s,E,Q,a)}return}let r=s.startsWith(hL)?s.replace(hL,""):s,{index:o,anchor:c}=await this.resolveNavigation(r),g=x(this,Yt,Yx).call(this,o);if(g){let{overlayer:I,doc:d}=g;if(I.remove(s),I.remove(`${s}::underline`),I.remove(`${s}::tooltip`),A&&x(this,Yt,xr).call(this,"delete-annotation",{value:s,doc:d}),!A){let u=d?c(d):c,f=(C,B,E)=>{let Q=E?`${s}::${E}`:s;I.add(Q,u,C,B)};x(this,Yt,xr).call(this,"draw-annotation",{draw:f,annotation:t,doc:d,range:u})}}let l=h(this,Da).getProgress(o)?.label??"";return{index:o,label:l}}deleteAnnotation(t){return this.addAnnotation(t,!0)}async showAnnotation(t){let{value:A}=t,s=await this.goTo(A);if(s){let{index:n,anchor:a}=s,{doc:r}=x(this,Yt,Yx).call(this,n),o=a(r);x(this,Yt,xr).call(this,"show-annotation",{value:A,index:n,range:o})}}getCFI(t,A){let s=this.book.sections[t].cfi??Kd.fromIndex(t);return A?Zx(s,$x(A)):s}resolveCFI(t){if(this.book.resolveCFI)return this.book.resolveCFI(t);let A=Vn(t);return{index:Kd.toIndex((A.parent??A).shift()),anchor:a=>_d(a,A)}}resolveNavigation(t){try{if(typeof t=="number")return{index:t};if(typeof t.fraction=="number"){let[A,s]=h(this,qo).getSection(t.fraction);return{index:A,anchor:s}}return jl.test(t)?this.resolveCFI(t):this.book.resolveHref(t)}catch(A){console.error(A),console.error(`Could not resolve target ${t}`)}}async goTo(t){t=decodeURIComponent(t);let A=this.resolveNavigation(t);try{return await this.renderer.goTo(A),this.history.pushState(t),A}catch(s){console.error(s),console.error(`Could not go to ${t}`)}}async goToFraction(t){let[A,s]=h(this,qo).getSection(t);await this.renderer.goTo({index:A,anchor:s}),this.history.pushState({fraction:t})}async select(t){try{let A=await this.resolveNavigation(t);await this.renderer.goTo({...A,select:!0}),this.history.pushState(t)}catch(A){console.error(A),console.error(`Could not go to ${t}`)}}deselect(){for(let{doc:t}of this.renderer.getContents())t.defaultView.getSelection().removeAllRanges()}getSectionFractions(){return(h(this,qo)?.sectionFractions??[]).map(t=>t+Number.EPSILON)}getProgressOf(t,A){let s=h(this,Da)?.getProgress(t,A),n=h(this,bc)?.getProgress(t,A);return{tocItem:s,pageItem:n}}async getTOCItemOf(t){try{let{index:A,anchor:s}=await this.resolveNavigation(t),n=await this.book.sections[A].createDocument(),a=s(n),r=a instanceof Range,o=r?a:n.createRange();return r||o.selectNodeContents(a),h(this,Da).getProgress(A,o)}catch(A){console.error(A),console.error(`Could not get ${t}`)}}async prev(t){await this.renderer.prev(t)}async next(t){await this.renderer.next(t)}goLeft(){return this.book.dir==="rtl"?this.next():this.prev()}goRight(){return this.book.dir==="rtl"?this.prev():this.next()}async*search(t){this.clearSearch();let{searchMatcher:A}=await Promise.resolve().then(()=>(AL(),iL)),{query:s,index:n}=t,a=A(tm,{defaultLocale:this.language,...t}),r=n!=null?x(this,Yt,BL).call(this,a,s,n):x(this,Yt,EL).call(this,a,s),o=[];h(this,Oo).set(n,o);for await(let c of r)if(c.subitems){let g=c.subitems.map(({cfi:l})=>({value:M1+l}));h(this,Oo).set(c.index,g);for(let l of g){let I={...l,indicatorType:h(this,mr).type,indicatorOptions:h(this,mr).options};this.addAnnotation(I)}yield{label:h(this,Da).getProgress(c.index)?.label??"",subitems:c.subitems}}else{if(c.cfi){let g={value:M1+c.cfi};o.push(g);let l={...g,indicatorType:h(this,mr).type,indicatorOptions:h(this,mr).options};this.addAnnotation(l)}yield c}yield"done"}clearSearch(){for(let t of h(this,Oo).values())for(let A of t)this.deleteAnnotation(A);h(this,Oo).clear()}setSearchIndicator(t="outline",A={}){m(this,mr,{type:t,options:A})}async initTTS(t="word",A,s){let n=this.renderer.getContents(),a=this.renderer.primaryIndex,r=n.find(l=>l.index===a)??n[0],o=r?.doc,c=r?.index??0;if(!o)return;if(this.tts&&this.tts.doc===o){A&&(this.tts.highlight=A);return}let{TTS:g}=await Promise.resolve().then(()=>(lL(),gL));this.tts=new g(o,tm,s||null,A||(l=>this.renderer.scrollToAnchor(l,!0)),l=>this.getCFI(c,l),t)}startMediaOverlay(){let{index:t}=this.renderer.getContents()[0];return this.mediaOverlay.start(t)}};Px=new WeakMap,qo=new WeakMap,Da=new WeakMap,bc=new WeakMap,Oo=new WeakMap,mr=new WeakMap,_x=new WeakMap,Yt=new WeakSet,xr=function(t,A,s){return this.dispatchEvent(new CustomEvent(t,{detail:A,cancelable:s}))},dL=function({reason:t,range:A,index:s,fraction:n,size:a}){let r=h(this,qo)?.getProgress(s,n,a)??{},o=h(this,Da)?.getProgress(s,A),c=h(this,bc)?.getProgress(s,A),g=this.getCFI(s,A);this.lastLocation={...r,tocItem:o,pageItem:c,cfi:g,range:A},(t==="snap"||t==="page"||t==="scroll")&&this.history.replaceState(g),x(this,Yt,xr).call(this,"relocate",this.lastLocation)},uL=function({doc:t,index:A}){var s,n;(s=t.documentElement).lang||(s.lang=this.language.canonical??""),this.language.isCJK||(n=t.documentElement).dir||(n.dir=this.language.direction??""),x(this,Yt,fL).call(this,t,A),h(this,_x).cloneFor(t.documentElement),x(this,Yt,xr).call(this,"load",{doc:t,index:A})},fL=function(t,A){let{book:s}=this,n=s.sections[A];t.addEventListener("click",a=>{let r=a.target.closest("a[href]");if(!r)return;a.preventDefault();let o=r.getAttribute("href"),c=n?.resolveHref?.(o)??o;s?.isExternal?.(c)?Promise.resolve(x(this,Yt,xr).call(this,"external-link",{a:r,href:c},!0)).then(g=>g?globalThis.open(c,"_blank"):null).catch(g=>console.error(g)):Promise.resolve(x(this,Yt,xr).call(this,"link",{a:r,href:c},!0)).then(g=>g?this.goTo(c):null).catch(g=>console.error(g))})},Yx=function(t){return this.renderer.getContents().find(A=>A.index===t&&A.overlayer)},CL=function({doc:t,index:A}){let s=new jo;t.addEventListener("click",a=>{let[r,o]=s.hitTest(a);r&&!r.startsWith(M1)&&x(this,Yt,xr).call(this,"show-annotation",{value:r,index:A,range:o})},!1);let n=h(this,Oo).get(A);if(n)for(let a of n){let r={...a,indicatorType:h(this,mr).type,indicatorOptions:h(this,mr).options};this.addAnnotation(r)}return x(this,Yt,xr).call(this,"create-overlay",{index:A}),s},BL=async function*(t,A,s){let n=await this.book.sections[s].createDocument();for(let{range:a,excerpt:r}of t(n,A))yield{cfi:this.getCFI(s,a),excerpt:r}},EL=async function*(t,A){let{sections:s}=this.book;for(let[n,{createDocument:a}]of s.entries()){if(!a)continue;let r=await a(),o=Array.from(t(r,A),({range:g,excerpt:l})=>({cfi:this.getCFI(n,g),excerpt:l}));yield{progress:(n+1)/s.length},o.length&&(yield{index:n,subitems:o})}};customElements.get("foliate-view")||customElements.define("foliate-view",G1);q1();wm();IE();$D();(async()=>{let i=null;try{let e=navigator.userAgentData;if(e&&typeof e.getHighEntropyValues=="function"){let{fullVersionList:t}=await e.getHighEntropyValues(["fullVersionList"]),A=(t||[]).find(s=>/Android WebView|Microsoft Edge/i.test(s.brand));A&&(i=A.version)}}catch{}window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:"readany-ua",ua:navigator.userAgent,fullVersion:i}))})();window.makeBook=TS;window.Overlayer=jo;window.CFI=qd;window._zipJs={configure:aE,ZipReader:Au,BlobReader:th,TextWriter:iu,BlobWriter:ih};window._EPUB=nu;window._makePDFFromURL=ZD;window._extractPDFChapters=zD;customElements.get("foliate-view")||customElements.define("foliate-view",G1);window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:"foliate-loaded"}));})();
From 631c59aa202267276f4018be346a54a6860373b5 Mon Sep 17 00:00:00 2001
From: k6G52m4Dz75W <74605402+k6G52m4Dz75W@users.noreply.github.com>
Date: Sat, 19 Sep 2026 00:36:49 +0800
Subject: [PATCH 18/20] feat(desktop): query the real WebView build from the
Tauri runtime
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Mirror readest#6275: Chromium's UA-Reduction freezes the User-Agent to
a stub on Windows WebView2, and the UA carries frozen fallback tokens
for the WebKit engines — so replace the whole User-Agent Client Hints
mechanism (brand table + getHighEntropyValues round-trip) with a single
get_webview_version command backed by tauri::webview_version(), which
returns the real build on every desktop platform (WebView2 via
GetAvailableCoreWebView2BrowserVersionString, WebKit via the framework
bundle, WebKitGTK via webkit_get_*_version).
The engine label stays with the UA parse (the runtime query has no
brand); the label falls back to the UA-derived version when the command
is unavailable — plain vite dev in a browser or a failed invoke.
Side effect beyond the simplification: the macOS and Linux labels stop
degrading to an engine-only string — the runtime query fills in the
framework build the UA could never provide. The mobile probe pipeline
(RN has no runtime-query equivalent) is untouched.
---
packages/app/src-tauri/src/lib.rs | 31 +++++++++++++
packages/app/src/lib/webview-info.ts | 69 +++++++++++-----------------
2 files changed, 57 insertions(+), 43 deletions(-)
diff --git a/packages/app/src-tauri/src/lib.rs b/packages/app/src-tauri/src/lib.rs
index a782cb4bd..acf9caa07 100644
--- a/packages/app/src-tauri/src/lib.rs
+++ b/packages/app/src-tauri/src/lib.rs
@@ -8,6 +8,36 @@ use std::sync::Mutex;
use tauri::Manager;
use vector::VectorDBState;
+#[derive(serde::Serialize)]
+struct WebViewInfo {
+ engine: String,
+ version: String,
+}
+
+/// The WebView runtime's real build number for Settings → About and the
+/// feedback device info. The User-Agent is reduced to a stub on Windows
+/// WebView2 (UA Reduction) and carries frozen fallback tokens for the WebKit
+/// engines, so the runtime's own query is the only reliable source on every
+/// desktop platform. The engine label stays with the frontend's UA parse —
+/// the runtime query has no brand.
+#[tauri::command]
+fn get_webview_version() -> Option {
+ let engine = match std::env::consts::OS {
+ "windows" => "WebView2",
+ "macos" => "WebKit",
+ "linux" => "WebKitGTK",
+ _ => return None,
+ };
+ let version = tauri::webview_version().ok()?.trim().to_string();
+ if version.is_empty() {
+ return None;
+ }
+ Some(WebViewInfo {
+ engine: engine.to_string(),
+ version,
+ })
+}
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -46,6 +76,7 @@ pub fn run() {
vector::vector_reinit,
vector::vector_shutdown,
readany_cli::readany_cli_run,
+ get_webview_version,
])
.setup(|app| {
let app_handle = app.handle().clone();
diff --git a/packages/app/src/lib/webview-info.ts b/packages/app/src/lib/webview-info.ts
index 88641271e..af86f6a9e 100644
--- a/packages/app/src/lib/webview-info.ts
+++ b/packages/app/src/lib/webview-info.ts
@@ -2,13 +2,21 @@
* Web-engine detection for the desktop app (Settings → About). The parsing
* itself lives in core (packages/core/src/utils/webview-info.ts) so the mobile
* app can parse the reader WebView's UA with identical results; this wrapper
- * only adds the Tauri-runtime check and the Chromium Client Hints lookup.
+ * adds the Tauri-runtime check and the runtime version query.
+ *
+ * The VERSION comes from the Tauri runtime (`tauri::webview_version()`):
+ * the User-Agent is reduced to a stub on Windows WebView2 (UA Reduction,
+ * e.g. Edg/152.0.0.0 on a 152.0.4191.62 runtime) and carries frozen fallback
+ * tokens for the WebKit engines, while the runtime query returns the real
+ * build on every desktop platform. Only the ENGINE label stays with the UA
+ * parse — the runtime query has no brand.
*
* Version floors differ per engine (e.g. :has() needs WebView2 ≥ 105 /
* WebKitGTK ≥ 2.36), which is exactly why the exact build matters. Detection
* is display-only diagnostics, not a security boundary.
*/
+import { invoke } from "@tauri-apps/api/core";
import { formatWebviewInfo, parseWebviewInfo } from "@readany/core/utils/webview-info";
import type { WebviewInfo } from "@readany/core/utils/webview-info";
@@ -22,50 +30,25 @@ export function getWebviewInfo(ua: string = navigator.userAgent): WebviewInfo {
}
/**
- * Chromium's UA Reduction freezes the minor/build/patch numbers in the UA
- * string (Edg/152.0.0.0 on a 152.0.4191.62 runtime), so the UA-parsed version
- * is incomplete on WebView2/Chrome/Android WebView. The real build is only in
- * the User-Agent Client Hints `fullVersionList` (high-entropy). WebView2
- * reports a distinct brand of its own — match loosely (the mobile probe in
- * app-expo/src/components/common/UAProbe.tsx keeps the same list in sync).
- */
-const CLIENT_HINT_BRANDS: Record = {
- WebView2: /Microsoft Edge/i,
- Edge: /Microsoft Edge/i,
- "Android WebView": /Android WebView/i,
- Chrome: /Google Chrome/i,
-};
-
-async function getFullVersionFromClientHints(engine: string): Promise {
- const brandPattern = CLIENT_HINT_BRANDS[engine];
- if (!brandPattern) return null;
- try {
- const uaData = (
- navigator as unknown as {
- userAgentData?: {
- getHighEntropyValues?: (
- hints: string[],
- ) => Promise<{ fullVersionList?: { brand: string; version: string }[] }>;
- };
- }
- ).userAgentData;
- const getHighEntropyValues = uaData?.getHighEntropyValues;
- if (typeof getHighEntropyValues !== "function") return null;
- const { fullVersionList } = await getHighEntropyValues.call(uaData, ["fullVersionList"]);
- return fullVersionList?.find((entry) => brandPattern.test(entry.brand))?.version ?? null;
- } catch {
- return null;
- }
-}
-
-/**
- * Display label for Settings → About, async because the full version needs a
- * round-trip through the Client Hints API on Chromium engines. Falls back to
- * the UA-parsed (reduced) version when Client Hints are unavailable.
+ * Display label for Settings → About, async because the real build number
+ * needs a round-trip to the Rust runtime. Falls back to the UA-parsed version
+ * (reduced on WebView2) when the query is unavailable — plain `vite` dev in a
+ * browser, or a failed command.
*/
export async function getWebviewLabel(): Promise {
const { engine, version } = getWebviewInfo();
if (!engine) return "";
- const fullVersion = (await getFullVersionFromClientHints(engine)) || version;
- return formatWebviewInfo({ engine, version: fullVersion });
+ if (isTauriRuntime()) {
+ try {
+ const native = await invoke<{ engine: string; version: string } | null>(
+ "get_webview_version",
+ );
+ if (native?.version) {
+ return formatWebviewInfo({ engine, version: native.version });
+ }
+ } catch (error) {
+ console.warn("[webview-info] get_webview_version failed, falling back to UA:", error);
+ }
+ }
+ return formatWebviewInfo({ engine, version });
}
From 4f929c649f6ae8b2280aa424abfecb2a8114a4ac Mon Sep 17 00:00:00 2001
From: k6G52m4Dz75W <74605402+k6G52m4Dz75W@users.noreply.github.com>
Date: Sat, 19 Sep 2026 10:26:56 +0800
Subject: [PATCH 19/20] =?UTF-8?q?fix(desktop):=20OCR=20follow-ups=20?=
=?UTF-8?q?=E2=80=94=20diagnosable=20fallbacks,=20single=20engine=20author?=
=?UTF-8?q?ity?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Rust: log webview_version() failures and empty results before
returning None — a resolved null at the IPC boundary was otherwise
indistinguishable from an unsupported platform, silently degrading
the label to the UA-reduced version (the frontend only logs on
invoke rejection).
- Frontend: prefer the runtime's engine label over the UA parse so the
two sources can never disagree on the same machine; reuse the shared
WebviewInfo type for the invoke result instead of a diverging
structural copy.
---
packages/app/src-tauri/src/lib.rs | 13 ++++++++++++-
packages/app/src/lib/webview-info.ts | 10 +++++-----
2 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/packages/app/src-tauri/src/lib.rs b/packages/app/src-tauri/src/lib.rs
index acf9caa07..24c36b60f 100644
--- a/packages/app/src-tauri/src/lib.rs
+++ b/packages/app/src-tauri/src/lib.rs
@@ -28,8 +28,19 @@ fn get_webview_version() -> Option {
"linux" => "WebKitGTK",
_ => return None,
};
- let version = tauri::webview_version().ok()?.trim().to_string();
+ let version = match tauri::webview_version() {
+ Ok(v) => v.trim().to_string(),
+ // A genuine desktop query failure must stay distinguishable from an
+ // unsupported platform: the frontend only logs on invoke rejection,
+ // so a resolved None with no trace would silently degrade to the
+ // UA-reduced version.
+ Err(e) => {
+ eprintln!("[webview-info] webview_version() failed: {e}");
+ return None;
+ }
+ };
if version.is_empty() {
+ eprintln!("[webview-info] webview_version() returned an empty string");
return None;
}
Some(WebViewInfo {
diff --git a/packages/app/src/lib/webview-info.ts b/packages/app/src/lib/webview-info.ts
index af86f6a9e..fcc23e6d8 100644
--- a/packages/app/src/lib/webview-info.ts
+++ b/packages/app/src/lib/webview-info.ts
@@ -40,11 +40,11 @@ export async function getWebviewLabel(): Promise {
if (!engine) return "";
if (isTauriRuntime()) {
try {
- const native = await invoke<{ engine: string; version: string } | null>(
- "get_webview_version",
- );
- if (native?.version) {
- return formatWebviewInfo({ engine, version: native.version });
+ const native = await invoke("get_webview_version");
+ // The runtime's OS→engine mapping is the desktop authority — prefer it
+ // over the UA parse so the two can never disagree on the same machine.
+ if (native?.engine && native.version) {
+ return formatWebviewInfo({ engine: native.engine, version: native.version });
}
} catch (error) {
console.warn("[webview-info] get_webview_version failed, falling back to UA:", error);
From ca8b032ce0ffed95bfc0d7f244c2b6e8360ad509 Mon Sep 17 00:00:00 2001
From: k6G52m4Dz75W <74605402+k6G52m4Dz75W@users.noreply.github.com>
Date: Sat, 19 Sep 2026 10:31:40 +0800
Subject: [PATCH 20/20] fix(desktop): let the runtime query answer even when
the UA parse is empty
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Round-2 OCR: the 'if (!engine) return ""' guard ran BEFORE the native
attempt, so an unknown/unsupported UA still short-circuited to an empty
label — exactly the UA-owns-the-engine behavior the previous commit
documented abandoning. Reorder to try the runtime query first; the UA
parse now serves only the fallback path (vite dev in a browser, failed
invoke), where formatWebviewInfo already collapses a missing engine to
an empty string.
Also align the module header, the getWebviewLabel JSDoc, and the Rust
command doc with the new contract (runtime is the desktop authority
for BOTH engine and version) — they still carried the stale 'the
engine label stays with the UA parse' claim.
---
packages/app/src-tauri/src/lib.rs | 7 +++----
packages/app/src/lib/webview-info.ts | 24 +++++++++++++-----------
2 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/packages/app/src-tauri/src/lib.rs b/packages/app/src-tauri/src/lib.rs
index 24c36b60f..77a458448 100644
--- a/packages/app/src-tauri/src/lib.rs
+++ b/packages/app/src-tauri/src/lib.rs
@@ -14,12 +14,11 @@ struct WebViewInfo {
version: String,
}
-/// The WebView runtime's real build number for Settings → About and the
+/// The WebView engine label + real build number for Settings → About and the
/// feedback device info. The User-Agent is reduced to a stub on Windows
/// WebView2 (UA Reduction) and carries frozen fallback tokens for the WebKit
-/// engines, so the runtime's own query is the only reliable source on every
-/// desktop platform. The engine label stays with the frontend's UA parse —
-/// the runtime query has no brand.
+/// engines, so this runtime query is the desktop authority for BOTH fields;
+/// the frontend falls back to its UA parse when this returns None.
#[tauri::command]
fn get_webview_version() -> Option {
let engine = match std::env::consts::OS {
diff --git a/packages/app/src/lib/webview-info.ts b/packages/app/src/lib/webview-info.ts
index fcc23e6d8..5542f3a67 100644
--- a/packages/app/src/lib/webview-info.ts
+++ b/packages/app/src/lib/webview-info.ts
@@ -4,12 +4,13 @@
* app can parse the reader WebView's UA with identical results; this wrapper
* adds the Tauri-runtime check and the runtime version query.
*
- * The VERSION comes from the Tauri runtime (`tauri::webview_version()`):
- * the User-Agent is reduced to a stub on Windows WebView2 (UA Reduction,
- * e.g. Edg/152.0.0.0 on a 152.0.4191.62 runtime) and carries frozen fallback
- * tokens for the WebKit engines, while the runtime query returns the real
- * build on every desktop platform. Only the ENGINE label stays with the UA
- * parse — the runtime query has no brand.
+ * Inside the Tauri runtime, BOTH the engine label and the version come from
+ * the runtime query (`tauri::webview_version()` + an OS→engine mapping):
+ * the User-Agent is reduced to a stub on Windows WebView2 (UA Reduction) and
+ * carries frozen fallback tokens for the WebKit engines, so the runtime is
+ * the only reliable source on every desktop platform. The UA parse is the
+ * fallback path — plain `vite` dev in a browser or a failed command — where
+ * the version is reduced on WebView2 and frozen on WebKit.
*
* Version floors differ per engine (e.g. :has() needs WebView2 ≥ 105 /
* WebKitGTK ≥ 2.36), which is exactly why the exact build matters. Detection
@@ -31,13 +32,13 @@ export function getWebviewInfo(ua: string = navigator.userAgent): WebviewInfo {
/**
* Display label for Settings → About, async because the real build number
- * needs a round-trip to the Rust runtime. Falls back to the UA-parsed version
- * (reduced on WebView2) when the query is unavailable — plain `vite` dev in a
- * browser, or a failed command.
+ * needs a round-trip to the Rust runtime. Inside the Tauri runtime the
+ * command's OS→engine mapping + runtime build is the authority (it answers
+ * even when the UA parse comes up empty); the UA parse is the fallback when
+ * the command is unavailable — plain `vite` dev in a browser, or a failed
+ * invoke — with the version reduced on WebView2 in that path.
*/
export async function getWebviewLabel(): Promise {
- const { engine, version } = getWebviewInfo();
- if (!engine) return "";
if (isTauriRuntime()) {
try {
const native = await invoke("get_webview_version");
@@ -50,5 +51,6 @@ export async function getWebviewLabel(): Promise {
console.warn("[webview-info] get_webview_version failed, falling back to UA:", error);
}
}
+ const { engine, version } = getWebviewInfo();
return formatWebviewInfo({ engine, version });
}