From 47e0e96ab869561af0bc58c0504f2f6012d94b90 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 19 Apr 2026 18:09:05 -0400 Subject: [PATCH] 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/*) --- web/src/reader/reader.ts | 39 +++++++++++++++++++++++++----- web/src/reader/settings-manager.ts | 26 +++++++++++++++++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index eed45a6..26bae9e 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -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(); diff --git a/web/src/reader/settings-manager.ts b/web/src/reader/settings-manager.ts index dcd56f6..b819095 100644 --- a/web/src/reader/settings-manager.ts +++ b/web/src/reader/settings-manager.ts @@ -1,4 +1,3 @@ -import { apiGet, apiPut } from "../api"; import { getToken } from "../storage"; const LOCALSTORAGE_KEY = "reader_settings_local"; export async function loadSettings(): Promise { @@ -8,7 +7,12 @@ export async function loadSettings(): Promise { 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 { 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); }