")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 246, Col: 95}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 257, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -229,7 +229,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 285, Col: 113}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 296, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -239,7 +239,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 287, Col: 52}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 298, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -250,7 +250,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 290, Col: 72}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 301, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -265,7 +265,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 367, Col: 51}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 378, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -275,7 +275,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 369, Col: 72}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 380, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -318,7 +318,7 @@ func drawerHeader(title string) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 382, Col: 35}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 393, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -423,7 +423,7 @@ func ReaderSearchDrawer() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Searching… 0 matches No matches
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
Searching… 0 matches No matches Search failed
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts
index d15f04b..8e97401 100644
--- a/web/src/reader/reader.ts
+++ b/web/src/reader/reader.ts
@@ -414,9 +414,11 @@ document.addEventListener("alpine:init", () => {
}[],
searchProgress: 0,
searching: false,
+ searchError: "",
searchGen: 0,
pdfPagesCache: null as PdfPageText[] | null,
pdfSearchKeys: [] as string[],
+ backStack: [] as { cfi?: string; page?: number }[],
selectionPopover: {
open: false,
mode: "create" as "create" | "edit",
@@ -748,6 +750,10 @@ document.addEventListener("alpine:init", () => {
}
});
// ----- highlight rendering (foliate overlayer pipeline) -----
+ // Internal link clicks (footnotes, cross-references): record where we
+ // came from so the back-to-location stack can return; foliate then
+ // navigates on its own.
+ this.view.addEventListener("link", () => this.pushBackStack());
this.view.addEventListener("draw-annotation", (e: any) => {
const { draw, annotation } = e.detail;
draw(Overlayer.highlight, { color: annotation.color || "#ffd54f" });
@@ -1261,9 +1267,11 @@ document.addEventListener("alpine:init", () => {
}) {
if (hl.pdfPage >= 0) {
// Fixed-layout: a bare number navigates to the section (page) index.
+ this.pushBackStack();
this.view?.goTo?.(hl.pdfPage);
this.closeDrawers();
} else if (hl.cfi) {
+ this.pushBackStack();
this.view?.showAnnotation({ value: hl.cfi })?.catch?.(() => {});
this.closeDrawers();
}
@@ -1485,14 +1493,17 @@ document.addEventListener("alpine:init", () => {
this.searching = true;
this.searchProgress = 0;
this.searchGroups = [];
+ this.searchError = "";
try {
if (this.isFixedLayout && this.isPDF) {
await this.runPdfSearch(q, gen);
} else {
await this.runEpubSearch(q, gen);
}
- } catch (_e) {
- /* search errors leave partial results */
+ } catch (e) {
+ // Swallowing this silently made a wiring bug look like "no matches".
+ console.warn("search failed:", e);
+ this.searchError = "Search failed (see browser console)";
}
if (gen === this.searchGen) this.searching = false;
},
@@ -1521,8 +1532,19 @@ document.addEventListener("alpine:init", () => {
}
},
async runPdfSearch(q: string, gen: number) {
- const pdf = this.book?.pdf;
- if (!pdf) return;
+ // CRITICAL: unwrap Alpine's reactive proxy. `this.book` is a plain
+ // object, so reactivity wraps it — and reading `.pdf` through the
+ // proxy wraps the PDFDocumentProxy too. pdf.js uses #private fields,
+ // so calling getPage() on the proxy throws "cannot read private
+ // member", which used to be swallowed as an empty result set.
+ const rawBook = (Alpine as any).raw
+ ? (Alpine as any).raw(this.book)
+ : this.book;
+ const pdf = rawBook?.pdf;
+ if (!pdf) {
+ this.searchError = "PDF text engine unavailable";
+ return;
+ }
if (!this.pdfPagesCache) {
// Extraction reports progress; it's cached so re-searches are instant.
this.pdfPagesCache = await extractPdfPages(pdf, (fraction) => {
@@ -1566,6 +1588,7 @@ document.addEventListener("alpine:init", () => {
clearSearchResults() {
this.searchGen++;
this.searching = false;
+ this.searchError = "";
this.searchGroups = [];
this.searchProgress = 0;
for (const key of this.pdfSearchKeys) {
@@ -1575,11 +1598,41 @@ document.addEventListener("alpine:init", () => {
this.view?.clearSearch?.();
},
goToSearchResult(item: { cfi?: string; page?: number | null }) {
- if (item.cfi) this.view?.goTo?.(item.cfi);
- else if (item.page != null) this.view?.goTo?.(item.page);
- else return;
+ if (item.cfi) {
+ this.pushBackStack();
+ this.view?.goTo?.(item.cfi);
+ } else if (item.page != null) {
+ this.pushBackStack();
+ this.view?.goTo?.(item.page);
+ } else return;
this.closeDrawers();
},
+ // ----- back-to-location stack -----
+ // Research pattern: jump somewhere (search hit, footnote link, TOC
+ // entry), read, then return. Recorded before every programmatic jump
+ // and on every internal link click (footnotes). Ordinary paging never
+ // touches the stack.
+ pushBackStack() {
+ let loc: { cfi?: string; page?: number } | null = null;
+ if (this.isFixedLayout) {
+ const page = this.renderer?.index;
+ if (typeof page === "number" && page >= 0) loc = { page };
+ } else {
+ const cfi = this.view?.lastLocation?.cfi;
+ if (cfi) loc = { cfi };
+ }
+ if (!loc) return;
+ const top = this.backStack[this.backStack.length - 1];
+ if (top && top.cfi === loc.cfi && top.page === loc.page) return;
+ this.backStack.push(loc);
+ if (this.backStack.length > 50) this.backStack.shift();
+ },
+ goBackToLocation() {
+ const loc = this.backStack.pop();
+ if (!loc) return;
+ if (loc.cfi) this.view?.goTo?.(loc.cfi);
+ else if (typeof loc.page === "number") this.view?.goTo?.(loc.page);
+ },
flattenTOC(items: any[], depth = 0): any[] {
const result: any[] = [];
for (const item of items) {
@@ -1592,6 +1645,7 @@ document.addEventListener("alpine:init", () => {
},
goToTOCItem(item: any) {
if (this.view && item.href) {
+ this.pushBackStack();
this.view.goTo(item.href);
this.tocOpen = false;
}
@@ -1599,8 +1653,10 @@ document.addEventListener("alpine:init", () => {
goToBookmark(item: { cfi: string; page: number | null }) {
if (!this.view) return;
if (item.cfi) {
+ this.pushBackStack();
this.view.goTo(item.cfi);
} else if (item.page != null && item.page > 0) {
+ this.pushBackStack();
// Fixed-layout/comic: sections are pages; foliate takes an index.
this.view.goTo(item.page - 1);
} else {
@@ -2049,8 +2105,12 @@ document.addEventListener("alpine:init", () => {
const typing =
tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA";
this.pokeChrome();
- if (k === "ArrowLeft" || k === "h") this.goLeft();
- else if (k === "ArrowRight" || k === "l") this.goRight();
+ if (k === "ArrowLeft" || k === "h") {
+ if (event.altKey) {
+ event.preventDefault();
+ this.goBackToLocation();
+ } else this.goLeft();
+ } else if (k === "ArrowRight" || k === "l") this.goRight();
else if (k === "+" || k === "=") this.zoomIn();
else if (k === "-" || k === "_") this.zoomOut();
else if (k === "0") this.resetZoom();