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
This commit is contained in:
2026-04-19 18:17:42 -04:00
parent 47e0e96ab8
commit 0589157b57
5 changed files with 224 additions and 95 deletions
+62 -70
View File
@@ -15,16 +15,6 @@ 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;
}
</style>
</head>
<body
x-data="readerShell"
@@ -36,19 +26,19 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
<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">
<div id="toc-panel" x-show="tocOpen" x-transition 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">
<div id="settings-panel" x-show="settingsOpen" x-transition 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">
<div id="navigator-panel" x-show="navigatorOpen" x-transition class="panel-container pointer-events-auto" data-panel="navigator">
@ReaderNavigatorPanel()
</div>
<div id="bookmarks-panel" class="panel-container pointer-events-auto" data-panel="bookmarks">
<div id="bookmarks-panel" x-show="bookmarksOpen" x-transition class="panel-container pointer-events-auto" data-panel="bookmarks">
@ReaderBookmarksPanel(bookmarks)
</div>
</div>
@@ -69,7 +59,7 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
</a>
<h1 class="text-lg font-semibold">{ metadata.Title }</h1>
<button
data-action="open-settings"
@click="toggleSettings()"
class="p-2 rounded-lg hover:bg-gray-700"
title="Settings"
>
@@ -150,9 +140,9 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
<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>
<button @click="toggleTOC()" class="p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
<button @click="addBookmark()" class="p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
<button @click="toggleBookmarks()" class="p-2 rounded-lg hover:bg-gray-700" title="Notes">📝</button>
</div>
</div>
</div>
@@ -161,16 +151,15 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
templ ReaderSettingsPanel() {
<div
id="settings-panel"
class="dockable-panel panel-collapsed"
class="dockable-panel"
data-panel="settings"
data-side="left"
x-ref="settingsPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.settingsPanel)">
<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>
<button class="window-shade-toggle"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
@@ -195,12 +184,12 @@ templ ReaderSettingsPanel() {
</select>
</label>
</div>
<!-- Typography (ebooks only) -->
<div class="mb-6" data-visible-for="ebook">
<!-- Reading Theme (reflowable only) -->
<div class="mb-6" x-show="!isFixedLayout">
<h3 class="font-semibold mb-2">Reading Theme</h3>
<label class="block mb-2">
<select name="reading_theme" class="w-full mt-1 px-3 py-2 rounded border">
<optgroup label="📖 Classic Reading">
<select name="reading_theme" x-model="readingTheme" @change="applyTheme()" class="w-full mt-1 px-3 py-2 rounded border">
<optgroup label="Classic Reading">
<option value="light">Light</option>
<option value="paper">Paper</option>
<option value="sepia">Sepia</option>
@@ -208,89 +197,95 @@ templ ReaderSettingsPanel() {
<option value="warm">Warm</option>
<option value="candlelight">Candlelight</option>
</optgroup>
<optgroup label="🌤️ Sky & Atmosphere">
<optgroup label="Sky and Atmosphere">
<option value="azure">Azure</option>
<option value="sky">Sky</option>
<option value="arctic">Arctic</option>
<option value="frost">Frost</option>
</optgroup>
<optgroup label="🌅 Sunset & Warmth">
<optgroup label="Sunset and Warmth">
<option value="dusk">Dusk</option>
<option value="sunset">Sunset</option>
<option value="twilight">Twilight</option>
</optgroup>
<optgroup label="🌲 Nature & Earth">
<optgroup label="Nature and Earth">
<option value="forest">Forest</option>
<option value="moss">Moss</option>
<option value="slate">Slate</option>
</optgroup>
<optgroup label="High Performance">
<optgroup label="High Performance">
<option value="oled">OLED</option>
<option value="solarized">Solarized</option>
</optgroup>
</select>
<p class="text-xs mt-1" style="color: var(--text-secondary)">18 themes organized by category</p>
</label>
</div>
<div class="mb-6" data-visible-for="ebook">
<!-- Typography (reflowable only) -->
<div class="mb-6" x-show="!isFixedLayout">
<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 name="reading_font" x-model="readingFont" @change="applyFont()" class="w-full mt-1 px-3 py-2 rounded border">
<option value="literata">Literata (Default)</option>
<option value="crimson">Crimson Text</option>
<option value="source-serif">Source Serif 4</option>
<option value="eb-garamond">EB Garamond</option>
<option value="libertinus">Libertinus Serif</option>
<option value="noto-serif">Noto Serif</option>
<option value="charis-sil">Charis SIL</option>
<option value="ibm-plex">IBM Plex Serif</option>
</select>
<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
<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>
<input type="range" name="font_size" x-model.number="fontSize" @change="applyFont()" min="12" max="24" class="w-full"/>
<span class="text-xs" x-text="fontSize + 'px'" style="color: var(--text-secondary)">16px</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>
<input type="range" name="line_height" x-model.number="lineHeight" @change="applyFont()" min="1.0" max="2.5" step="0.1" class="w-full"/>
<span class="text-xs" x-text="lineHeight" style="color: var(--text-secondary)">1.6</span>
</label>
</div>
<!-- Navigation -->
<div class="mb-6">
<div class="mb-6" x-show="isFixedLayout">
<h3 class="font-semibold mb-2">Navigation</h3>
<label class="flex items-center mb-2">
<input type="checkbox" name="double_page_spread" class="mr-2"/>
Double Page Spread (Comics/Manga)
Double Page Spread
</label>
</div>
<button data-action="close-settings" class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
<button @click="toggleSettings()" class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Done
</button>
</div>
</div>
}
templ ReaderTOCPanel(metadata ReaderMetadata) {
templ ReaderTOCPanel(_ ReaderMetadata) {
<div
id="toc-panel"
class="dockable-panel"
data-panel="toc"
data-side="left"
x-ref="tocPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.tocPanel)">
<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>
<button class="window-shade-toggle"></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 id="toc-list" class="space-y-1">
<template x-for="item in tocItems" :key="item.href">
<a
href="#"
@click.prevent="goToTOCItem(item)"
class="block py-1 px-2 rounded hover:bg-gray-700 text-sm"
:style="'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'"
x-text="item.label"
></a>
</template>
</nav>
</div>
</div>
@@ -298,22 +293,19 @@ templ ReaderTOCPanel(metadata ReaderMetadata) {
templ ReaderNavigatorPanel() {
<div
id="navigator-panel"
class="dockable-panel"
data-panel="navigator"
data-side="right"
x-ref="navigatorPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.navigatorPanel)">
<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>
<button class="window-shade-toggle"></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>
@@ -321,16 +313,15 @@ templ ReaderNavigatorPanel() {
templ ReaderBookmarksPanel(bookmarks []Bookmark) {
<div
id="bookmarks-panel"
class="dockable-panel panel-collapsed"
class="dockable-panel"
data-panel="bookmarks"
data-side="right"
x-ref="bookmarksPanel"
>
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" data-action="toggle-panel">
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.bookmarksPanel)">
<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>
<button class="window-shade-toggle"></button>
</div>
</div>
<div class="panel-content p-4 overflow-y-auto">
@@ -339,7 +330,8 @@ templ ReaderBookmarksPanel(bookmarks []Bookmark) {
for _, bookmark := range bookmarks {
<a
href="#"
data-bookmark-id={ bookmark.ID }
@click.prevent="goToBookmarkTarget($el.dataset.cfi)"
data-cfi={ bookmark.CfiPosition }
class="block py-2 hover:bg-gray-700 rounded px-2"
>
<span class="font-medium">{ bookmark.Title }</span>
@@ -352,7 +344,7 @@ templ ReaderBookmarksPanel(bookmarks []Bookmark) {
} 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">
<button @click="addBookmark()" class="w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700">
+ Add Bookmark
</button>
</div>
+21 -21
View File
@@ -44,14 +44,14 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Reader</title><link rel=\"manifest\" href=\"/static/manifest.json\"><link href=\"/static/reader-fonts.css\" rel=\"stylesheet\"><link href=\"/static/foliate-themes.css\" rel=\"stylesheet\"><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/reader.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><style>\n\t\t\t\t.reader-icon {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tfill: none;\n\t\t\t\t\tstroke: currentColor;\n\t\t\t\t\tstroke-width: 2px;\n\t\t\t\t\tstroke-linecap: round;\n\t\t\t\t\tstroke-linejoin: round;\n\t\t\t\t}\n\t\t\t</style></head><body x-data=\"readerShell\" x-init=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Reader</title><link rel=\"manifest\" href=\"/static/manifest.json\"><link href=\"/static/reader-fonts.css\" rel=\"stylesheet\"><link href=\"/static/foliate-themes.css\" rel=\"stylesheet\"><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/reader.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"readerShell\" x-init=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`initReader({mediaItemId:'%s',fileUrl:'%s',formatGroup:'%s',readingDirection:'%s',mangaType:'%s'})`, metadata.MediaItemID, metadata.FileURL, metadata.FormatGroup, metadata.ReadingDirection, metadata.MangaType))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 31, Col: 233}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 21, Col: 233}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -65,7 +65,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- 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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- 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\" x-show=\"tocOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"toc\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -73,7 +73,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div id=\"settings-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"settings\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div id=\"settings-panel\" x-show=\"settingsOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"settings\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -81,7 +81,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</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\" x-show=\"navigatorOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"navigator\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -89,7 +89,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div id=\"bookmarks-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"bookmarks\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div id=\"bookmarks-panel\" x-show=\"bookmarksOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"bookmarks\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -141,26 +141,26 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 70, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 60, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</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 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=\"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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h1><button @click=\"toggleSettings()\" 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 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=\"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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 147, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 137, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><!-- 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></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Action buttons --><div class=\"flex items-center gap-1\"><button @click=\"toggleTOC()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents\">📖</button> <button @click=\"addBookmark()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Notes\">📝</button></div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -189,7 +189,7 @@ func ReaderSettingsPanel() templ.Component {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<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\">Reading Theme</h3><label class=\"block mb-2\"><select name=\"reading_theme\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">18 themes organized by category</p></label></div><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 ml-2\" 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=\"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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"dockable-panel\" data-panel=\"settings\" data-side=\"left\" x-ref=\"settingsPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.settingsPanel)\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</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><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Reading Theme</h3><label class=\"block mb-2\"><select name=\"reading_theme\" x-model=\"readingTheme\" @change=\"applyTheme()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"Sky and Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"Sunset and Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"Nature and Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select></label></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"12\" max=\"24\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">16px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"1.0\" max=\"2.5\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Navigation --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread</label></div><button @click=\"toggleSettings()\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -197,7 +197,7 @@ func ReaderSettingsPanel() templ.Component {
})
}
func ReaderTOCPanel(metadata ReaderMetadata) templ.Component {
func ReaderTOCPanel(_ ReaderMetadata) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -218,7 +218,7 @@ func ReaderTOCPanel(metadata ReaderMetadata) templ.Component {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"dockable-panel\" data-panel=\"toc\" data-side=\"left\" x-ref=\"tocPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.tocPanel)\"><h3 class=\"panel-title font-semibold\">📖 Table of Contents</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><nav id=\"toc-list\" class=\"space-y-1\"><template x-for=\"item in tocItems\" :key=\"item.href\"><a href=\"#\" @click.prevent=\"goToTOCItem(item)\" class=\"block py-1 px-2 rounded hover:bg-gray-700 text-sm\" :style=\"'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'\" x-text=\"item.label\"></a></template></nav></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -247,7 +247,7 @@ func ReaderNavigatorPanel() templ.Component {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"dockable-panel\" data-panel=\"navigator\" data-side=\"right\" x-ref=\"navigatorPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.navigatorPanel)\"><h3 class=\"panel-title font-semibold\">🗺️ Navigator</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-2 overflow-hidden\"><div id=\"navigator-viewport\" class=\"relative w-full h-full\"></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -276,7 +276,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"dockable-panel\" data-panel=\"bookmarks\" data-side=\"right\" x-ref=\"bookmarksPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.bookmarksPanel)\"><h3 class=\"panel-title font-semibold\">🔖 Bookmarks</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -286,14 +286,14 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
return templ_7745c5c3_Err
}
for _, bookmark := range bookmarks {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a href=\"#\" data-bookmark-id=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a href=\"#\" @click.prevent=\"goToBookmarkTarget($el.dataset.cfi)\" data-cfi=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.ID)
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 342, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 334, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -306,7 +306,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 345, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 337, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -319,7 +319,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 347, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 339, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -340,7 +340,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<button @click=\"addBookmark()\" class=\"w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Bookmark</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+99 -3
View File
@@ -1,6 +1,6 @@
import "foliate-js/view.js";
import { Alpine } from "../alpine";
import { loadSettings } from "./settings-manager";
import { loadSettings, saveSettings } from "./settings-manager";
import { getToken } from "../storage";
const getCSS = ({
spacing,
@@ -79,6 +79,15 @@ document.addEventListener("alpine:init", () => {
justify: true,
hyphenate: true,
},
tocOpen: false,
settingsOpen: false,
bookmarksOpen: false,
navigatorOpen: false,
tocItems: [] as any[],
readingTheme: "dark" as string,
readingFont: "literata" as string,
fontSize: 16 as number,
lineHeight: 1.6 as number,
async initReader(config: {
mediaItemId: string;
fileUrl: string;
@@ -87,8 +96,14 @@ document.addEventListener("alpine:init", () => {
mangaType: string;
}) {
this.settings = await loadSettings();
if (this.settings?.reading_theme) {
document.body.classList.add(`theme-${this.settings.reading_theme}`);
if (this.settings) {
this.readingTheme = this.settings.reading_theme || "dark";
this.readingFont = this.settings.reading_font || "literata";
this.fontSize = this.settings.font_size || 16;
this.lineHeight = this.settings.line_height || 1.6;
if (this.settings.reading_theme) {
document.body.classList.add(`theme-${this.settings.reading_theme}`);
}
}
this.view = document.getElementById("reader-view") as any;
const resp = await fetch(config.fileUrl, {
@@ -200,6 +215,87 @@ document.addEventListener("alpine:init", () => {
goToFraction(value: string) {
this.view?.goToFraction?.(parseFloat(value));
},
toggleTOC() {
this.tocOpen = !this.tocOpen;
if (this.tocOpen && this.tocItems.length === 0 && this.book?.toc) {
this.tocItems = this.flattenTOC(this.book.toc);
}
},
toggleSettings() {
this.settingsOpen = !this.settingsOpen;
},
toggleBookmarks() {
this.bookmarksOpen = !this.bookmarksOpen;
},
toggleNavigator() {
this.navigatorOpen = !this.navigatorOpen;
},
toggleWindowShade(panelEl: HTMLElement) {
panelEl.classList.toggle("panel-collapsed");
},
flattenTOC(items: any[], depth = 0): any[] {
const result: any[] = [];
for (const item of items) {
result.push({ ...item, depth });
if (item.subitems?.length) {
result.push(...this.flattenTOC(item.subitems, depth + 1));
}
}
return result;
},
goToTOCItem(item: any) {
if (this.view && item.href) {
this.view.goTo(item.href);
this.tocOpen = false;
}
},
goToBookmarkTarget(cfi: string) {
if (this.view && cfi) {
this.view.goTo(cfi);
this.bookmarksOpen = false;
}
},
applyTheme() {
document.body.className = document.body.className
.replace(/theme-(?!tokyo|dracula|nord|solarized|monokai|one-dark|material|catppuccin)[\w-]+/g, "")
.trim();
document.body.classList.add(`theme-${this.readingTheme}`);
this.applyStyles();
saveSettings({ reading_theme: this.readingTheme });
},
applyFont() {
this.applyStyles();
saveSettings({
reading_font: this.readingFont,
font_size: this.fontSize,
line_height: this.lineHeight,
});
},
applyStyles() {
if (!this.renderer?.setStyles) return;
this.renderer.setStyles(getCSS(this.style));
},
async addBookmark() {
const token = getToken();
if (!token || !this.view) return;
const location = this.view.lastLocation;
if (!location) return;
try {
await fetch("/readers/bookmarks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `Bookmark at ${this.progressText}`,
position: JSON.stringify(location),
}),
});
} catch (_e) {
/* ignore bookmark errors for now */
}
},
handleKeydown(event: KeyboardEvent) {
const k = event.key;
if (k === "ArrowLeft" || k === "h") this.goLeft();
+41
View File
@@ -357,4 +357,45 @@
#collections-container[data-wood] .book-card {
border: 1px solid var(--wood-border);
}
.reader-icon {
display: block;
fill: none;
stroke: currentColor;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
.dockable-panel {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border);
transition: max-height 0.2s ease-out;
overflow: hidden;
}
.dockable-panel.panel-collapsed .panel-content {
display: none;
}
.panel-header {
user-select: none;
}
.panel-header:hover {
background-color: color-mix(in srgb, var(--bg-secondary) 90%, white);
}
.panel-container {
background-color: var(--bg-secondary);
border-right: 1px solid var(--border);
width: 320px;
max-height: calc(100vh - 8rem);
overflow-y: auto;
}
.panel-container[data-side="right"] {
border-right: none;
border-left: 1px solid var(--border);
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long