feat(reader): wire up TOC, bookmarks, and navigator panels with Alpine.js bindings

Replace dead data-action attributes with Alpine.js @click handlers and
x-ref references across all reader panels:

- TOC panel: replaced static <nav> with x-for loop over tocItems array,
  added goToTOCItem() click handler, window-shade toggle via .tocPanel
- Bookmarks panel: replaced data-action with @click.prevent handlers,
  added goToBookmarkTarget() using data-cfi attributes for navigation,
  window-shade toggle via .bookmarksPanel
- Navigator panel: replaced data-action with @click window-shade toggle
  via .navigatorPanel
- Added goToBookmarkTarget() and toggleWindowShade() methods to reader.ts
- Removed unused panel-lock buttons (lock feature not yet implemented)
- Regenerated reader_templ.go, rebuilt CSS and JS bundles
This commit is contained in:
2026-04-19 18:17:42 -04:00
parent 47e0e96ab8
commit 0589157b57
5 changed files with 224 additions and 95 deletions
+99 -3
View File
@@ -1,6 +1,6 @@
import "foliate-js/view.js";
import { Alpine } from "../alpine";
import { loadSettings } from "./settings-manager";
import { loadSettings, saveSettings } from "./settings-manager";
import { getToken } from "../storage";
const getCSS = ({
spacing,
@@ -79,6 +79,15 @@ document.addEventListener("alpine:init", () => {
justify: true,
hyphenate: true,
},
tocOpen: false,
settingsOpen: false,
bookmarksOpen: false,
navigatorOpen: false,
tocItems: [] as any[],
readingTheme: "dark" as string,
readingFont: "literata" as string,
fontSize: 16 as number,
lineHeight: 1.6 as number,
async initReader(config: {
mediaItemId: string;
fileUrl: string;
@@ -87,8 +96,14 @@ document.addEventListener("alpine:init", () => {
mangaType: string;
}) {
this.settings = await loadSettings();
if (this.settings?.reading_theme) {
document.body.classList.add(`theme-${this.settings.reading_theme}`);
if (this.settings) {
this.readingTheme = this.settings.reading_theme || "dark";
this.readingFont = this.settings.reading_font || "literata";
this.fontSize = this.settings.font_size || 16;
this.lineHeight = this.settings.line_height || 1.6;
if (this.settings.reading_theme) {
document.body.classList.add(`theme-${this.settings.reading_theme}`);
}
}
this.view = document.getElementById("reader-view") as any;
const resp = await fetch(config.fileUrl, {
@@ -200,6 +215,87 @@ document.addEventListener("alpine:init", () => {
goToFraction(value: string) {
this.view?.goToFraction?.(parseFloat(value));
},
toggleTOC() {
this.tocOpen = !this.tocOpen;
if (this.tocOpen && this.tocItems.length === 0 && this.book?.toc) {
this.tocItems = this.flattenTOC(this.book.toc);
}
},
toggleSettings() {
this.settingsOpen = !this.settingsOpen;
},
toggleBookmarks() {
this.bookmarksOpen = !this.bookmarksOpen;
},
toggleNavigator() {
this.navigatorOpen = !this.navigatorOpen;
},
toggleWindowShade(panelEl: HTMLElement) {
panelEl.classList.toggle("panel-collapsed");
},
flattenTOC(items: any[], depth = 0): any[] {
const result: any[] = [];
for (const item of items) {
result.push({ ...item, depth });
if (item.subitems?.length) {
result.push(...this.flattenTOC(item.subitems, depth + 1));
}
}
return result;
},
goToTOCItem(item: any) {
if (this.view && item.href) {
this.view.goTo(item.href);
this.tocOpen = false;
}
},
goToBookmarkTarget(cfi: string) {
if (this.view && cfi) {
this.view.goTo(cfi);
this.bookmarksOpen = false;
}
},
applyTheme() {
document.body.className = document.body.className
.replace(/theme-(?!tokyo|dracula|nord|solarized|monokai|one-dark|material|catppuccin)[\w-]+/g, "")
.trim();
document.body.classList.add(`theme-${this.readingTheme}`);
this.applyStyles();
saveSettings({ reading_theme: this.readingTheme });
},
applyFont() {
this.applyStyles();
saveSettings({
reading_font: this.readingFont,
font_size: this.fontSize,
line_height: this.lineHeight,
});
},
applyStyles() {
if (!this.renderer?.setStyles) return;
this.renderer.setStyles(getCSS(this.style));
},
async addBookmark() {
const token = getToken();
if (!token || !this.view) return;
const location = this.view.lastLocation;
if (!location) return;
try {
await fetch("/readers/bookmarks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `Bookmark at ${this.progressText}`,
position: JSON.stringify(location),
}),
});
} catch (_e) {
/* ignore bookmark errors for now */
}
},
handleKeydown(event: KeyboardEvent) {
const k = event.key;
if (k === "ArrowLeft" || k === "h") this.goLeft();