fix(reader): authenticate book file fetch and inject reading theme colors

Books were failing to load because foliate-js fetches the file URL
without auth headers, getting rejected by JWT middleware. Also,
reading themes were not being applied because getCSS() didn't
inject background/text colors into the book iframe.

- Fetch book file with Bearer token, pass as File (not URL) to
  view.open() so foliate-js can detect format via filename extension
- Add reading theme class to body so foliate-themes.css activates
  the correct --reader-bg/--reader-text CSS variables
- Update getCSS() to read theme colors from outer page and embed
  them in the iframe CSS string (background-color, color, link
  color, selection color)
- Fix Alpine.start() deadlock: move call outside the alpine:init
  listener so Alpine actually initializes
- Remove unused tocItem from relocate handler destructuring
- Replace apiGet/apiPut with direct fetch in settings-manager to
  fix /api prefix mismatch (reader routes are at /readers/*, not
  /api/readers/*)
This commit is contained in:
2026-04-19 18:09:05 -04:00
parent 88c8fbc89d
commit 47e0e96ab8
2 changed files with 55 additions and 10 deletions
+33 -6
View File
@@ -1,6 +1,7 @@
import "foliate-js/view.js";
import { Alpine } from "../alpine";
import { loadSettings, saveSettings } from "./settings-manager";
import { loadSettings } from "./settings-manager";
import { getToken } from "../storage";
const getCSS = ({
spacing,
justify,
@@ -9,14 +10,29 @@ const getCSS = ({
spacing: number;
justify: boolean;
hyphenate: boolean;
}) => `
}) => {
const style = getComputedStyle(document.body);
const bg = style.getPropertyValue("--reader-bg").trim() || "#fafafa";
const text = style.getPropertyValue("--reader-text").trim() || "#1a1a1a";
const link = style.getPropertyValue("--reader-link").trim() || "#0066cc";
const selection =
style.getPropertyValue("--reader-selection").trim() || "#b3d9ff";
return `
@namespace epub "http://www.idpf.org/2007/ops";
html {
color-scheme: light dark;
background-color: ${bg};
color: ${text};
}
::selection {
background-color: ${selection};
}
a:link {
color: ${link};
}
@media (prefers-color-scheme: dark) {
a:link {
color: lightblue;
color: ${link};
}
}
p, li, blockquote, dd {
@@ -44,6 +60,7 @@ const getCSS = ({
display: none;
}
`;
};
document.addEventListener("alpine:init", () => {
Alpine.data("readerShell", () => ({
view: null as any,
@@ -70,8 +87,17 @@ document.addEventListener("alpine:init", () => {
mangaType: string;
}) {
this.settings = await loadSettings();
if (this.settings?.reading_theme) {
document.body.classList.add(`theme-${this.settings.reading_theme}`);
}
this.view = document.getElementById("reader-view") as any;
await this.view.open(config.fileUrl);
const resp = await fetch(config.fileUrl, {
headers: { Authorization: `Bearer ${getToken()}` },
});
const blob = await resp.blob();
const fileName = new URL(config.fileUrl, window.location.origin).pathname;
const file = new File([blob], fileName, { type: blob.type });
await this.view.open(file);
this.renderer = this.view.renderer;
this.book = this.view.book;
this.isFixedLayout = this.view.isFixedLayout;
@@ -90,7 +116,7 @@ document.addEventListener("alpine:init", () => {
);
});
this.view.addEventListener("relocate", (e: any) => {
const { fraction, location, tocItem, pageItem } = e.detail;
const { fraction, location, pageItem } = e.detail;
const percent = new Intl.NumberFormat("en", {
style: "percent",
}).format(fraction);
@@ -188,5 +214,6 @@ document.addEventListener("alpine:init", () => {
}
},
}));
Alpine.start();
});
Alpine.start();
+22 -4
View File
@@ -1,4 +1,3 @@
import { apiGet, apiPut } from "../api";
import { getToken } from "../storage";
const LOCALSTORAGE_KEY = "reader_settings_local";
export async function loadSettings(): Promise<ReaderSettings> {
@@ -8,7 +7,12 @@ export async function loadSettings(): Promise<ReaderSettings> {
return local ? JSON.parse(local) : getDefaultSettings();
}
try {
const response = await apiGet("/readers/settings");
const response = await fetch("/readers/settings", {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const settings = await response.json();
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(settings));
return settings;
@@ -28,7 +32,14 @@ export async function saveSettings(
return;
}
try {
await apiPut("/readers/settings", settings);
await fetch("/readers/settings", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(settings),
});
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
} catch (error) {
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
@@ -41,7 +52,14 @@ export async function syncSettings(): Promise<void> {
const token = getToken();
if (token) {
try {
await apiPut("/readers/settings", settings);
await fetch("/readers/settings", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(settings),
});
} catch (error) {
console.error("Failed to sync settings:", error);
}