feat(reader): in-book search for reflowable formats
Wires foliate's search engine into the new drawer system:
- 🔍 topbar button (reflowable-only; PDF/comic sections have no
searchable text documents) and the '/' keyboard shortcut open a
Search drawer: query input (Enter to run), live progress while
scanning (per-section percent), match count, and results grouped
by section with TOC labels.
- Each result shows pre/match/post excerpt rendered as three text
nodes (no x-html — book content never enters the DOM as markup);
the match is styled with a translucent <mark>. Clicking jumps to
the hit's CFI and closes the drawer.
- Hits are drawn on the page through foliate's overlayer (outline
style) and persist across page turns — the engine re-applies
search results when a section's overlay is created. Clearing the
query removes the outlines.
- A generation counter discards results and progress from superseded
searches (rapid re-query), and starting a new search clears the
previous one server-side via view.clearSearch().
- Search integrates with the drawer system: scrim, Esc-to-close,
one-drawer-at-a-time, / focuses the input via .
This commit is contained in:
+66
-1
@@ -77,7 +77,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
|||||||
|
|
||||||
<!-- Drawer scrim -->
|
<!-- Drawer scrim -->
|
||||||
<div
|
<div
|
||||||
x-show="tocOpen || settingsOpen || bookmarksOpen"
|
x-show="tocOpen || settingsOpen || bookmarksOpen || searchOpen"
|
||||||
x-transition.opacity.duration.200ms
|
x-transition.opacity.duration.200ms
|
||||||
@click="closeDrawers()"
|
@click="closeDrawers()"
|
||||||
class="drawer-scrim"
|
class="drawer-scrim"
|
||||||
@@ -214,6 +214,23 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
|||||||
>
|
>
|
||||||
@ReaderAnnotationsDrawer()
|
@ReaderAnnotationsDrawer()
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search drawer (right, reflowable only) -->
|
||||||
|
<div
|
||||||
|
id="search-drawer"
|
||||||
|
x-show="searchOpen"
|
||||||
|
x-transition:enter="transition-transform duration-200 ease-out"
|
||||||
|
x-transition:enter-start="translate-x-full"
|
||||||
|
x-transition:enter-end="translate-x-0"
|
||||||
|
x-transition:leave="transition-transform duration-150 ease-in"
|
||||||
|
x-transition:leave-start="translate-x-0"
|
||||||
|
x-transition:leave-end="translate-x-full"
|
||||||
|
class="reader-drawer right"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Search"
|
||||||
|
>
|
||||||
|
@ReaderSearchDrawer()
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
@@ -229,6 +246,7 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
|||||||
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
|
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
|
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
|
||||||
|
<button x-show="!isFixedLayout" @click="toggleSearch()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Search (/)">🔍</button>
|
||||||
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Annotations">📝</button>
|
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Annotations">📝</button>
|
||||||
<button @click="toggleSettings()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Settings (s)">Aa</button>
|
<button @click="toggleSettings()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Settings (s)">Aa</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -491,6 +509,53 @@ templ ReaderAnnotationsDrawer() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ ReaderSearchDrawer() {
|
||||||
|
@drawerHeader("Search")
|
||||||
|
<div class="reader-search-bar">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
x-ref="searchInput"
|
||||||
|
x-model="searchQuery"
|
||||||
|
@keydown.enter.prevent="runSearch()"
|
||||||
|
placeholder="Search in book…"
|
||||||
|
class="reader-note-input grow"
|
||||||
|
aria-label="Search query"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
x-show="searchQuery || searchGroups.length"
|
||||||
|
@click="searchQuery = ''; clearSearchResults()"
|
||||||
|
class="p-2 rounded-lg hover:bg-gray-700 shrink-0"
|
||||||
|
title="Clear results"
|
||||||
|
aria-label="Clear results"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="reader-search-status" x-show="searchQuery || searching || searchGroups.length">
|
||||||
|
<span x-show="searching" x-text="'Searching… ' + Math.round(searchProgress * 100) + '%'">Searching…</span>
|
||||||
|
<span x-show="!searching && searchGroups.length" x-text="searchMatchCount + ' match' + (searchMatchCount === 1 ? '' : 'es')">0 matches</span>
|
||||||
|
<span x-show="!searching && searchQuery && !searchGroups.length">No matches</span>
|
||||||
|
</div>
|
||||||
|
<div class="reader-drawer-body">
|
||||||
|
<template x-for="(group, gi) in searchGroups" :key="gi">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary)" x-text="group.label"></div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<template x-for="item in group.items" :key="item.cfi">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
@click.prevent="goToSearchResult(item.cfi)"
|
||||||
|
class="search-result"
|
||||||
|
>
|
||||||
|
<span x-text="item.pre"></span><mark x-text="item.match"></mark><span x-text="item.post"></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ ReaderSettingsDrawer() {
|
templ ReaderSettingsDrawer() {
|
||||||
@drawerHeader("Settings")
|
@drawerHeader("Settings")
|
||||||
<div class="reader-drawer-body">
|
<div class="reader-drawer-body">
|
||||||
|
|||||||
+64
-23
@@ -133,7 +133,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Drawer scrim --><div x-show=\"tocOpen || settingsOpen || bookmarksOpen\" x-transition.opacity.duration.200ms @click=\"closeDrawers()\" class=\"drawer-scrim\" aria-hidden=\"true\"></div><!-- TOC drawer (left) --><div id=\"toc-drawer\" x-show=\"tocOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"-translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"-translate-x-full\" class=\"reader-drawer left\" role=\"dialog\" aria-label=\"Table of contents\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Drawer scrim --><div x-show=\"tocOpen || settingsOpen || bookmarksOpen || searchOpen\" x-transition.opacity.duration.200ms @click=\"closeDrawers()\" class=\"drawer-scrim\" aria-hidden=\"true\"></div><!-- TOC drawer (left) --><div id=\"toc-drawer\" x-show=\"tocOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"-translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"-translate-x-full\" class=\"reader-drawer left\" role=\"dialog\" aria-label=\"Table of contents\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,15 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></body></html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><!-- Search drawer (right, reflowable only) --><div id=\"search-drawer\" x-show=\"searchOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Search\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ReaderSearchDrawer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></body></html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -186,33 +194,33 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
|
|||||||
templ_7745c5c3_Var6 = templ.NopComponent
|
templ_7745c5c3_Var6 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div id=\"reader-chrome\" class=\"transition-opacity duration-300\" :class=\"chromeVisible ? 'opacity-100' : 'chrome-hidden opacity-0 pointer-events-none'\"><!-- Top bar --><div id=\"reader-topbar\" class=\"fixed top-0 left-0 right-0 border-b z-40 pt-[env(safe-area-inset-top)] reader-glass\"><div class=\"flex items-center justify-between px-3 py-2 sm:px-4\"><a id=\"reader-back\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div id=\"reader-chrome\" class=\"transition-opacity duration-300\" :class=\"chromeVisible ? 'opacity-100' : 'chrome-hidden opacity-0 pointer-events-none'\"><!-- Top bar --><div id=\"reader-topbar\" class=\"fixed top-0 left-0 right-0 border-b z-40 pt-[env(safe-area-inset-top)] reader-glass\"><div class=\"flex items-center justify-between px-3 py-2 sm:px-4\"><a id=\"reader-back\" href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var7 templ.SafeURL
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 226, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 243, Col: 63}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"text-base sm:text-lg hover:underline\">← Back</a><h1 class=\"text-base sm:text-lg font-semibold hidden sm:block sm:truncate\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"text-base sm:text-lg hover:underline\">← Back</a><h1 class=\"text-base sm:text-lg font-semibold hidden sm:block sm:truncate\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 229, Col: 95}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 246, Col: 95}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h1><div class=\"flex items-center gap-1\"><button @click=\"addBookmark()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark this position (b)\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Annotations\">📝</button> <button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings (s)\">Aa</button></div></div></div><!-- Bottom bar --><div id=\"reader-bottombar\" class=\"fixed bottom-0 left-0 right-0 border-t z-40 pb-[env(safe-area-inset-bottom)] reader-glass\"><!-- Reflowable row --><div x-show=\"!isFixedLayout\" class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1\"><button @click=\"goLeft()\" class=\"p-1.5 sm: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> <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> <button @click=\"goRight()\" class=\"p-1.5 sm: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><div class=\"w-px h-6 mx-1 reader-sep\"></div><div id=\"progress-display\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden\"><span class=\"hidden sm:inline\" x-text=\"progressLabel\"></span><span x-text=\"progressMain\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</h1><div class=\"flex items-center gap-1\"><button @click=\"addBookmark()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark this position (b)\">🏷️</button> <button x-show=\"!isFixedLayout\" @click=\"toggleSearch()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Search (/)\">🔍</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Annotations\">📝</button> <button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings (s)\">Aa</button></div></div></div><!-- Bottom bar --><div id=\"reader-bottombar\" class=\"fixed bottom-0 left-0 right-0 border-t z-40 pb-[env(safe-area-inset-bottom)] reader-glass\"><!-- Reflowable row --><div x-show=\"!isFixedLayout\" class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1\"><button @click=\"goLeft()\" class=\"p-1.5 sm: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> <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> <button @click=\"goRight()\" class=\"p-1.5 sm: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><div class=\"w-px h-6 mx-1 reader-sep\"></div><div id=\"progress-display\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden\"><span class=\"hidden sm:inline\" x-text=\"progressLabel\"></span><span x-text=\"progressMain\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -221,7 +229,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 267, Col: 113}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 285, Col: 113}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -231,7 +239,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 269, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 287, Col: 52}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -242,14 +250,14 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
|
|||||||
var templ_7745c5c3_Var11 string
|
var templ_7745c5c3_Var11 string
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 272, Col: 72}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 290, Col: 72}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></div><div class=\"w-px h-6 mx-1 reader-sep\"></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents (t)\">📖</button></div><!-- Fixed-layout row (comics, PDFs, manga) --><div x-show=\"isFixedLayout\" class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1 overflow-x-auto\"><button @click=\"goLeft()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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> <input id=\"progress-slider-fx\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" class=\"grow min-w-[4rem]\"> <button @click=\"goRight()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><!-- Fit mode --><select @change=\"applyFitMode($event.target.value)\" class=\"reader-select text-xs px-1.5 py-1 rounded-lg shrink-0\" title=\"Fit mode\" aria-label=\"Fit mode\"><option value=\"fit-page\">Fit Page</option> <option value=\"fit-width\">Fit Width</option></select><!-- Zoom --><button @click=\"zoomOut()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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> <button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem] shrink-0\" title=\"Reset Zoom (0)\" aria-label=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button> <button @click=\"zoomIn()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" :class=\"magnifierEnabled ? 'bg-blue-600 hover:bg-blue-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><!-- Pointer mode (PDF only): Smart / Pan / Text --><div class=\"reader-seg shrink-0\" x-show=\"isPDF\" role=\"group\" aria-label=\"Pointer mode\" title=\"Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.\"><button :class=\"interactionMode === 'select' ? 'active' : ''\" @click=\"setInteractionMode('select')\" title=\"Smart: drag text to select, drag blank to pan\">Smart</button> <button :class=\"interactionMode === 'pan' ? 'active' : ''\" @click=\"setInteractionMode('pan')\" title=\"Pan: drag anywhere to move\">Pan</button> <button :class=\"interactionMode === 'text' ? 'active' : ''\" @click=\"setInteractionMode('text')\" title=\"Text: selection only, never pan (Shift overrides)\">Text</button></div><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><!-- Double page spread --><button @click=\"toggleSpread()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" :class=\"doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''\" title=\"Double Page Spread\" aria-label=\"Toggle double page spread\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z\"></path></svg></button><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><div id=\"progress-display-fx\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden shrink-0\"><span x-text=\"progressMain\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"w-px h-6 mx-1 reader-sep\"></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents (t)\">📖</button></div><!-- Fixed-layout row (comics, PDFs, manga) --><div x-show=\"isFixedLayout\" class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1 overflow-x-auto\"><button @click=\"goLeft()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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> <input id=\"progress-slider-fx\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" class=\"grow min-w-[4rem]\"> <button @click=\"goRight()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><!-- Fit mode --><select @change=\"applyFitMode($event.target.value)\" class=\"reader-select text-xs px-1.5 py-1 rounded-lg shrink-0\" title=\"Fit mode\" aria-label=\"Fit mode\"><option value=\"fit-page\">Fit Page</option> <option value=\"fit-width\">Fit Width</option></select><!-- Zoom --><button @click=\"zoomOut()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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> <button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem] shrink-0\" title=\"Reset Zoom (0)\" aria-label=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button> <button @click=\"zoomIn()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" 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-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" :class=\"magnifierEnabled ? 'bg-blue-600 hover:bg-blue-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><!-- Pointer mode (PDF only): Smart / Pan / Text --><div class=\"reader-seg shrink-0\" x-show=\"isPDF\" role=\"group\" aria-label=\"Pointer mode\" title=\"Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.\"><button :class=\"interactionMode === 'select' ? 'active' : ''\" @click=\"setInteractionMode('select')\" title=\"Smart: drag text to select, drag blank to pan\">Smart</button> <button :class=\"interactionMode === 'pan' ? 'active' : ''\" @click=\"setInteractionMode('pan')\" title=\"Pan: drag anywhere to move\">Pan</button> <button :class=\"interactionMode === 'text' ? 'active' : ''\" @click=\"setInteractionMode('text')\" title=\"Text: selection only, never pan (Shift overrides)\">Text</button></div><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><!-- Double page spread --><button @click=\"toggleSpread()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" :class=\"doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''\" title=\"Double Page Spread\" aria-label=\"Toggle double page spread\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z\"></path></svg></button><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><div id=\"progress-display-fx\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden shrink-0\"><span x-text=\"progressMain\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -257,7 +265,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
|
|||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 349, Col: 51}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 367, Col: 51}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -267,14 +275,14 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 351, Col: 72}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 369, Col: 72}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" title=\"Table of Contents (t)\">📖</button></div></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div><div class=\"w-px h-6 mx-1 shrink-0 reader-sep\"></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0\" title=\"Table of Contents (t)\">📖</button></div></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -303,20 +311,20 @@ func drawerHeader(title string) templ.Component {
|
|||||||
templ_7745c5c3_Var14 = templ.NopComponent
|
templ_7745c5c3_Var14 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"reader-drawer-header\"><h3 class=\"font-semibold\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"reader-drawer-header\"><h3 class=\"font-semibold\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var15 string
|
var templ_7745c5c3_Var15 string
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 364, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 382, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</h3><button @click=\"closeDrawers()\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" title=\"Close (Esc)\" aria-label=\"Close\">✕</button></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h3><button @click=\"closeDrawers()\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" title=\"Close (Esc)\" aria-label=\"Close\">✕</button></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -349,7 +357,7 @@ func ReaderTOCDrawer() templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"reader-drawer-body\"><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><template x-if=\"tocItems.length === 0\"><p class=\"text-sm py-2\" style=\"color: var(--text-secondary)\">No table of contents</p></template></nav></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-drawer-body\"><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><template x-if=\"tocItems.length === 0\"><p class=\"text-sm py-2\" style=\"color: var(--text-secondary)\">No table of contents</p></template></nav></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -382,7 +390,40 @@ func ReaderAnnotationsDrawer() templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-tabs\"><button :class=\"annotationsTab === 'highlights' ? 'active' : ''\" @click=\"annotationsTab = 'highlights'\">Highlights <span class=\"reader-tab-count\" x-text=\"highlightItems.length\">0</span></button> <button :class=\"annotationsTab === 'notes' ? 'active' : ''\" @click=\"annotationsTab = 'notes'\">Notes <span class=\"reader-tab-count\" x-text=\"noteItems.length\">0</span></button> <button :class=\"annotationsTab === 'bookmarks' ? 'active' : ''\" @click=\"annotationsTab = 'bookmarks'\">Bookmarks <span class=\"reader-tab-count\" x-text=\"bookmarkItems.length\">0</span></button></div><div class=\"reader-drawer-body\"><!-- Highlights --><div x-show=\"annotationsTab === 'highlights'\" class=\"space-y-2\"><template x-for=\"hl in highlightItems\" :key=\"hl.id\"><div class=\"reader-hl-row group\"><a href=\"#\" @click.prevent=\"goToHighlight(hl)\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"block text-sm truncate\" :style=\"'border-left: 3px solid ' + hl.color + '; padding-left: 0.5rem;'\" x-text=\"hl.text\"></span> <span class=\"block text-xs mt-0.5 truncate pl-2\" style=\"color: var(--text-secondary)\" x-show=\"hl.note\" x-text=\"'📝 ' + hl.note\"></span></a> <button @click=\"deleteHighlightById(hl.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete highlight\" aria-label=\"Delete highlight\">✕</button></div></template><template x-if=\"highlightItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Select text in the book to highlight it</p></template></div><!-- Notes --><div x-show=\"annotationsTab === 'notes'\"><textarea x-model=\"newNoteText\" rows=\"2\" class=\"reader-note-input\" placeholder=\"Add a note at the current position…\"></textarea> <button @click=\"addNote(newNoteText); newNoteText = ''\" class=\"w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Note</button><div class=\"space-y-2\"><template x-for=\"note in noteItems\" :key=\"note.id\"><div class=\"reader-hl-row group\"><a href=\"#\" @click.prevent=\"closeDrawers()\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"block text-sm\" x-text=\"note.content\"></span> <span class=\"block text-xs mt-0.5 truncate\" style=\"color: var(--text-secondary)\" x-text=\"note.positionLabel\"></span></a> <button @click=\"deleteNoteById(note.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete note\" aria-label=\"Delete note\">✕</button></div></template><template x-if=\"noteItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No notes yet</p></template></div></div><!-- Bookmarks --><div x-show=\"annotationsTab === 'bookmarks'\" class=\"space-y-2\"><template x-for=\"bookmark in bookmarkItems\" :key=\"bookmark.id\"><div class=\"flex items-center gap-1 group\"><a href=\"#\" @click.prevent=\"goToBookmark(bookmark)\" class=\"flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2\"><span class=\"font-medium block truncate\" x-text=\"bookmark.title\"></span> <span class=\"text-xs block truncate\" style=\"color: var(--text-secondary)\" x-text=\"bookmark.positionLabel\"></span></a> <button @click=\"deleteBookmark(bookmark.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete bookmark\" aria-label=\"Delete bookmark\">✕</button></div></template><template x-if=\"bookmarkItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No bookmarks yet</p></template><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>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"reader-tabs\"><button :class=\"annotationsTab === 'highlights' ? 'active' : ''\" @click=\"annotationsTab = 'highlights'\">Highlights <span class=\"reader-tab-count\" x-text=\"highlightItems.length\">0</span></button> <button :class=\"annotationsTab === 'notes' ? 'active' : ''\" @click=\"annotationsTab = 'notes'\">Notes <span class=\"reader-tab-count\" x-text=\"noteItems.length\">0</span></button> <button :class=\"annotationsTab === 'bookmarks' ? 'active' : ''\" @click=\"annotationsTab = 'bookmarks'\">Bookmarks <span class=\"reader-tab-count\" x-text=\"bookmarkItems.length\">0</span></button></div><div class=\"reader-drawer-body\"><!-- Highlights --><div x-show=\"annotationsTab === 'highlights'\" class=\"space-y-2\"><template x-for=\"hl in highlightItems\" :key=\"hl.id\"><div class=\"reader-hl-row group\"><a href=\"#\" @click.prevent=\"goToHighlight(hl)\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"block text-sm truncate\" :style=\"'border-left: 3px solid ' + hl.color + '; padding-left: 0.5rem;'\" x-text=\"hl.text\"></span> <span class=\"block text-xs mt-0.5 truncate pl-2\" style=\"color: var(--text-secondary)\" x-show=\"hl.note\" x-text=\"'📝 ' + hl.note\"></span></a> <button @click=\"deleteHighlightById(hl.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete highlight\" aria-label=\"Delete highlight\">✕</button></div></template><template x-if=\"highlightItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Select text in the book to highlight it</p></template></div><!-- Notes --><div x-show=\"annotationsTab === 'notes'\"><textarea x-model=\"newNoteText\" rows=\"2\" class=\"reader-note-input\" placeholder=\"Add a note at the current position…\"></textarea> <button @click=\"addNote(newNoteText); newNoteText = ''\" class=\"w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Note</button><div class=\"space-y-2\"><template x-for=\"note in noteItems\" :key=\"note.id\"><div class=\"reader-hl-row group\"><a href=\"#\" @click.prevent=\"closeDrawers()\" class=\"flex-1 min-w-0 block py-2 px-2 rounded hover:bg-gray-700\"><span class=\"block text-sm\" x-text=\"note.content\"></span> <span class=\"block text-xs mt-0.5 truncate\" style=\"color: var(--text-secondary)\" x-text=\"note.positionLabel\"></span></a> <button @click=\"deleteNoteById(note.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete note\" aria-label=\"Delete note\">✕</button></div></template><template x-if=\"noteItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No notes yet</p></template></div></div><!-- Bookmarks --><div x-show=\"annotationsTab === 'bookmarks'\" class=\"space-y-2\"><template x-for=\"bookmark in bookmarkItems\" :key=\"bookmark.id\"><div class=\"flex items-center gap-1 group\"><a href=\"#\" @click.prevent=\"goToBookmark(bookmark)\" class=\"flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2\"><span class=\"font-medium block truncate\" x-text=\"bookmark.title\"></span> <span class=\"text-xs block truncate\" style=\"color: var(--text-secondary)\" x-text=\"bookmark.positionLabel\"></span></a> <button @click=\"deleteBookmark(bookmark.id)\" class=\"p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity\" title=\"Delete bookmark\" aria-label=\"Delete bookmark\">✕</button></div></template><template x-if=\"bookmarkItems.length === 0\"><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No bookmarks yet</p></template><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
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReaderSearchDrawer() 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 {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var18 == nil {
|
||||||
|
templ_7745c5c3_Var18 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = drawerHeader("Search").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"reader-search-bar\"><input type=\"search\" x-ref=\"searchInput\" x-model=\"searchQuery\" @keydown.enter.prevent=\"runSearch()\" placeholder=\"Search in book…\" class=\"reader-note-input grow\" aria-label=\"Search query\"> <button x-show=\"searchQuery || searchGroups.length\" @click=\"searchQuery = ''; clearSearchResults()\" class=\"p-2 rounded-lg hover:bg-gray-700 shrink-0\" title=\"Clear results\" aria-label=\"Clear results\">✕</button></div><div class=\"reader-search-status\" x-show=\"searchQuery || searching || searchGroups.length\"><span x-show=\"searching\" x-text=\"'Searching… ' + Math.round(searchProgress * 100) + '%'\">Searching…</span> <span x-show=\"!searching && searchGroups.length\" x-text=\"searchMatchCount + ' match' + (searchMatchCount === 1 ? '' : 'es')\">0 matches</span> <span x-show=\"!searching && searchQuery && !searchGroups.length\">No matches</span></div><div class=\"reader-drawer-body\"><template x-for=\"(group, gi) in searchGroups\" :key=\"gi\"><div class=\"mb-3\"><div class=\"text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary)\" x-text=\"group.label\"></div><div class=\"space-y-1\"><template x-for=\"item in group.items\" :key=\"item.cfi\"><a href=\"#\" @click.prevent=\"goToSearchResult(item.cfi)\" class=\"search-result\"><span x-text=\"item.pre\"></span><mark x-text=\"item.match\"></mark><span x-text=\"item.post\"></span></a></template></div></div></template></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -406,16 +447,16 @@ func ReaderSettingsDrawer() templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var18 == nil {
|
if templ_7745c5c3_Var19 == nil {
|
||||||
templ_7745c5c3_Var18 = templ.NopComponent
|
templ_7745c5c3_Var19 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = drawerHeader("Settings").Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = drawerHeader("Settings").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"reader-drawer-body\"><!-- Behavior --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Behavior</h3><label class=\"block mb-2\">Chrome <select name=\"chrome_behavior\" x-model=\"chromeBehavior\" @change=\"applyChromeBehavior()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" x-model=\"progressMode\" @change=\"applyProgressMode()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label> <label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"tap_zones_enabled\" x-model=\"tapZonesEnabled\" @change=\"applyTapZoneSettings()\" class=\"mr-2\"> Tap Zones <span class=\"text-xs ml-1\" style=\"color: var(--text-secondary)\">(touch)</span></label> <label class=\"block mb-2\" x-show=\"tapZonesEnabled\">Tap Zone Size <input type=\"range\" name=\"tap_zone_size\" x-model.number=\"tapZoneSize\" @change=\"applyTapZoneSettings()\" min=\"10\" max=\"50\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"tapZoneSize + '%'\" style=\"color: var(--text-secondary)\">30%</span></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-14 h-7 rounded-full transition-colors duration-200 flex items-center px-1\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 flex items-center justify-center text-xs\" :class=\"readingMode === 'dark' ? 'translate-x-7' : 'translate-x-0'\" x-text=\"readingMode === 'dark' ? '🌙' : '☀️'\">☀️</span></button></div><div class=\"grid grid-cols-6 gap-2\"><template x-for=\"sw in themeSwatches\" :key=\"sw.value\"><button type=\"button\" @click=\"readingTheme = sw.value; applyTheme()\" class=\"theme-swatch\" :class=\"readingTheme === sw.value ? 'ring-2 ring-blue-500' : ''\" :style=\"'background-color:' + sw.bg + '; color:' + sw.fg\" :title=\"sw.label\" :aria-label=\"'Theme ' + sw.label\" :aria-pressed=\"readingTheme === sw.value\"><span class=\"text-xs font-semibold\">Aa</span></button></template></div></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\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><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=\"10\" max=\"40\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">18px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"0.8\" max=\"3.0\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Layout (fixed-layout only) --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Layout</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" x-model=\"doublePageSpread\" @change=\"applyDoublePageSpread()\" class=\"mr-2\"> Double Page Spread</label></div><div class=\"flex gap-2 mt-6\"><button @click=\"restoreDefaults()\" class=\"flex-1 py-2 border rounded hover:opacity-80\" style=\"border-color: var(--border);\">Restore Defaults</button> <button @click=\"closeDrawers()\" class=\"flex-1 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"reader-drawer-body\"><!-- Behavior --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Behavior</h3><label class=\"block mb-2\">Chrome <select name=\"chrome_behavior\" x-model=\"chromeBehavior\" @change=\"applyChromeBehavior()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" x-model=\"progressMode\" @change=\"applyProgressMode()\" class=\"w-full mt-1 px-3 py-2 rounded border\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label> <label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"tap_zones_enabled\" x-model=\"tapZonesEnabled\" @change=\"applyTapZoneSettings()\" class=\"mr-2\"> Tap Zones <span class=\"text-xs ml-1\" style=\"color: var(--text-secondary)\">(touch)</span></label> <label class=\"block mb-2\" x-show=\"tapZonesEnabled\">Tap Zone Size <input type=\"range\" name=\"tap_zone_size\" x-model.number=\"tapZoneSize\" @change=\"applyTapZoneSettings()\" min=\"10\" max=\"50\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"tapZoneSize + '%'\" style=\"color: var(--text-secondary)\">30%</span></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><div class=\"flex items-center justify-between mb-2\"><h3 class=\"font-semibold\">Reading Theme</h3><button @click=\"toggleReadingMode()\" class=\"relative w-14 h-7 rounded-full transition-colors duration-200 flex items-center px-1\" :class=\"readingMode === 'dark' ? 'bg-blue-500' : 'bg-gray-400'\" type=\"button\" :aria-label=\"readingMode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'\"><span class=\"absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 flex items-center justify-center text-xs\" :class=\"readingMode === 'dark' ? 'translate-x-7' : 'translate-x-0'\" x-text=\"readingMode === 'dark' ? '🌙' : '☀️'\">☀️</span></button></div><div class=\"grid grid-cols-6 gap-2\"><template x-for=\"sw in themeSwatches\" :key=\"sw.value\"><button type=\"button\" @click=\"readingTheme = sw.value; applyTheme()\" class=\"theme-swatch\" :class=\"readingTheme === sw.value ? 'ring-2 ring-blue-500' : ''\" :style=\"'background-color:' + sw.bg + '; color:' + sw.fg\" :title=\"sw.label\" :aria-label=\"'Theme ' + sw.label\" :aria-pressed=\"readingTheme === sw.value\"><span class=\"text-xs font-semibold\">Aa</span></button></template></div></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\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><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=\"10\" max=\"40\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"fontSize + 'px'\" style=\"color: var(--text-secondary)\">18px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" x-model.number=\"lineHeight\" @change=\"applyFont()\" min=\"0.8\" max=\"3.0\" step=\"0.1\" class=\"w-full\"> <span class=\"text-xs\" x-text=\"lineHeight\" style=\"color: var(--text-secondary)\">1.6</span></label></div><!-- Layout (fixed-layout only) --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Layout</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" x-model=\"doublePageSpread\" @change=\"applyDoublePageSpread()\" class=\"mr-2\"> Double Page Spread</label></div><div class=\"flex gap-2 mt-6\"><button @click=\"restoreDefaults()\" class=\"flex-1 py-2 border rounded hover:opacity-80\" style=\"border-color: var(--border);\">Restore Defaults</button> <button @click=\"closeDrawers()\" class=\"flex-1 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,6 +401,15 @@ document.addEventListener("alpine:init", () => {
|
|||||||
annotationsTab: "highlights" as string,
|
annotationsTab: "highlights" as string,
|
||||||
newNoteText: "",
|
newNoteText: "",
|
||||||
highlightColors: HIGHLIGHT_COLORS,
|
highlightColors: HIGHLIGHT_COLORS,
|
||||||
|
searchOpen: false,
|
||||||
|
searchQuery: "",
|
||||||
|
searchGroups: [] as {
|
||||||
|
label: string;
|
||||||
|
items: { cfi: string; pre: string; match: string; post: string }[];
|
||||||
|
}[],
|
||||||
|
searchProgress: 0,
|
||||||
|
searching: false,
|
||||||
|
searchGen: 0,
|
||||||
selectionPopover: {
|
selectionPopover: {
|
||||||
open: false,
|
open: false,
|
||||||
mode: "create" as "create" | "edit",
|
mode: "create" as "create" | "edit",
|
||||||
@@ -477,6 +486,12 @@ document.addEventListener("alpine:init", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
get searchMatchCount(): number {
|
||||||
|
return this.searchGroups.reduce(
|
||||||
|
(n: number, g: { items: unknown[] }) => n + g.items.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
const link = document.getElementById("reader-back");
|
const link = document.getElementById("reader-back");
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
@@ -1425,13 +1440,80 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.closeDrawers();
|
this.closeDrawers();
|
||||||
this.bookmarksOpen = opening;
|
this.bookmarksOpen = opening;
|
||||||
},
|
},
|
||||||
|
toggleSearch() {
|
||||||
|
if (this.isFixedLayout) return; // search requires reflowable text
|
||||||
|
const opening = !this.searchOpen;
|
||||||
|
this.closeDrawers();
|
||||||
|
this.searchOpen = opening;
|
||||||
|
if (opening) {
|
||||||
|
(this as any).$nextTick(() =>
|
||||||
|
(this as any).$refs.searchInput?.focus(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
anyDrawerOpen(): boolean {
|
anyDrawerOpen(): boolean {
|
||||||
return this.tocOpen || this.settingsOpen || this.bookmarksOpen;
|
return this.tocOpen || this.settingsOpen || this.bookmarksOpen || this.searchOpen;
|
||||||
},
|
},
|
||||||
closeDrawers() {
|
closeDrawers() {
|
||||||
this.tocOpen = false;
|
this.tocOpen = false;
|
||||||
this.settingsOpen = false;
|
this.settingsOpen = false;
|
||||||
this.bookmarksOpen = false;
|
this.bookmarksOpen = false;
|
||||||
|
this.searchOpen = false;
|
||||||
|
},
|
||||||
|
// ----- in-book search (reflowable) -----
|
||||||
|
// view.search() is an async generator: it progressively scans sections,
|
||||||
|
// yields {progress} per section and {label, subitems} per section with
|
||||||
|
// hits, and draws outline highlights through the overlayer pipeline.
|
||||||
|
// A generation counter discards results from superseded searches.
|
||||||
|
async runSearch() {
|
||||||
|
const q = this.searchQuery.trim();
|
||||||
|
if (!q) {
|
||||||
|
this.clearSearchResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.searchGen++;
|
||||||
|
const gen = this.searchGen;
|
||||||
|
this.searching = true;
|
||||||
|
this.searchProgress = 0;
|
||||||
|
this.searchGroups = [];
|
||||||
|
try {
|
||||||
|
for await (const r of this.view.search({ query: q })) {
|
||||||
|
if (gen !== this.searchGen) return;
|
||||||
|
if (r === "done") break;
|
||||||
|
if (r && typeof r === "object") {
|
||||||
|
if (typeof r.progress === "number") {
|
||||||
|
this.searchProgress = r.progress;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(r.subitems) && r.subitems.length) {
|
||||||
|
this.searchGroups.push({
|
||||||
|
label: r.label || `Section ${this.searchGroups.length + 1}`,
|
||||||
|
items: r.subitems.map((s: any) => ({
|
||||||
|
cfi: s.cfi,
|
||||||
|
pre: s.excerpt?.pre ?? "",
|
||||||
|
match: s.excerpt?.match ?? "",
|
||||||
|
post: s.excerpt?.post ?? "",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
/* search errors leave partial results */
|
||||||
|
}
|
||||||
|
if (gen === this.searchGen) this.searching = false;
|
||||||
|
},
|
||||||
|
clearSearchResults() {
|
||||||
|
this.searchGen++;
|
||||||
|
this.searching = false;
|
||||||
|
this.searchGroups = [];
|
||||||
|
this.searchProgress = 0;
|
||||||
|
this.view?.clearSearch?.();
|
||||||
|
},
|
||||||
|
goToSearchResult(cfi: string) {
|
||||||
|
if (!cfi) return;
|
||||||
|
this.view?.goTo?.(cfi);
|
||||||
|
this.closeDrawers();
|
||||||
},
|
},
|
||||||
flattenTOC(items: any[], depth = 0): any[] {
|
flattenTOC(items: any[], depth = 0): any[] {
|
||||||
const result: any[] = [];
|
const result: any[] = [];
|
||||||
@@ -1923,6 +2005,10 @@ document.addEventListener("alpine:init", () => {
|
|||||||
if (k === "t") this.toggleTOC();
|
if (k === "t") this.toggleTOC();
|
||||||
else if (k === "s") this.toggleSettings();
|
else if (k === "s") this.toggleSettings();
|
||||||
else if (k === "b") this.addBookmark();
|
else if (k === "b") this.addBookmark();
|
||||||
|
else if (k === "/") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.toggleSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -991,6 +991,40 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* In-book search */
|
||||||
|
.reader-search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.reader-search-status {
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 1.75rem;
|
||||||
|
}
|
||||||
|
.search-result {
|
||||||
|
display: block;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.search-result:hover {
|
||||||
|
background-color: color-mix(in srgb, currentColor 9%, transparent);
|
||||||
|
}
|
||||||
|
.search-result mark {
|
||||||
|
background-color: rgba(255, 213, 79, 0.4);
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Series stacked covers ---------- */
|
/* ---------- Series stacked covers ---------- */
|
||||||
.stacked-covers {
|
.stacked-covers {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user