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,27 +0,0 @@
|
|||||||
<script>
|
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index';
|
|
||||||
|
|
||||||
let { chapters } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Sidebar.Root>
|
|
||||||
<Sidebar.Header />
|
|
||||||
<Sidebar.Content>
|
|
||||||
<Sidebar.GroupLabel>Chapters</Sidebar.GroupLabel>
|
|
||||||
<Sidebar.GroupContent>
|
|
||||||
<Sidebar.Menu>
|
|
||||||
{#each chapters as chapter}
|
|
||||||
<Sidebar.MenuItem>
|
|
||||||
<Sidebar.MenuButton>
|
|
||||||
{#snippet child({ props })}
|
|
||||||
<span>{chapter.label}</span>
|
|
||||||
{/snippet}
|
|
||||||
</Sidebar.MenuButton>
|
|
||||||
</Sidebar.MenuItem>
|
|
||||||
{/each}
|
|
||||||
</Sidebar.Menu>
|
|
||||||
</Sidebar.GroupContent>
|
|
||||||
<Sidebar.Group />
|
|
||||||
</Sidebar.Content>
|
|
||||||
<Sidebar.Footer />
|
|
||||||
</Sidebar.Root>
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
<script lang="ts">
|
<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();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,99 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
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 { data } = $props();
|
||||||
let bookId = page.params.bookId;
|
|
||||||
|
|
||||||
// Make sure this endpoint returns the complete PDF file
|
const fileId = page.params.fileId;
|
||||||
let url = `${page.url.origin}/api/books/download/${bookId}/${fileId}`;
|
const bookId = page.params.bookId;
|
||||||
|
|
||||||
|
const fileUrl = `${page.url.origin}/api/books/download/${bookId}/${fileId}`;
|
||||||
|
|
||||||
let iframeEl = $state<HTMLIFrameElement>();
|
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(() => {
|
$effect(() => {
|
||||||
if (iframeEl && url) {
|
attempt;
|
||||||
// Use relative path to viewer.html with the PDF file URL as a parameter
|
if (iframeEl) load();
|
||||||
iframeEl.src = `/pdfjs/web/viewer.html?file=${encodeURIComponent(url)}`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="pdf-container">
|
<svelte:head>
|
||||||
<iframe bind:this={iframeEl} title="PDF Viewer" class="pdf-iframe"></iframe>
|
<title>{data?.title ? `${data.title} — Chitai` : 'Reader — Chitai'}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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}
|
||||||
|
|
||||||
<style>
|
<iframe
|
||||||
/* Make the container take up the full viewport height */
|
bind:this={iframeEl}
|
||||||
.pdf-container {
|
title="PDF viewer"
|
||||||
position: fixed;
|
class="h-full w-full border-0 {loading || loadError ? 'invisible' : ''}"
|
||||||
top: 0;
|
onload={() => (loading = false)}
|
||||||
left: 0;
|
></iframe>
|
||||||
right: 0;
|
</div>
|
||||||
bottom: 0;
|
</main>
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make the iframe fill its container */
|
|
||||||
.pdf-iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -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