Commit Graph
243 Commits
Author SHA1 Message Date
john-okeefe 6e2d304b35 feat(frontend): integrate shared library switcher into all pages
Wire up the shared library switcher module on dashboard, collections
list, collection detail, and collection rules pages. All pages use SSR
for initial load and AJAX with fade transitions on library switch.

web/src/collections.ts:
- Add initCollectionsPage() that auto-detects list vs detail page
  by checking for #collection-data element
- Collections list: onSwitch fetches /api/collections?library_id=X and
  re-renders the grid with per-library book counts
- Collection detail: onSwitch fetches /api/collections/:id?library_id=X
  and re-renders the books grid
- Add renderCollectionsGrid() and renderCollectionBooks() with
  Alpine.initTree() calls for dynamic content
- Collection cards now link with ?library_id= from selected library
- Update hidden #collection-data data-library-id on switch

web/src/dashboard.ts:
- Replace standalone switchLibrary() with initLibrarySwitcher() +
  switchWithTransition() from shared module
- Extract fetchAndRenderSections() helper shared by onSwitch callback,
  reloadPage(), and saveDashboardSettings()
- Remove inline #library-select change listener and switch-library
  data-action handler (now handled by shared module)
- Scan-complete event handler unchanged (independent incremental logic)

web/src/collection-rules.ts:
- Update backToCollection() to preserve library context by appending
  ?library_id= from localStorage selectedLibrary key
2026-05-17 21:12:45 -04:00
john-okeefe 59cb0e3439 feat(library-switcher): add shared library switcher component and module
Add reusable library switcher infrastructure that can be used across
dashboard, collections list, and collection detail pages.

New files:
- templates/library_switcher.templ: Shared LibrarySwitcher component
  with variadic actions slot for page-specific buttons (e.g. dashboard
  settings/refresh). Includes DashboardActions sub-component.
- web/src/library-switcher.ts: Shared module providing:
  - initLibrarySwitcher(): syncs dropdown with localStorage, attaches
    change listener with configurable onSwitch callback
  - switchWithTransition(): generic fade-out -> spinner -> fetch ->
    fade-in transition used by all pages
  - getCurrentLibraryId(): reads "selectedLibrary" from localStorage

Modified:
- web/src/main.ts: import new library-switcher module
- web/src/types/api.d.ts: add book_count field to CollectionData
2026-05-17 21:11:47 -04:00
john-okeefe 158fcb510b chore(templates): regenerate all templ Go files, add scan spinner to header
Regenerated templ output for all template files. Key source change:
- templates/header.templ: add scan progress spinner SVG and percentage
  display to header nav, initialize scan listener via x-init
2026-05-16 19:31:52 -04:00
john-okeefe c41e4af8b0 feat(dashboard): dynamic scan-complete refresh without page reload
When a scan completes, dynamically update the dashboard carousels instead of
requiring a full page reload:

- Listen for bookhoard:scan-complete custom event dispatched by header
- Fetch updated sections from /api/dashboard/sections
- Diff existing book cards by data-media-item-id attribute
- Prepend new items to carousel tracks (afterbegin) to match API sort order
- Create entirely new section DOM for sections that don't yet exist on page
- Remove 'No items' placeholder when items are added
- Scroll carousel to left (scrollLeft=0) so newly prepended items are visible

Also:
- Extract renderSectionHTML() helper from renderDashboardCollections() for reuse
- Add data-media-item-id attribute to book card template for DOM diffing
- Add diagnostic console.log statements for debugging scan-complete flow
2026-05-16 19:31:34 -04:00
john-okeefe 55a299540c feat(header): add scan progress spinner and dispatch scan-complete event
Add a scan progress indicator to the header that shows during library scans:
- Spinning SVG icon next to the BookHoard title
- Percentage display during active scans
- Dispatches bookhoard:scan-complete custom DOM event on window when scan
  finishes, enabling other components (dashboard) to react without polling
- Auto-resets progress display after 3 seconds
- Uses WebSocket pub/sub via addListener/removeListener with cleanup on
  header element removal
2026-05-16 19:31:23 -04:00
john-okeefe c4f972aba1 refactor(websocket): convert to pub/sub pattern with addListener/removeListener
Replace the single-listener createWebSocket pattern with a pub/sub model
using addListener/removeListener. This allows multiple components (header
spinner, dashboard refresh) to subscribe to WebSocket messages independently
without clobbering each other's handlers.

- Maintain a Set of message listeners
- Auto-connect on first addListener, auto-disconnect when last listener removed
- Retain reconnect logic with configurable delay
2026-05-16 19:31:16 -04:00
john-okeefe 6782d7a741 refactor(reader): remove vendored pdfjs files from git, drop CJK cmaps
Remove 185 binary files (169 CMaps + 16 standard fonts) from git
tracking. These are build artifacts copied from
node_modules/@bookhoard/foliate-js at build time and should not be
version-controlled.

Changes:
- Remove web/static/vendor/pdfjs/ from git (169 cmap files + 16
  standard font files)
- Add web/static/vendor/ to .gitignore
- Drop CJK cmap copying from build scripts — the app is English-only
  and CJK support can be re-added later if needed (saves ~1.7MB in
  the container image)
- Update all three build scripts (build:ts, build:ts:dev,
  build:ts:watch) to copy only standard_fonts/ from node_modules
- Remove cMapUrl from reader.ts PDF config since we no longer ship
  cmaps
- Keep standardFontDataUrl pointing to the build-copied fonts which
  are needed for PDFs with non-embedded standard fonts (Helvetica,
  Times, Courier, etc.)
2026-05-11 14:57:22 -04:00
john-okeefe be1c373b19 feat(metadata-editor): replace tags text input with badge picker + autocomplete
Replace the comma-separated text input for tags in the metadata editor
with a badge-based tag picker:
- Current tags shown as removable pill badges (✕ button per tag)
- Autocomplete input queries existing tags via shared tag-dropdown module
- Results shown as inline dropdown (not absolute, avoids overflow clipping
  from the modal's overflow-y-auto content area)
- Keyboard navigation: ArrowUp/Down, Enter to select, comma to add new,
  Escape to close
- Supports adding new tags not in the database (type + Enter/comma)
- Hidden data-editor-tag spans seed initial tags from server-side render
- collectFormData() now accepts editorTags array, skips the removed
  tags text input
2026-05-10 16:13:07 -04:00
john-okeefe fb9c471959 feat(bookshelf): replace broken datalist tag filter with custom autocomplete
The HTML <datalist> approach for tag autocomplete was unreliable across
browsers — showed empty suggestions or no dropdown at all.

Replace with a custom Alpine.js dropdown:
- New tag-dropdown.ts shared module: searchTagSuggestions() queries
  /api/media-items/search?tags=...&library_id=... and returns results
- Bookshelf: absolute-positioned dropdown below tags_filter input, shows
  tag name + book count per suggestion
- Keyboard navigation: ArrowUp/Down to highlight, Enter to select,
  Escape to close
- Click suggestion to populate the filter input
2026-05-10 16:12:44 -04:00
john-okeefe 1724bc0767 feat(frontend): wire metadata editor Alpine component in book-detail.ts
Replace placeholder toast with full metadata editor Alpine data component:
- Modal show/hide (showMetadataEditor, hideMetadataEditor)
- Accordion section toggle
- Cover upload via FileReader preview
- Cover generation via dynamic cover-generator import
- Cover removal with placeholder fallback
- saveMetadata(): collects form data, sends PUT as JSON or multipart
  depending on whether a cover file is present
- Back button fix: skip overwriting sessionStorage back URL when
  referrer is the current page (preserves navigation after page reload)
2026-05-10 11:53:50 -04:00
john-okeefe 2ef1f9580f feat(frontend): add client-side cover generator via dynamic foliate-js import
New cover-generator.ts module that dynamically imports foliate-js/view.js
only when cover generation is requested, keeping it out of the main bundle.

Supports all media types:
- PDF (fixed_layout): renders page 1 to canvas via view.renderer
- EPUB/CBZ (reflowable): extracts book.cover blob from parsed metadata
- Falls back to canvas-to-JPEG conversion for non-JPEG sources
2026-05-10 11:53:31 -04:00
john-okeefe 0682d0a1cb fix(reader): add explicit PDF.js resource paths and pin foliate-js fork
foliate-js could not locate cmaps and standard_fonts at runtime because
no explicit paths were provided to the PDF.js config. This caused
rendering failures for PDFs using CJK fonts or standard PDF fonts.

Changes:
- Pass cMapUrl and standardFontDataUrl to view.open() in reader.ts
- Pin foliate-js fork to commit 74c317d in package.json for reproducibility
- Update build:ts script to copy cmaps/ and standard_fonts/ to
  web/static/vendor/pdfjs/ during build
2026-05-10 11:52:23 -04:00
john-okeefe 52464581dc feat(series): add dedicated series detail page instead of bookshelf filter
Create a new /series/detail?name=X&library_id=Y SSR page that shows
all books in a specific series, replacing the broken approach of
linking to /bookshelf?series_filter=X (the bookshelf SSR handler
ignores all filter query params).

The series detail page features:
- Back link to /series browse page
- Library selector dropdown (full page navigation on change)
- Series name header with book count badge
- Book grid using the shared BookCard template
- Empty state for series with no books

Update all links to point to the new page:
- Series cards on /series browse page
- Series badge on book detail page
- JS-rendered cards in series.ts switchLibrary

Add seriesDetailPage Alpine component for the detail page's
library switcher (simple navigation, no AJAX needed).
2026-05-08 20:50:59 -04:00
john-okeefe 4cb72fc9ae feat(series): add AJAX library switching and stacked-cascade CSS
Create web/src/series.ts with Alpine component implementing the same
switchLibrary pattern as the dashboard:
- Fetch /api/series on library change instead of full page reload
- Fade out/in transition with loading spinner
- Re-render series grid and pagination from JSON response
- Save selected library to localStorage

Import series.ts in main.ts.

Add stacked-cascade CSS to input.css for multi-cover series cards:
- Covers cascade from top-left to bottom-right with increasing z-index
- Front cover sits at bottom-right (highest z-index)
- Separate layout rules for 1-7 covers with rotation offsets
- Hover lift effect on series cards
2026-05-08 20:27:27 -04:00
john-okeefe d222257797 feat(ui): persist library selection across page navigation
Save the selected library to localStorage when the user changes the
dropdown on the bookshelf page, and restore it on every page load via
a new restoreLibrarySelection() call in the header Alpine component.

This ensures that when a user navigates between dashboard, bookshelf,
collections, etc., their last-chosen library filter is automatically
re-applied rather than resetting to the default.

Changes:
- web/src/bookshelf.ts: listen for change events on #library-select
  and persist the value to localStorage
- web/src/header.ts: add restoreLibrarySelection() which checks
  localStorage and sets the matching dropdown option on page load
- templates/header.templ: call restoreLibrarySelection() in x-init
- templates/header_templ.go: regenerated from templ source
2026-04-26 21:21:40 -04:00
john-okeefe 82d0d378a6 feat(reader): send richer progress payload with chapter boundaries and zoom
The reader's saveProgress() now sends a more complete payload to the
backend so ProgressService has more data for enrichment and merge:

- chapter: computed from TOC boundary index instead of missing
- reading_mode: current display mode (page, chapter, percent, time-left)
- zoom_level: for fixed-layout books (renderer.zoomPercent / 100)
- current_page: real page number for fixed-layout, location.current for
  reflowable
- total_pages: section count for fixed-layout, location.total for
  reflowable

Adds computeChapterPageBoundaries(doc) for reflowable EPUBs that maps
TOC anchors to rendered page numbers, recomputes after fonts load.
Adds computeFixedLayoutChapterBoundaries() for fixed-layout books that
resolves TOC hrefs to page indices via view.resolveNavigation().

Updates reader.templ to expose isFixedLayout to Alpine init.
2026-04-25 21:16:52 -04:00
john-okeefe e66308c323 feat(reader): wire up progress mode switching with four display modes
Connect the existing progress_mode setting dropdown to the reader's
progress display. Four modes are now functional:
- pages: overall percent + page/location number (default, existing)
- chapter: chapter title + page X / Y within current section
- percentage: overall percent only
- time-left: percent + estimated time remaining via reading speed API
The progress display in the bottom bar is now clickable to cycle through
modes with immediate visual feedback. The settings dropdown is bound
with x-model for persistence. Reading speed is fetched once on init
from the backend reading-speed API for time-left estimates.
2026-04-25 13:40:18 -04:00
john-okeefe f24e354549 feat(types): add FoliateTocItem interface for foliate-js TOC entries
Add a typed interface for the tocItem data returned by foliate-js
relocate events, replacing untyped usage in the reader progress display.
2026-04-25 13:39:07 -04:00
john-okeefe 163b3162b9 feat(reader): wire up reading progress save and restore
The web reader had all the infrastructure for progress persistence
(updateReadingProgress/getReadingProgress API functions, PUT/GET
endpoints, database queries) but the reader.ts never called them.

Changes:
- Add debounced (2s) saveProgress call on every relocate event that
  PUTs percentage, current_page, total_pages, and epubcfi to the
  existing /api/media-items/:id/progress endpoint
- Replace renderer.next() with view.init({ lastLocation }) to restore
  the reader to the last saved position on load (CFI first, then
  fraction fallback, then default first page)
- Pass savedPercentage and savedCfi from server-side progress data
  through readerInitExpr config to the JS initReader function
- Add mediaItemId and saveTimeout to the Alpine data object

This fixes both the blank /progress page and the missing progress
section on book detail pages — both were empty because the
reading_progress table never received any data from the web reader.
2026-04-23 21:08:30 -04:00
john-okeefe dac7c03ce7 build: regenerate CSS after dashboard changes 2026-04-23 17:01:30 -04:00
john-okeefe abef40575f fix(dashboard): use anchor tags for client-side rendered book cards
When switching libraries via TypeScript, renderBookCard() built book
cards as <div> elements with data-action="view-book" for event
delegation, but the click handler was commented out — making books
unclickable after any library switch. The SSR path used proper <a> tags.

Now renderBookCard() wraps cards in <a href="/media/{id}"> to match the
SSR BookCard template, so book links work identically regardless of
whether content was server-rendered or client-rendered.

Also removed the dead view-book handler code and viewBook() stub.

Additionally fixed a listener re-registration bug where the input and
library-select change listeners were nested inside the click callback,
causing them to be registered N times after N clicks. Moved them to
initDashboard() scope so they register exactly once.
2026-04-23 17:01:20 -04:00
john-okeefe 2fe5fea3d8 feat(reader): add reading_mode (dark/light) to ReaderSettings type
Add the 'reading_mode' field with 'dark' | 'light' values to the
ReaderSettings TypeScript interface, preparing the frontend for a
dark/light reading mode toggle.
2026-04-20 20:45:54 -04:00
john-okeefe 3825b93d22 feat(reader): expand reading theme palette with 20 named themes
Replace the original 5-theme allowlist (light, sepia, dark, night,
high-contrast) with a richer 20-theme palette organized into tonal
families: neutrals (light, paper, slate, oled), warm tones (sepia,
parchment, warm, candlelight), cool tones (azure, sky, arctic, frost),
and evening tones (dusk, sunset, twilight, forest, moss, solarized).

The backend validator in UpdateSettings now accepts all 20 theme names,
and the frontend Tailwind build is updated to include the new theme CSS
variables and preflight reset.
2026-04-19 21:50:40 -04:00
john-okeefe ba502ec750 refactor(reader): remove test font, move settings panel to right sidebar
- Remove Playwrite NZ Guides test font and all references (FONT_MAP,
  FONT_FILES, reader-fonts.css, dropdown option, font files)
- Move settings panel from left sidebar to right sidebar (left sidebar
  now only contains TOC)
- Move Restore Defaults button from top bar icon to a styled button
  inside the settings panel, side by side with Done button
2026-04-19 21:42:19 -04:00
john-okeefe 9ca8e2aa44 fix(reader): use blob URLs for fonts to bypass sandboxed iframe restrictions
The paginator renders inside a sandboxed iframe that blocks @font-face
URL fetches. Fonts were never loading — all font-family rules fell back
to the generic 'serif' system font, making every reading font identical.

Fix: fetch font files on the parent page, create blob: URLs via
URL.createObjectURL(), and use those blob URLs in the @font-face rules
injected into the iframe via setStyles(). Blob URLs are always
same-origin with the creating document, so the sandboxed iframe can
access them with allow-same-origin.

Also added Playwrite NZ Guides as a test font for verifying font
switching works.
2026-04-19 21:36:19 -04:00
john-okeefe 0007b4d1d0 fix(reader): inject reader-fonts.css into iframe, remove foliate-themes.css
Fonts weren't loading because the paginator renders inside a sandboxed
iframe. @font-face declarations in the parent page's CSS are invisible
to the iframe's document. Even injecting @font-face rules via
setStyles() may not trigger font loading in sandboxed iframes.

Fix: inject a <link> to reader-fonts.css directly into the iframe's
document on each section load, so @font-face declarations are parsed
in the iframe's own document context where font-family rules can
reference them.

Also:
- Remove foliate-themes.css entirely (no longer needed)
- Set viewport background color directly via JS using THEME_COLORS map
- Remove reading theme CSS classes from viewport element
2026-04-19 21:23:55 -04:00
john-okeefe 4f5825529a fix(reader): set background-color on html/body instead of background: none
background: none on html/body exposed the iframe's default black
background in gaps around the content. Now uses background-color
matching the theme color, and stops forcing background on body *
which caused black bars around page margins.
2026-04-19 21:16:34 -04:00
john-okeefe db33771575 fix(reader): inject @font-face rules into shadow DOM iframe for font switching
The paginator renders book content inside a sandboxed iframe within a
closed shadow DOM. @font-face declarations from the parent page's
reader-fonts.css are NOT available inside the iframe's document context.
All font-family rules fell back to the generic 'serif' system font,
making every reading font look identical.

Fix: prepend all @font-face declarations (Literata, Crimson Pro,
Source Serif 4, EB Garamond, Libertinus Serif, Noto Serif, Charis SIL,
IBM Plex Serif) into the CSS string returned by getCSS(), so they're
injected into the iframe via renderer.setStyles().
2026-04-19 21:15:10 -04:00
john-okeefe 95202f1e28 fix(reader): remove Comic Sans test font (system font not available on Linux)
Comic Sans MS is a system font not available on Linux. The cursive
fallback rendered as a script font, giving false negatives. All 8
loaded reading fonts are serif fonts loaded via @font-face, so they
intentionally look similar — font switching is confirmed working.
2026-04-19 21:12:20 -04:00
john-okeefe 0eb3a7df48 fix(reader): rewrite getCSS with concrete theme colors and aggressive !important
Studied grimmory-tools/grimmory's ebook-reader style.service.ts and adopted
their approach:

- Replace CSS custom property resolution (getComputedStyle) with a hardcoded
  THEME_COLORS map containing concrete fg/bg/link values for all 18 themes
  in both light and dark modes. No more variable resolution failures.

- Font family now targets body + body * with !important, overriding book CSS
  on every element (matching grimmory's approach).

- Colors use grimmory's aggressive pattern:
  html, body { color: ... !important; background: none !important; }
  body * { color: inherit !important; background-color: ... !important; }
  This forces reading theme colors on ALL book elements, overriding inline
  styles and book stylesheets.

- Line height uses !important on p, li, blockquote, dd to override book CSS.

- Removed fragile getComputedStyle calls entirely. getCSS() now receives
  themeName and themeMode parameters for direct color lookup.
2026-04-19 21:01:07 -04:00
john-okeefe d68879a6c6 fix(reader): add test font, expand ranges, fix colors, add restore defaults
- Add Comic Sans MS as a test font option to verify font switching works
- Add !important to background-color and color in getCSS() to prevent
  book CSS from overriding user's reading theme colors
- Expand font size range from 12-24px to 10-40px, bump default to 18px
- Expand line height range from 1.0-2.5 to 0.8-3.0
- Add restoreDefaults() method that resets reading theme, font, size,
  line height, and justify/hyphenate to sensible defaults
- Add ↩️ restore defaults button in top bar underneath the settings gear
2026-04-19 20:51:02 -04:00
john-okeefe 863c0fd9af fix(reader): scope reading theme to viewport, wire up font/size/line-height to shadow DOM
Two root causes fixed:

1. Reading theme CSS variables were on document.body, leaking font/color
   into chrome UI. Now scoped to #reader-viewport so chrome keeps its own
   theme (system font, --text-primary colors) while the reading area uses
   reading theme colors/background.

2. getCSS() never received font family, font size, or line height settings.
   The settings UI (dropdowns, sliders) saved values but they were never
   injected into the book's shadow DOM. Now getCSS() accepts all four
   settings and generates proper CSS rules for them.

Changes:
- Wrap foliate-view in #reader-viewport div (absolute positioned between
  chrome bars)
- getCSS() reads computed style from #reader-viewport, not document.body
- getCSS() params expanded: fontFamily, fontSize, lineHeight, justify,
  hyphenate (removed unused 'spacing')
- Added FONT_MAP to translate setting keys to CSS font-family values
- applyTheme() targets #reader-viewport instead of document.body
- Removed dead #reader-viewport typography rules from foliate-themes.css
  (shadow DOM doesn't inherit outer styles), kept only background-color
2026-04-19 20:36:18 -04:00
john-okeefe 96effde421 fix(reader): inset foliate-view between chrome bars and remove base typography
- Position foliate-view with absolute inset-x-0 top-[52px] bottom-[52px]
  so book content renders between the fixed header and footer bars instead
  of behind them
- Confirmed removal of foliate-themes.css base typography was correct:
  body color now comes from chrome theme's --text-primary (light for
  dark chrome themes like tokyo-night)
2026-04-19 20:19:35 -04:00
john-okeefe 36de7cfa2f fix(reader): dark text on dark themes and add toggle icons
Two fixes:

1. Remove base typography block from foliate-themes.css. The html/body
   rules were unlayered CSS that overrode the chrome theme's layered
   body styles, causing dark reading theme text colors (--reader-text)
   to apply to the outer chrome UI on dark backgrounds. These styles
   are only meant for the shadow DOM, which getCSS() already handles.

2. Move missing typography rules (img, blockquote, a, p orphans/widows)
   into getCSS() so the shadow DOM still gets them.

3. Add sun/moon emoji indicators to the light/dark toggle switch.
2026-04-19 20:12:34 -04:00
john-okeefe 48a8716a25 feat(reader): add light/dark mode toggle for reading themes
Add explicit light/dark mode toggle switch to the reading theme settings.
The reading mode defaults based on the chrome theme (dark chrome themes
like tokyo-night default to dark reading mode).

- Add readingMode property to readerShell Alpine component
- Add toggleReadingMode() method that toggles dark class on body
- Add detectChromeDarkMode() to infer default from chrome theme
- Update applyTheme() to add/remove dark class and persist reading_mode
- Add toggle switch UI in settings panel (blue pill style, next to
  Reading Theme heading)
- Add reading_mode to default settings in settings-manager
2026-04-19 20:05:42 -04:00
john-okeefe 0589157b57 feat(reader): wire up TOC, bookmarks, and navigator panels with Alpine.js bindings
Replace dead data-action attributes with Alpine.js @click handlers and
x-ref references across all reader panels:

- TOC panel: replaced static <nav> with x-for loop over tocItems array,
  added goToTOCItem() click handler, window-shade toggle via .tocPanel
- Bookmarks panel: replaced data-action with @click.prevent handlers,
  added goToBookmarkTarget() using data-cfi attributes for navigation,
  window-shade toggle via .bookmarksPanel
- Navigator panel: replaced data-action with @click window-shade toggle
  via .navigatorPanel
- Added goToBookmarkTarget() and toggleWindowShade() methods to reader.ts
- Removed unused panel-lock buttons (lock feature not yet implemented)
- Regenerated reader_templ.go, rebuilt CSS and JS bundles
2026-04-19 18:17:42 -04:00
john-okeefe 47e0e96ab8 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/*)
2026-04-19 18:09:05 -04:00
john-okeefe afeb3f5b45 refactor(reader): rewrite reader module for foliate-js pan/zoom integration
Major rewrite of the web reader to properly interface with
@bookhoard/foliate-js, replacing the abandoned panel-detection
architecture with direct pan and zoom support built into the
foliate-js FixedLayout renderer.

Template (reader.templ):
- Fix critical bug: x-init config was using literal strings
  '{ readerData.X }' inside a quoted attribute, which templ
  treated as raw text and never interpolated. Values were never
  actually passed to JavaScript. Now uses fmt.Sprintf() with
  templ's expression attribute syntax ={ }.
- Pass fileUrl from server so foliate-js can open books directly.
- Redesign bottom bar with foliate-js parity: left/right navigation
  buttons, progress slider with tick marks, and zoom controls
  (zoom out, percentage display, zoom in, magnifier, pan/select
  mode toggle for PDFs).
- Remove panel editor button and enablePanelDetection config.
- Add SVG icon styles for consistent reader controls.

Go types (templates/types.go):
- Expand ReaderMetadata with FormatGroup, MangaType,
  ReadingDirection, FileURL, and LibraryID fields needed by
  the reader frontend.

Router (internal/router/reader.go):
- Populate new ReaderMetadata fields from database values.
- Construct FileURL from library ID and file path for the
  /uploads/library-{id}/* file serving route.

Reader JS (reader.ts):
- Full rewrite modeled on foliate-js Reader class, adapted for
  Alpine.js. Opens books via view.open(fileUrl), accesses
  view.renderer for zoom/pan/navigation, and wires up keyboard
  shortcuts (+/-/0 for zoom, arrows for nav, Escape for magnifier).
- Uses view.isFixedLayout instead of importing FixedLayout class,
  avoiding a TypeScript module resolution issue with the Vite alias.

Settings manager (settings-manager.ts):
- Remove dependency on deleted ReaderContext event bus.
- Export loadSettings/saveSettings/syncSettings directly as
  standalone async functions.

Cleanup:
- Delete reader-context.ts and reader-events.ts (over-engineered
  event system replaced by direct function calls).
- Remove panel_zoom_enabled from ReaderSettings type.
2026-04-19 14:27:56 -04:00
john-okeefe 03f7c15445 feat(reader): Add dynamic panel detection loading in frontend
- Add enablePanelDetection, libraryType, formatGroup state
- Add panelDetector instance for dynamic loading
- Update initReader to accept and process configuration
- Conditionally load panel detection only when enabled
- Use dynamic import for foliate-js/panel-detection.js
- Add error handling for panel detection loading
2026-04-12 20:44:00 -04:00
john-okeefe c7a9098c69 feat: Replace foliate-js submodule with npm git dependency
Migrate from git submodule to npm package management for better
developer experience and simplified deployment.
Changes:
- Add @bookhoard/foliate-js from GitHub fork
(john-okeefe/foliate-js#bookhoard-panel-detection)
- Update vite alias to point to node_modules instead of vendor
- Delete .gitmodules (no submodules tracked)
- Remove scripts/setup-git-hooks.sh (no longer needed)
- Delete web/vendor/foliate-js/ submodule directory
- Remove sc-commit git alias (submodule-specific)
Benefits:
- Standard npm workflow (npm install / npm update)
- No authentication issues for end users (public GitHub)
- Simpler deployment (npm ci in containers)
- foliate-js protected in node_modules (AI won't rewrite)
- Independent project management
- Cleaner git history
Technical details:
- Import remains unchanged: import "foliate-js/view.js"
- Vite alias maps "foliate-js" to "/node_modules/@bookhoard/foliate-js"
- Build verified working (reader.js includes foliate-js)
- Package installed from git branch: bookhoard-panel-detection
2026-04-12 17:09:19 -04:00
john-okeefe 157bf734c7 feat: Create minimal reader entry point for foliate-js
Create minimal Alpine.js integration for foliate-js reader. This file
serves as the entry point that imports foliate-js and provides basic
navigation controls.

Implementation:
1. Import foliate-js/view.js:
   - Registers <foliate-view> custom element globally
   - Makes foliate reader functional when element is added to DOM
   - No explicit EPUB imports needed (foliate detects format automatically)

2. Alpine.js integration:
   - Create readerShell data object for UI state management
   - Provide nextPage() and previousPage() methods for button controls
   - Methods access <foliate-view> custom element's API (next(), prev())
   - Simple, functional approach (no OOP, follows project guidelines)

3. Init placeholder:
   - initReader() method for future initialization logic
   - Currently just logs for debugging
   - Will be extended with theme switching, progress sync, etc.

Design Choices:
- Follow project guidelines: No classes, functional/procedural style
- Use Alpine.js for UI state (consistent with rest of application)
- Defer book loading to foliate's internal format detection
- Minimal footprint: Only what's needed to make <foliate-view> work

Next Steps (Future Commits):
- Theme switching logic (apply CSS custom properties)
- Progress sync to API (listen to foliate's relocate event)
- Book loading integration (open book path, handle CFI locations)
- Settings persistence (save theme, font, spacing preferences)

File: web/src/reader/reader.ts (31 lines)
- Clean separation: Foliate handles rendering, Alpine handles UI state
- Type-safe with @ts-ignore for foliate custom element API access
2026-04-12 12:10:02 -04:00
john-okeefe aaa2dff8e2 feat: Add 18 reading themes for ebook reader
Add comprehensive reading theme system with 18 themes organized into 5 categories.
All themes include light and dark mode variants, optimized for readability and
eye comfort during long reading sessions.

Theme Categories:
1. Classic Reading (6 themes)
   - Light, Paper, Sepia, Parchment, Warm, Candlelight
   - Time-tested, comfortable for general reading

2. Sky & Atmosphere (4 themes)
   - Azure, Sky, Arctic, Frost
   - Open, airy, contemplative feel with blue tones

3. Sunset & Warmth (3 themes)
   - Dusk, Sunset, Twilight
   - Warm, energizing colors for evening reading

4. Nature & Earth (3 themes)
   - Forest, Moss, Slate
   - Grounded, natural, calming greens and grays

5. High Performance (2 themes)
   - OLED, Solarized
   - Optimized for specific use cases (battery saving, precision design)

Theme Features:
- All themes pass WCAG AAA contrast standards (7:1 ratio)
- Each theme has light and dark mode variants
- CSS custom properties for dynamic theme switching
- Optimized color temperatures for different lighting conditions
- Inspired by best practices from e-readers (Kindle, Kobo) and community projects (Grimmory)

Design Principles:
- Readability first: Avoid pure black on pure white (causes eye strain)
- Color temperature: Warm tones for evening, cool tones for daytime focus
- Typography support: Works seamlessly with 9 bundled libre fonts
- Progressive enhancement: Themes work without JavaScript

File: web/static/foliate-themes.css (301 lines)
- CSS custom properties for each theme variant
- Typography base styles (font-family, font-size, line-height, margins)
- Link, selection, and image handling styles
- Orphan/widow prevention for better text flow
2026-04-12 12:09:51 -04:00
john-okeefe 231a1c64e3 refactor: Remove broken reader implementation for foliate-js migration
Remove 60+ files from the old reader implementation that relied on
CSS columns pagination, which was fundamentally broken. This includes:
- Comic/Manga format handlers (panel detection, reading direction)
- PDF rendering, bookmarks, annotations, outlines
- Reflowable content pagination (EPUB, FB2, TXT, HTML parsers)
- UI components (gestures, keyboard shortcuts, panel dock system)
- Core navigation and state management

The old implementation used CSS columns for EPUB pagination, but this
approach is fundamentally incompatible with horizontal book layouts
because CSS columns fill vertically first, then wrap horizontally.
This causes only 1 column to be created instead of the expected 92+.

This cleanup prepares the codebase for foliate-js integration, which
uses JavaScript-driven pagination with CFI-based positioning that
actually works for book reading.

Files removed:
- formats/: comic/, manga/, pdf/, reflowable/ (65 files)
- parsers/: EPUB, FB2, TXT, HTML (4 files)
- ui/: gestures, keyboard shortcuts, panel dock, progress tracker (8 files)
- core/: parser-manager, reader-navigation, reader-services, reader-state (4 files)
- reader-shell.ts: Main reader orchestrator (565 lines)

Total: 9,361 lines removed

Reader functionality will be restored via foliate-js integration.
2026-04-12 12:09:39 -04:00
john-okeefe 87cb11cef2 Update foliate-js: Fix Vite build 2026-04-12 12:06:20 -04:00
john-okeefe 562de71a2b Add foliate-js submodule with import alias 2026-04-12 10:54:25 -04:00
john-okeefe ef15fb9ab4 Remove foliate-js submodule 2026-04-12 10:47:59 -04:00
john-okeefe 002c367710 Remove foliate-js submodule 2026-04-12 10:31:21 -04:00
john-okeefe 90c1e7b56a refactor: Improve type safety by removing 'as any' casts throughout reader code
TYPE SAFETY IMPROVEMENTS:
1. manga/reading-direction.ts
   - Create MangaMetadata interface to replace 'any' type
   - Remove redundant 'as any' casts inside detectFromMetadata()
   - Add proper typing for manga_type and reading_direction fields
   - Function signature now properly typed

2. ui/gestures.ts
   - Remove 'as any' cast for comic/manga reader
   - TypeScript already knows the type after type guard checks
   - Improves type safety and enables better autocomplete

3. ui/keyboard-shortcuts.ts
   - Remove 'as any' cast for comic/manga reader
   - Type guard on lines 22-24 narrows the type correctly
   - No cast needed, TypeScript infers ComicReader | MangaReader

4. reader-shell.ts
   - Remove 'as any' cast for comic/manga images array access
   - Type checking after format checks ensures correct type
   - Change: (state.currentReader as any).images.length
   - To: state.currentReader.images.length

BENEFITS:
 Compiler catches property name mismatches (e.g., pageCalculationResult)
 Better IDE autocomplete and inline documentation
 Prevents runtime type errors that would slip through with 'any'
 Code becomes self-documenting with explicit types
 Easier refactoring with compiler assistance

This change improves overall type safety in the reader codebase by
removing unnecessary type casts that were bypassing TypeScript's
type checking. The 'as any' casts were hiding bugs and preventing
the compiler from catching errors at compile time.

Related to: Type system improvements, bug prevention
2026-04-11 00:52:47 -04:00
john-okeefe eee83d6cb9 fix: Synchronize currentSpineIndex with position.spineIndex on position updates
CRITICAL BUG FIX:
The UniversalReader interface had a duplicate spine index issue:
- Top-level property: currentSpineIndex: number
- Nested property: position.spineIndex: number

When navigation updated the position object, only position.spineIndex
was updated, but the top-level currentSpineIndex remained unchanged.
This caused a mismatch that broke navigation across spine boundaries.

ROOT CAUSE:
- Navigation functions update position (which contains spineIndex)
- updateCurrentPosition() only spread position, not currentSpineIndex
- progress-indicator.ts reads from currentSpineIndex to get spine info
- Result: Trying to access spine 0 when actually on spine 1, etc.

THE FIX:
Add currentSpineIndex to the returned object in updateCurrentPosition():
  return {
    ...book,
    position,
    currentSpineIndex: position.spineIndex,  // Keep them in sync
  };

IMPACT:
 Navigation works correctly across spine boundaries (e.g., cover → content)
 Progress indicator displays accurate chapter information
 Content rendering uses correct spine data
 No more state corruption when navigating between spines

This fix ensures that both spine index properties stay synchronized,
preventing the navigation failures that occurred when crossing from
one spine item to another (e.g., from cover.xhtml to Frankenstein.xhtml).

Related to: Spine boundary navigation, state synchronization
2026-04-11 00:52:39 -04:00
john-okeefe 8388847ec3 fix: Correct progress indicator pagination data access and improve type safety
BUG FIXES:
1. Fix incorrect property access causing page counter to show wrong totals
   - Changed: reader.pageCalculationResult → reader.pagination
   - The pageCalculationResult property doesn't exist on UniversalReader
   - This caused fallback to metadata.total_pages (299) instead of
     calculated pagination.totalPages (119)

2. Fix non-existent chapterMap property access
   - chapterMap doesn't exist on PaginationData interface
   - Changed to use spineMap which provides the same information
   - Calculate chapter start page from spine.pages[0].pageIndex
   - Calculate chapter pages from spine.pages.length

TYPE SAFETY IMPROVEMENTS:
3. Remove 'as any' type cast for ebook reader
   - Changed: const reader = state.currentReader as any
   - To: const reader = state.currentReader
   - TypeScript already knows the type after type check

4. Remove 'as any' type casts for comic/manga readers
   - Added proper type narrowing with if statement
   - Changed: (state.currentReader as any).currentPage
   - To: reader.currentPage with type guard
   - Improves type safety and enables better IDE autocomplete

IMPACT:
 Page counter shows correct total (119 instead of 299)
 Chapter progress displays accurately
 No TypeScript errors for missing properties
 Better type safety prevents similar bugs in the future
 IDE autocomplete works correctly for reader properties

This fix resolves the pagination data access issues that caused the
page counter to display incorrect totals and improves overall type
safety in the progress indicator component.

Related to: Page counter display, type safety improvements
2026-04-11 00:52:32 -04:00