Reader: paginated/scrolled mode parity (PDF + EPUB) with shared settings - #62
Merged
Conversation
Adds the design for unifying paginated/scrolled flow across both readers with a shared settings model. Also fixes the books-read 500 where availableFormats are CalibreFormat objects, not strings, which breaks .toLowerCase() in the load fn — and switches the format default to fall back to the first available format instead of always 'epub'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bite-sized tasks for the spec — shared ReaderSettings module + tests, useReaderInputs rune + tests, PaginatedViewport wrapper, shared ReaderSettingsPanel, BookReader/PdfReader rewires, cleanup, build verification, deploy to jellyfin host. Also patched the spec's persistence section to reflect that settings live in localStorage, not server-side userReaderPrefs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Node 25 ships a stub `localStorage` that requires `--localstorage-file=<path>` to function. Inside vitest's jsdom environment, that stub shadows jsdom's own Storage and leaves `setItem`/`clear` undefined. Add a Map-backed Storage polyfill as a vitest setup file so component tests get the DOM behavior they expect without Node CLI flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nce (#61) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shim added in e5af12f works without happy-dom or jsdom — the optional-peer churn in the lockfile was leftover from experimentation and is not needed. Tests still 9/9 with the original lockfile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace BookReader's bespoke localStorage settings + discrete per-field $state with the shared ReaderSettings module. Swap the ad-hoc flow toggle for <ReaderSettingsPanel variant="epub" /> and wrap the foliate-view host in <PaginatedViewport> so paginated-mode inputs/animation live in one place. Theme/font/margin dictionaries renamed to match the canonical ReaderSettings type (oled→night, display→dyslexic, medium→normal); visible UI labels are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T5 implementer renamed BookReader's existing oled/display/medium keys to night/dyslexic/normal to match the plan's reader-settings.ts. That silently broke existing users' stored settings (coerce() would reject oled/display/medium and reset to defaults) and also mislabeled the 'dyslexic' font key — it actually maps to Playfair Display, not an OpenDyslexic stack. Realign reader-settings.ts to the names already in use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T6 added the settings drawer to PdfReader but wired only the PaginatedViewport tap-zone as the entry point. That leaves scrolled mode with no way to open the drawer. Add a Settings icon to the PDF toolbar so the drawer is reachable from any flow mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ader PaginatedViewport's keyboard listener (gated by settings.inputs.keyboard) now owns prev/next navigation. BookReader's own arrow handler was firing the same view.prev()/next() calls, doubling each press. Foliate's prev/next is idempotent so this didn't visibly break anything, but it's redundant and would surface as a race the moment those calls weren't idempotent. KeyboardShortcuts.svelte is purely a help-overlay panel — no listener work to consolidate there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PaginatedViewport wrapped its children in {#key animationKey} so each
prev/next would replay the slide animation. For PDF that's harmless —
the page-card snippet remounts on currentPage anyway. For EPUB it's
fatal: foliate-js's iframe gets destroyed mid-load and the dangling
"once" load handler hits a null contentDocument, throwing
"Cannot read properties of null (reading 'head')". The reader
appears blank and pages "fail to load."
Drop the {#key} wrap. Animations still play in PDF (canvas mounts on
each page change). EPUB loses the cross-iframe slide effect, which is
acceptable — foliate-js handles its own internal page transitions.
Also patches a separate bug in PdfReader's paginated render: the
canvas mounts inside the page-card snippet but renderedPages was
keeping the old page numbers, so revisits would skip the re-render
onto the freshly-mounted canvas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed mode Two real bugs the deploy surfaced: 1. PDF canvases stayed blank in paginated mode. The render-trigger effect ran on currentPage change, but bind:this with array indexing inside a snippet doesn't reliably populate the array slot before the effect fires — canvasRefs[idx] reads null and renderPage early-returns. Add a use:onCanvasMount action that fires render after the canvas is in the DOM and the ref has settled. Independent of the bind:this timing, and gets called on update when pageNum changes (revisits). 2. Paginated dual mode rendered each page at single-page-fit-width scale, so two pages ended up wider than the viewport (page 1 sat at x=-370). Fit-to-viewport: divide width by pagesAcross (1 or 2), take min of fit-width and fit-height. Apply the same scale in both getPageDims (wrapper sizing) and renderPage (canvas content) so wrapper and canvas always agree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Even with use:onCanvasMount firing, canvasRefs[idx] reads as null inside renderPage. bind:this with array indexing (canvasRefs[pageNum-1]) inside a snippet is fragile in Svelte 5 — the slot can be observed empty by code running on the same tick as the snippet mount, even when the canvas is fully in the DOM. Fall back to a data-page DOM lookup when the array slot is empty; populate the array slot at the same time so subsequent calls hit the cache. Works for both paginated and scrolled modes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Paginated mode auto-fits each page to the viewport, which made the toolbar zoom controls dead — they wrote to scale but renderPage ignored scale in favor of the fit calculation. Add a paginatedZoom multiplier (default 1.0 = exact fit) that gets applied on top of auto-fit. ZoomIn/Out adjust paginatedZoom in paginated mode and scale in scrolled mode. The toolbar percentage display reflects whichever is active. Toolbar W (fit-width) / P (fit-page) buttons remain only meaningful in scrolled mode — they're no-ops in paginated since auto-fit owns that. Leaving them visible for now; can hide later if it confuses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zoom-in clicked while page 1 was still mid-render did nothing because invalidateAllPages only iterated renderedPages (finished pages), not renderingPages (in-flight). The in-flight render kept its old scale, finished, and added itself to renderedPages — masking the new zoom. Iterate the union so cleanupPage can cancel the active render task too. Also strip the temporary console traces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PetalCat
added a commit
that referenced
this pull request
May 19, 2026
PDF reader gains paginated mode (was scroll-only). EPUB gains the missing flow/spread/animation/inputs/direction controls. Shared ReaderSettings model + shared settings UI (ReaderSettingsPanel), persisted client-side under nexus-reader-settings. New components: PaginatedViewport (generic wrapper), useReaderInputs rune (tap zones / swipe / keyboard with RTL flip), ReaderSettingsPanel. Animations: slide / fade / none. PDF toolbar gains a Reader Settings gear button. Bug fix: /books/read/[id]/+page.server.ts no longer crashes on CalibreFormat objects and no longer defaults to EPUB when EPUB isn't available. Closes #61. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PetalCat
added a commit
that referenced
this pull request
May 19, 2026
PDF reader gains paginated mode (was scroll-only). EPUB gains the missing flow/spread/animation/inputs/direction controls. Shared ReaderSettings model + shared settings UI (ReaderSettingsPanel), persisted client-side under nexus-reader-settings. New components: PaginatedViewport (generic wrapper), useReaderInputs rune (tap zones / swipe / keyboard with RTL flip), ReaderSettingsPanel. Animations: slide / fade / none. PDF toolbar gains a Reader Settings gear button. Bug fix: /books/read/[id]/+page.server.ts no longer crashes on CalibreFormat objects and no longer defaults to EPUB when EPUB isn't available. Closes #61. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #61.
Summary
ReaderSettingsmodel + shared settings UI (ReaderSettingsPanel), persisted client-side inlocalStorageunder the existingnexus-reader-settingskey.PaginatedViewport(generic wrapper),useReaderInputsrune (tap zones / swipe / keyboard with RTL flip),ReaderSettingsPanel./books/read/[id]/+page.server.tsno longer crashes whenmetadata.formatsareCalibreFormatobjects (it always was —.toLowerCase()on an object), and no longer defaults to EPUB when EPUB isn't an available format.Spec:
docs/superpowers/specs/2026-04-19-reader-paginated-mode-design.mdPlan:
docs/superpowers/plans/2026-04-19-reader-paginated-mode.mdWhat was harder than expected
bind:thiswith array indexing inside Svelte 5 snippets is fragile — the slot can read empty even when the canvas is mounted. Switched to ause:onCanvasMountaction that caches the ref into the array directly and kicks off render viaqueueMicrotask(RAF gets cancelled by the action's lifecycle during commit).PaginatedViewport's original{#key animationKey}wrap killed the EPUB iframe each navigation (foliate-js'saddEventListener.oncethen hit a nullcontentDocument). Removed the key wrap; PDF still re-mounts naturally oncurrentPagechange so animations still play.pagesAcross.invalidateAllPagesonly iteratedrenderedPages(finished), notrenderingPages(in-flight). Fixed by iterating the union so the active task gets cancelled.oled/display/mediumkeys to match a stale plan, which would silently reset existing users' stored settings; reverted to the original keys.Out of scope (per spec, intentional)
Test plan
/books/read/<id>— paginated mode renders, settings drawer has new controlspnpm checkclean,pnpm vitest run161/161 passing,pnpm buildsucceeds. Verified live on jellyfin host via:devbuild through the entire iteration.🤖 Generated with Claude Code