Files
bookhoard/templates/reader.templ
T
john-okeefe 7fd8ccb129 feat(reader): add reader JavaScript loading and backend support
- templates/reader.templ: add script tag to load main.js
- Enables reader shell and Alpine components to initialize
- internal/handlers/reader.go: add GetMediaReadingProgress handler
- Backend now supports fetching reading progress via API
- GET /api/media-items/:id/progress returns current progress
2026-04-04 13:38:23 -04:00

272 lines
10 KiB
Templ

package templates
import "fmt"
templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookmarks []Bookmark) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ metadata.Title } - Bookhoard Reader</title>
<link rel="manifest" href="/static/manifest.json"/>
<link href="/static/reader-fonts.css" rel="stylesheet"/>
<script src="/static/htmx.min.js"></script>
<script src="/static/main.js" defer></script>
<link href="/static/style.css" rel="stylesheet"/>
</head>
<body
x-data="readerShell"
x-init="initReader()"
class="theme-{ user.Theme }"
data-media-item-id={ metadata.MediaItemID }
data-media-type={ metadata.LibraryType }
data-total-pages={ metadata.TotalPages }
>
@ReaderChrome(user, metadata, progress)
<!-- Dockable Panels Container -->
<div id="reader-panels" class="fixed inset-0 pointer-events-none z-30">
<!-- Left Sidebar (TOC, Settings) -->
<div id="left-sidebar" class="absolute left-0 top-0 bottom-0 pointer-events-auto flex flex-col">
<div id="toc-panel" class="panel-container pointer-events-auto" data-panel="toc">
@ReaderTOCPanel(metadata)
</div>
<div id="settings-panel" class="panel-container pointer-events-auto" data-panel="settings">
@ReaderSettingsPanel()
</div>
</div>
<!-- Right Sidebar (Navigator, Bookmarks) -->
<div id="right-sidebar" class="absolute right-0 top-0 bottom-0 pointer-events-auto flex flex-col">
<div id="navigator-panel" class="panel-container pointer-events-auto" data-panel="navigator">
@ReaderNavigatorPanel()
</div>
<div id="bookmarks-panel" class="panel-container pointer-events-auto" data-panel="bookmarks">
@ReaderBookmarksPanel(bookmarks)
</div>
</div>
</div>
<main id="reader-content" class="w-full h-full">
<!-- Content loaded by JavaScript based on media type -->
</main>
@DictionaryPopup()
</body>
</html>
}
templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) {
<div id="reader-chrome" class="transition-opacity duration-300">
<!-- Top bar -->
<div class="fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40" style="background-color: var(--bg-primary);">
<div class="flex items-center justify-between px-4 py-3">
<a href="/media-items/{ metadata.MediaItemID }" class="text-lg hover:underline">
Back
</a>
<h1 class="text-lg font-semibold">{ metadata.Title }</h1>
<button
data-action="open-settings"
class="p-2 rounded-lg hover:bg-gray-700"
title="Settings"
>
⚙️
</button>
</div>
</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">
{ 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>
</div>
</div>
</div>
</div>
}
templ ReaderSettingsPanel() {
<div
id="settings-panel"
class="dockable-panel panel-collapsed"
data-panel="settings"
data-side="left"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<h3 class="panel-title font-semibold">⚙️ Settings</h3>
<div class="panel-controls flex items-center gap-2">
<button class="panel-lock" data-action="lock-panel" title="Lock position">🔓</button>
<button class="window-shade-toggle" data-action="window-shade"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
<!-- Display settings -->
<div class="mb-6">
<h3 class="font-semibold mb-2">Display</h3>
<label class="block mb-2">
Chrome Behavior
<select name="chrome_behavior" class="w-full mt-1 px-3 py-2 rounded border">
<option value="auto-hide">Auto Hide</option>
<option value="always-visible">Always Visible</option>
<option value="hide-on-scroll">Hide on Scroll</option>
</select>
</label>
<label class="block mb-2">
Progress Mode
<select name="progress_mode" class="w-full mt-1 px-3 py-2 rounded border">
<option value="pages">Pages</option>
<option value="chapter">Chapter</option>
<option value="percentage">Percentage</option>
<option value="time-left">Time Left</option>
</select>
</label>
</div>
<!-- Typography (ebooks only) -->
<div class="mb-6" data-visible-for="ebook">
<h3 class="font-semibold mb-2">Typography</h3>
<label class="block mb-2">
Reading Font
<select name="reading_font" class="w-full mt-1 px-3 py-2 rounded border">
<option value="literata">Literata (Default - Designed for ebooks)</option>
<option value="crimson">Crimson Text (Screen-optimized)</option>
<option value="source-serif">Source Serif 4 (Adobe quality)</option>
<option value="eb-garamond">EB Garamond (Classic)</option>
<option value="libertinus">Libertinus Serif (Technical)</option>
<option value="noto-serif">Noto Serif (All languages)</option>
<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>
</label>
<label class="block mb-2">
Font Size
<input type="range" name="font_size" min="12" max="24" value="16" class="w-full"/>
<span class="text-xs ml-2" style="color: var(--text-secondary)">12-24px</span>
</label>
<label class="block mb-2">
Line Height
<input type="range" name="line_height" min="1.0" max="2.5" step="0.1" value="1.6" class="w-full"/>
<span class="text-xs ml-2" style="color: var(--text-secondary)">1.0-2.5</span>
</label>
</div>
<!-- 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)
</label>
</div>
<button data-action="close-settings" class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Done
</button>
</div>
</div>
}
templ ReaderTOCPanel(metadata ReaderMetadata) {
<div
id="toc-panel"
class="dockable-panel"
data-panel="toc"
data-side="left"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<h3 class="panel-title font-semibold">📖 Table of Contents</h3>
<div class="panel-controls flex items-center gap-2">
<button class="panel-lock" data-action="lock-panel" title="Lock position">🔓</button>
<button class="window-shade-toggle" data-action="window-shade"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
<nav id="toc-list" class="space-y-2">
<!-- TOC items populated by JavaScript -->
</nav>
</div>
</div>
}
templ ReaderNavigatorPanel() {
<div
id="navigator-panel"
class="dockable-panel"
data-panel="navigator"
data-side="right"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<h3 class="panel-title font-semibold">🗺️ Navigator</h3>
<div class="panel-controls flex items-center gap-2">
<button class="panel-lock" data-action="lock-panel" title="Lock position">🔓</button>
<button class="window-shade-toggle" data-action="window-shade"></button>
</div>
</div>
<div class="panel-content p-2 overflow-hidden">
<div id="navigator-viewport" class="relative w-full h-full">
<!-- Full page preview with draggable viewport box -->
<!-- JavaScript renders current page as scaled thumbnail with draggable viewport -->
</div>
</div>
</div>
}
templ ReaderBookmarksPanel(bookmarks []Bookmark) {
<div
id="bookmarks-panel"
class="dockable-panel panel-collapsed"
data-panel="bookmarks"
data-side="right"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<h3 class="panel-title font-semibold">🔖 Bookmarks</h3>
<div class="panel-controls flex items-center gap-2">
<button class="panel-lock" data-action="lock-panel" title="Lock position">🔓</button>
<button class="window-shade-toggle" data-action="window-shade"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
if len(bookmarks) > 0 {
<div id="bookmarks-list" class="space-y-2">
for _, bookmark := range bookmarks {
<a
href="#"
data-bookmark-id={ bookmark.ID }
class="block py-2 hover:bg-gray-700 rounded px-2"
>
<span class="font-medium">{ bookmark.Title }</span>
<span class="text-xs ml-2" style="color: var(--text-secondary)">
{ bookmark.Position }
</span>
</a>
}
</div>
} else {
<p class="text-sm" style="color: var(--text-secondary)">No bookmarks yet</p>
}
<button data-action="add-bookmark" class="w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700">
+ Add Bookmark
</button>
</div>
</div>
}
templ DictionaryPopup() {
<div id="dictionary-popup" class="hidden fixed bg-white text-black p-4 rounded-lg shadow-xl max-w-md z-50"></div>
}