fix: resume epub progress from the fields the API returns

The load function read progress.epub_loc and progress.progress, neither of
which BookProgressRead carries, so resuming never worked and every open
started at page one. Read epub_cfi and percentage instead.

Fail loudly when the book cannot be fetched. The previous version caught
everything and returned {status, error} that nothing consumed, so a missing
book produced a reader sitting on its spinner forever. Check response.ok
before parsing too: fetch resolves on a 404, and arrayBuffer() happily
returns the error page, which only surfaced later as an opaque epub parse
error.

Give the reader a working sidebar, a theme that survives the book's own
CSS, and an error state with a retry. The theme is registered rather than
applied as bare overrides because most EPUBs ship their own body colours
and win otherwise, which is why the page stayed white against a dark UI.
This commit is contained in:
2026-08-11 21:47:12 -04:00
parent ac5a5c75aa
commit ee46f5568c
3 changed files with 329 additions and 227 deletions
@@ -1,21 +1,24 @@
<script>
<script lang="ts">
import { page } from '$app/state';
import EpubReader from '$lib/components/reader/epub-reader.svelte';
let { data } = $props();
let fileId = page.params.fileId;
let bookId = page.params.bookId;
const fileId = page.params.fileId!;
const bookId = page.params.bookId!;
// Make sure this endpoint returns the complete EPUB file
let bookUrl = `/api/books/download/${bookId}/${fileId}`;
// Streams the file through the proxy so the browser fetches it directly
const bookUrl = `/api/books/download/${bookId}/${fileId}`;
</script>
<div class="h-screen w-screen">
<EpubReader
{bookUrl}
{bookId}
initialProgress={data.bookProgress?.progress}
initialEpubLoc={data.bookProgress?.epub_loc}
/>
</div>
<svelte:head>
<title>{data.title} — Chitai</title>
</svelte:head>
<EpubReader
{bookUrl}
{bookId}
title={data.title}
initialProgress={data.initialProgress}
initialEpubLoc={data.initialEpubLoc}
/>
@@ -1,17 +1,29 @@
export async function load({ fetch, params, parent }) {
try {
const response = await fetch(`/api/books/${params.bookId}`);
const result = await response.json();
let bookProgress = result.progress;
import { error } from '@sveltejs/kit';
return {
bookProgress
};
} catch (error) {
console.error('Error fetching book: ', error);
return {
status: error.status || 500,
error: error.message
};
export async function load({ fetch, params }) {
// Fail loudly. The previous version caught everything and returned
// {status, error}, which nothing consumed — so a missing book produced a
// reader that sat on its spinner forever.
const response = await fetch(`/api/books/${params.bookId}`);
if (!response.ok) {
error(response.status, 'This book could not be loaded.');
}
const book = await response.json();
const file = book.files?.find((f: { id: number }) => String(f.id) === params.fileId);
if (!file) {
error(404, 'That file is not part of this book.');
}
return {
title: book.title,
filename: file.filename,
// epub_cfi and percentage are what BookProgressRead actually carries;
// this used to read progress.epub_loc and progress.progress, so resuming
// never worked.
initialEpubLoc: book.progress?.epub_cfi ?? null,
initialProgress: book.progress?.percentage ?? 0
};
}