fix: surface reader load failures instead of spinning forever
The PDF reader pointed an iframe at the vendored pdf.js viewer and had no way to report a failure: the viewer renders its own errors inside the frame, so a missing or unreadable file showed an empty grey panel. Probe the file with a request first and render an error card with a retry when that fails, matching the chrome the EPUB reader uses. Add the missing +page.ts for the PDF route so the tab gets the book title rather than a bare "Reader". Drop chapter-sidebar.svelte and its import. It rendered a list of chapters whose buttons had no click handler, and read/+layout@.svelte imported it without ever rendering it. The reader's own sidebar replaces it.
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
<script lang="ts">
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import ChapterSidebar from '$lib/components/reader/chapter-sidebar.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,42 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import { Spinner } from '$lib/components/ui/spinner/index';
|
||||
import ThemeToggle from '$lib/components/layout/theme-toggle.svelte';
|
||||
import { RotateCcw, TriangleAlert } from '@lucide/svelte';
|
||||
|
||||
let fileId = page.params.fileId;
|
||||
let bookId = page.params.bookId;
|
||||
let { data } = $props();
|
||||
|
||||
// Make sure this endpoint returns the complete PDF file
|
||||
let url = `${page.url.origin}/api/books/download/${bookId}/${fileId}`;
|
||||
const fileId = page.params.fileId;
|
||||
const bookId = page.params.bookId;
|
||||
|
||||
const fileUrl = `${page.url.origin}/api/books/download/${bookId}/${fileId}`;
|
||||
|
||||
let iframeEl = $state<HTMLIFrameElement>();
|
||||
let loading = $state(true);
|
||||
let loadError = $state<string | null>(null);
|
||||
let attempt = $state(0);
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
loadError = null;
|
||||
|
||||
try {
|
||||
// Ask for the file before handing it to pdf.js. The viewer runs in an
|
||||
// iframe and reports its own failures inside that frame, so without
|
||||
// this probe a missing file just showed an empty grey panel.
|
||||
const response = await fetch(fileUrl, { method: 'HEAD' });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`The server returned ${response.status} for this file.`);
|
||||
}
|
||||
|
||||
if (iframeEl) {
|
||||
iframeEl.src = `/pdfjs/web/viewer.html?file=${encodeURIComponent(fileUrl)}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF', error);
|
||||
loadError = error instanceof Error ? error.message : 'The file could not be opened.';
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (iframeEl && url) {
|
||||
// Use relative path to viewer.html with the PDF file URL as a parameter
|
||||
iframeEl.src = `/pdfjs/web/viewer.html?file=${encodeURIComponent(url)}`;
|
||||
}
|
||||
attempt;
|
||||
if (iframeEl) load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="pdf-container">
|
||||
<iframe bind:this={iframeEl} title="PDF Viewer" class="pdf-iframe"></iframe>
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>{data?.title ? `${data.title} — Chitai` : 'Reader — Chitai'}</title>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
/* Make the container take up the full viewport height */
|
||||
.pdf-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
<main class="flex h-screen w-full flex-col overflow-hidden bg-background">
|
||||
<!-- Same chrome as the EPUB reader, so leaving works the same way in both -->
|
||||
<header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
||||
<a
|
||||
href="/book/{bookId}"
|
||||
class="min-w-0 flex-1 truncate font-serif text-sm hover:underline"
|
||||
title={data?.title}
|
||||
>
|
||||
{data?.title || 'Reader'}
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
/* Make the iframe fill its container */
|
||||
.pdf-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
<div class="relative flex-1">
|
||||
{#if loadError}
|
||||
<div class="absolute inset-0 flex items-center justify-center p-6">
|
||||
<div class="flex max-w-sm flex-col items-center gap-3 text-center">
|
||||
<TriangleAlert class="size-8 text-destructive" />
|
||||
<h2 class="font-serif text-lg">This file wouldn't open</h2>
|
||||
<p class="text-sm text-muted-foreground">{loadError}</p>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<Button onclick={() => attempt++}>
|
||||
<RotateCcw class="size-4" />
|
||||
Try again
|
||||
</Button>
|
||||
<a href="/book/{bookId}" class={buttonVariants({ variant: 'outline' })}>
|
||||
Back to book
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center gap-3">
|
||||
<Spinner />
|
||||
<span class="text-sm text-muted-foreground">Opening…</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<iframe
|
||||
bind:this={iframeEl}
|
||||
title="PDF viewer"
|
||||
class="h-full w-full border-0 {loading || loadError ? 'invisible' : ''}"
|
||||
onload={() => (loading = false)}
|
||||
></iframe>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ fetch, params }) {
|
||||
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();
|
||||
|
||||
return { title: book.title };
|
||||
}
|
||||
Reference in New Issue
Block a user