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.
This commit is contained in:
+87
-31
@@ -15,18 +15,23 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/reader.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.reader-icon {
|
||||
display: block;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#progress-slider {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
x-data="readerShell"
|
||||
x-init="initReader({
|
||||
mediaItemId: '{ readerData.MediaItemID }',
|
||||
title: '{ readerData.Title }',
|
||||
enablePanelDetection: { readerData.EnablePanelDetection },
|
||||
libraryType: '{ readerData.LibraryType }',
|
||||
formatGroup: '{ readerData.FormatGroup }',
|
||||
mangaType: '{ readerData.MangaType }',
|
||||
readingDirection: '{ readerData.ReadingDirection }'
|
||||
})"
|
||||
x-init={ fmt.Sprintf(`initReader({mediaItemId:'%s',fileUrl:'%s',formatGroup:'%s',readingDirection:'%s',mangaType:'%s'})`, metadata.MediaItemID, metadata.FileURL, metadata.FormatGroup, metadata.ReadingDirection, metadata.MangaType) }
|
||||
class="theme-tokyo-night"
|
||||
>
|
||||
@ReaderChrome(user, metadata, progress)
|
||||
@@ -77,25 +82,80 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
</div>
|
||||
<!-- Bottom bar -->
|
||||
<div class="fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40" style="background-color: var(--bg-primary);">
|
||||
<div class="flex items-center justify-between px-4 py-3">
|
||||
<div id="progress-display" data-progress-mode="pages">
|
||||
<div class="flex items-center px-2 py-2 gap-1">
|
||||
<!-- Left navigation -->
|
||||
<button @click="goLeft()" class="p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Progress slider -->
|
||||
<input
|
||||
id="progress-slider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
list="tick-marks"
|
||||
@input="goToFraction($event.target.value)"
|
||||
class="flex-grow"
|
||||
/>
|
||||
<datalist id="tick-marks"></datalist>
|
||||
<!-- Right navigation -->
|
||||
<button @click="goRight()" class="p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||
<!-- Zoom controls (fixed-layout only) -->
|
||||
<template x-if="isFixedLayout">
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Zoom out -->
|
||||
<button @click="zoomOut()" class="p-2 rounded-lg hover:bg-gray-700" title="Zoom Out" aria-label="Zoom out">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Zoom percentage -->
|
||||
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom" aria-label="Reset zoom">
|
||||
<span x-text="zoomPercent + '%'">100%</span>
|
||||
</button>
|
||||
<!-- Zoom in -->
|
||||
<button @click="zoomIn()" class="p-2 rounded-lg hover:bg-gray-700" title="Zoom In" aria-label="Zoom in">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Magnifier -->
|
||||
<button @click="toggleMagnifier()" class="p-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5"></circle>
|
||||
<path d="M 13 13 L 18 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Pan/Select mode (PDF only) -->
|
||||
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-2 rounded-lg hover:bg-gray-700" title="Pan/Select Mode" aria-label="Toggle pan/select mode">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||
<!-- Progress display -->
|
||||
<div id="progress-display" x-text="progressText" data-progress-mode="pages" class="text-sm min-w-[4rem] text-center">
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button data-action="toggle-toc" title="Table of Contents">📖</button>
|
||||
<button data-action="add-bookmark" title="Bookmark">🏷️</button>
|
||||
<button data-action="add-note" title="Note">📝</button>
|
||||
<!-- Panel editor - shown for comics/manga -->
|
||||
<button
|
||||
x-data="panelEditor"
|
||||
x-show="isComicOrManga"
|
||||
@click="openPanelEditor(readerShell.currentPage)"
|
||||
data-action="edit-panels"
|
||||
title="Edit Panels"
|
||||
class="p-2 rounded-lg hover:bg-gray-700"
|
||||
>
|
||||
🎨
|
||||
</button>
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button data-action="toggle-toc" class="p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
||||
<button data-action="add-bookmark" class="p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
|
||||
<button data-action="add-note" class="p-2 rounded-lg hover:bg-gray-700" title="Note">📝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +249,7 @@ templ ReaderSettingsPanel() {
|
||||
<option value="charis-sil">Charis SIL (Multilingual)</option>
|
||||
<option value="ibm-plex">IBM Plex Serif (Modern)</option>
|
||||
</select>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">8 libre fonts bundled with Bookhoard</p>
|
||||
<p class="text-xs ml-2" style="color: var(--text-secondary)">8 libre fonts bundled with Bookhoard</p>
|
||||
</label>
|
||||
<label class="block mb-2">
|
||||
Font Size
|
||||
@@ -205,10 +265,6 @@ templ ReaderSettingsPanel() {
|
||||
<!-- Navigation -->
|
||||
<div class="mb-6">
|
||||
<h3 class="font-semibold mb-2">Navigation</h3>
|
||||
<label class="flex items-center mb-2">
|
||||
<input type="checkbox" name="panel_zoom_enabled" class="mr-2"/>
|
||||
Panel Zoom (Comics/Manga)
|
||||
</label>
|
||||
<label class="flex items-center mb-2">
|
||||
<input type="checkbox" name="double_page_spread" class="mr-2"/>
|
||||
Double Page Spread (Comics/Manga)
|
||||
|
||||
Reference in New Issue
Block a user