feat(reader): webtoon reading mode + brightness/contrast/night filters

Phase 4 of the reader redesign (foliate-js ea268df):

- Webtoon mode for comics: continuous vertical scroll of all pages
  (900px centered column on wide screens), lazy-loaded with a 150%
  IntersectionObserver margin, far pages unloaded to bound memory
  with stable aspect-ratio placeholders so the scrollbar never jumps.
  Chosen per book (Paged | Webtoon segmented control in Settings →
  Layout & Display; stored in localStorage per media item since a
  webtoon title and a paged manga volume want different flows).
  Toggling reloads the reader — the renderer is chosen at open time —
  and progress restores from the saved page. Relocate events flow
  through the same pipeline, so the slider, progress saving, back
  stack, tap zones, and edge zones all work unchanged. Zoom/fit/
  magnifier/spread controls hide in webtoon (natural-width scroll).
- Display filters for fixed-layout: brightness (30-130%) and
  contrast (70-130%) sliders with live preview, plus Night Mode
  (invert) — also a quick row in the ⋯ tools menu. One --fx-filter
  CSS var drives everything: ::part(filter) on foliate-view iframes
  (forwarded via the new exportparts attribute) and the webtoon
  page images alike. Persisted as fx_brightness/fx_contrast/fx_invert
  (types + defaults both sides); Restore Defaults resets them.
This commit is contained in:
john-okeefe committed 2026-08-18 08:25:00 -04:00
1 parent 94be6edceb
commit e500039d1b
9 files changed
+109 -19

No files matched your search

+5 -5
View File
@@ -125,7 +125,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, "\"><!-- Reading surface: edge-to-edge. Chrome overlays translucently;\n\t\t\t drawers and scrim float above everything. --><div id=\"reader-viewport\" class=\"fixed inset-0\"><foliate-view id=\"reader-view\" class=\"block w-full h-full\"></foliate-view><!-- Desktop edge page-turn zones: hover to reveal the arrow.\n\t\t\t\t Hidden on touch (tap zones cover that) and while a\n\t\t\t\t fixed-layout page is zoomed (edges belong to content then). --><div class=\"reader-edge-zone left\" :class=\"edgeZonesActive ? '' : 'pointer-events-none'\" @click=\"goLeft()\" aria-hidden=\"true\"><svg class=\"reader-icon\" width=\"28\" height=\"28\" aria-hidden=\"true\"><path d=\"M 18 6 L 10 14 L 18 22\"></path></svg></div><div class=\"reader-edge-zone right\" :class=\"edgeZonesActive ? '' : 'pointer-events-none'\" @click=\"goRight()\" aria-hidden=\"true\"><svg class=\"reader-icon\" width=\"28\" height=\"28\" aria-hidden=\"true\"><path d=\"M 10 6 L 18 14 L 10 22\"></path></svg></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><!-- Reading surface: edge-to-edge. Chrome overlays translucently;\n\t\t\t drawers and scrim float above everything. --><div id=\"reader-viewport\" class=\"fixed inset-0\"><foliate-view id=\"reader-view\" exportparts=\"filter\" class=\"block w-full h-full\"></foliate-view><!-- Desktop edge page-turn zones: hover to reveal the arrow.\n\t\t\t\t Hidden on touch (tap zones cover that) and while a\n\t\t\t\t fixed-layout page is zoomed (edges belong to content then). --><div class=\"reader-edge-zone left\" :class=\"edgeZonesActive ? '' : 'pointer-events-none'\" @click=\"goLeft()\" aria-hidden=\"true\"><svg class=\"reader-icon\" width=\"28\" height=\"28\" aria-hidden=\"true\"><path d=\"M 18 6 L 10 14 L 18 22\"></path></svg></div><div class=\"reader-edge-zone right\" :class=\"edgeZonesActive ? '' : 'pointer-events-none'\" @click=\"goRight()\" aria-hidden=\"true\"><svg class=\"reader-icon\" width=\"28\" height=\"28\" aria-hidden=\"true\"><path d=\"M 10 6 L 18 14 L 10 22\"></path></svg></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -257,7 +257,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"w-px h-6 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></div><!-- Fixed-layout row (comics, PDFs, manga) --><div x-show=\"isFixedLayout\"><!-- Full toolbar: desktop/tablet (>= 768px, md). Wraps gracefully in between. --><div class=\"hidden md:flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1\"><div class=\"flex items-center gap-0.5\"><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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button></div><input id=\"progress-slider-fx\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" class=\"grow min-w-[3rem]\"> <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><!-- Fit mode + zoom cluster --><div class=\"flex items-center gap-1\"><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> <button @click=\"zoomOut()\" class=\"p-1.5 sm: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> <button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]\" 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\" 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><!-- Recenter: reset pan, keep zoom --><button @click=\"recenterView()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Recenter page (reset pan, keep zoom)\" aria-label=\"Recenter page\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"4\"></circle> <path d=\"M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22\"></path></svg></button><!-- Magnifier --><button @click=\"toggleMagnifier()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" :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></div><!-- 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><!-- Double page spread --><button @click=\"toggleSpread()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" :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><!-- Progress + TOC --><div class=\"flex items-center gap-1\"><div id=\"progress-display-fx\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[3.5rem] textLine truncated
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"w-px h-6 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></div><!-- Fixed-layout row (comics, PDFs, manga) --><div x-show=\"isFixedLayout\"><!-- Full toolbar: desktop/tablet (>= 768px, md). Wraps gracefully in between. --><div class=\"hidden md:flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1\"><div class=\"flex items-center gap-0.5\"><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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button></div><input id=\"progress-slider-fx\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" class=\"grow min-w-[3rem]\"> <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><!-- Fit mode + zoom cluster (not applicable in webtoon flow) --><div class=\"flex items-center gap-1\" x-show=\"comicFlow !== 'webtoon'\"><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> <button @click=\"zoomOut()\" class=\"p-1.5 sm: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> <button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]\" 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\" 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><!-- Recenter: reset pan, keep zoom --><button @click=\"recenterView()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Recenter page (reset pan, keep zoom)\" aria-label=\"Recenter page\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"4\"></circle> <path d=\"M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22\"></path></svg></button><!-- Magnifier --><button @click=\"toggleMagnifier()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" :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></div><!-- 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><!-- Double page spread (not applicable in webtoon flow) --><button x-show=\"comicFlow !== 'webtoon'\" @click=\"toggleSpread()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" :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><!-- Progress + TOC --><div class=\"flex items-centerLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -282,7 +282,7 @@ func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Compo
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents (t)\">📖</button></div></div><!-- Compact row (phone-sized windows): single line + ⋯ overflow.\n\t\t\t\t Standard mobile-reader pattern: paging, slider, progress stay;\n\t\t\t\t power tools move into a labeled menu. --><div class=\"flex md:hidden items-center gap-1 px-1.5 py-1.5\"><button @click=\"goLeft()\" class=\"p-1.5 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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button> <input id=\"progress-slider-fx-compact\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" class=\"grow min-w-[3rem]\"> <button @click=\"goRight()\" class=\"p-1.5 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 @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[3rem] text-center cursor-pointer truncate shrink-0\"><span x-text=\"progressMain\">0/0</span></div><button data-tools-toggle @click=\"toggleTools()\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" :title=\"toolsOpen ? 'Close tools' : 'More tools'\" aria-label=\"More tools\" :aria-expanded=\"toolsOpen ? 'true' : 'false'\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"></circle> <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"></circle> <circle cx=\"19\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"></circle></svg></button></div><!-- Tools overflow menu: labeled rows (mobile-reader pattern —\n\t\t\t\t labels beat mystery icons). Anchored above the bottom bar. --><div x-show=\"toolsOpen\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 translate-y-2\" x-transition:enter-end=\"opacity-100 translate-y-0\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 translate-y-0\" x-transition:leave-end=\"opacity-0 translate-y-2\" class=\"reader-tools-popover\" role=\"menu\" aria-label=\"Reader tools\" @pointerdown.stop><div class=\"reader-tools-row\"><span>Zoom</span><div class=\"flex items-center gap-1\"><button @click=\"zoomOut()\" class=\"reader-popover-btn\" title=\"Zoom out\">−</button> <button @click=\"resetZoom()\" class=\"text-xs px-1 min-w-[3rem] text-center\" title=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button> <button @click=\"zoomIn()\" class=\"reader-popover-btn\" title=\"Zoom in\">+</button></div></div><div class=\"reader-tools-row\"><span>Fit</span> <select @change=\"applyFitMode($event.target.value)\" class=\"reader-select text-xs px-1.5 py-1 rounded-lg\" aria-label=\"Fit mode\"><option value=\"fit-page\">Fit Page</option> <option value=\"fit-width\">Fit Width</option></select></div><div class=\"reader-tools-row\"><span>Page position</span> <button @click=\"recenterView()\" class=\"reader-popover-btn text-xs px-2\" title=\"Reset pan, keep zoom\">Recenter</button></div><div class=\"reader-tools-row\"><span>Magnifier</span> <button @click=\"toggleMagnifier()\" class=\"reader-popover-btn text-xs px-2\" :class=\"magnifierEnabled ? 'text-blue-400' : ''\" x-text=\"magnifierEnabled ? 'On' : 'Off'\">Off</button></div><div class=\"reader-tools-row\" x-show=\"isPDF\"><span>Pointer</span><div class=\"reader-seg\" role=\"group\" aria-label=\"Pointer mode\"><button :class=\"interactionMode === 'select' ? 'active' : ''\" @click=\"setInteractionMode('select')\">Smart</button> <button :class=\"interactionMode === 'pan' ? 'active' : ''\" @click=\"setInteractionMode('pan')\">Pan</button> <button :class=\"interactionMode === 'text' ? 'active' : ''\" @click=\"setInteractionMode('text')\">Text</button></div></div><div class=\"reader-tools-row\"><span>Double page</span> <button @click=\"toggleSpread()\" class=\"reader-popover-btn text-xs px-2\" :class=\"doublePageSpread ? 'text-blue-400' : ''\" x-text=\"doublePageSpread ? 'On' : 'Off'\">Off</button></div><div class=\"reader-tools-row\"><span>Contents</span> <button @click=\"toggleTOC()\" class=\"reader-popover-btn text-xs px-2\">Open</button></div></div></div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents (t)\">📖</button></div></div><!-- Compact row (phone-sized windows): single line + ⋯ overflow.\n\t\t\t\t Standard mobile-reader pattern: paging, slider, progress stay;\n\t\t\t\t power tools move into a labeled menu. --><div class=\"flex md:hidden items-center gap-1 px-1.5 py-1.5\"><button @click=\"goLeft()\" class=\"p-1.5 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> <button x-show=\"backStack.length > 0\" @click=\"goBackToLocation()\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" title=\"Return to previous location (Alt+←)\" aria-label=\"Return to previous location\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z\"></path> <circle cx=\"12\" cy=\"9.5\" r=\"2\"></circle></svg></button> <input id=\"progress-slider-fx-compact\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" @input=\"goToFraction($event.target.value)\" class=\"grow min-w-[3rem]\"> <button @click=\"goRight()\" class=\"p-1.5 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 @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[3rem] text-center cursor-pointer truncate shrink-0\"><span x-text=\"progressMain\">0/0</span></div><button data-tools-toggle @click=\"toggleTools()\" class=\"p-1.5 rounded-lg hover:bg-gray-700\" :title=\"toolsOpen ? 'Close tools' : 'More tools'\" aria-label=\"More tools\" :aria-expanded=\"toolsOpen ? 'true' : 'false'\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"></circle> <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"></circle> <circle cx=\"19\" cy=\"12\" r=\"1.5\" fill=\"currentColor\"></circle></svg></button></div><!-- Tools overflow menu: labeled rows (mobile-reader pattern —\n\t\t\t\t labels beat mystery icons). Anchored above the bottom bar. --><div x-show=\"toolsOpen\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 translate-y-2\" x-transition:enter-end=\"opacity-100 translate-y-0\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 translate-y-0\" x-transition:leave-end=\"opacity-0 translate-y-2\" class=\"reader-tools-popover\" role=\"menu\" aria-label=\"Reader tools\" @pointerdown.stop><div class=\"reader-tools-row\" x-show=\"comicFlow !== 'webtoon'\"><span>Zoom</span><div class=\"flex items-center gap-1\"><button @click=\"zoomOut()\" class=\"reader-popover-btn\" title=\"Zoom out\">−</button> <button @click=\"resetZoom()\" class=\"text-xs px-1 min-w-[3rem] text-center\" title=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button> <button @click=\"zoomIn()\" class=\"reader-popover-btn\" title=\"Zoom in\">+</button></div></div><div class=\"reader-tools-row\" x-show=\"comicFlow !== 'webtoon'\"><span>Fit</span> <select @change=\"applyFitMode($event.target.value)\" class=\"reader-select text-xs px-1.5 py-1 rounded-lg\" aria-label=\"Fit mode\"><option value=\"fit-page\">Fit Page</option> <option value=\"fit-width\">Fit Width</option></select></div><div class=\"reader-tools-row\" x-show=\"comicFlow !== 'webtoon'\"><span>Page position</span> <button @click=\"recenterView()\" class=\"reader-popover-btn text-xs px-2\" title=\"Reset pan, keep zoom\">Recenter</button></div><div class=\"reader-tools-row\" x-show=\"comicFlow !== 'webtoon'\"><span>Magnifier</span> <button @click=\"toggleMagnifier()\" class=\"reader-popover-btn text-xs px-2\" :class=\"magnifierEnabled ? 'text-blue-400' : ''\" x-text=\"magnifierEnabled ? 'On' : 'Off'\">Off</button></div><div class=\"reader-tools-row\" x-show=\"comicFlow !== 'webtoon'\"><span>Night mode</span> <button @click=\"fxInvert = !fxInvert; applyFxFilter(true)\" class=\"reader-popover-btn text-xs px-2\" :class=\"fxInvert ? 'text-blue-400' : ''\" x-text=\"fxInvert ? 'On' : 'Off'\">Off</button></div><div class=\"reader-tools-row\" x-show=\"isPDF\"><span>Pointer</span><div class=\"reader-seg\" role=\"group\" aria-label=\"Pointer mode\"><button :class=\"interactionMode === 'select' ? 'active' : ''\" @click=\"setInteractionMode('select')\">Smart</button> <button :class=\"interactionMode === 'pan' ? 'active' : ''\" @click=\"setInteractionMode('pan')\">Pan</button> <button :class=\"interactionMode === 'text' ? 'active' : ''\" @click=\"setInteractionMode('text')\">Text</button></div></div><div class=\"rLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -318,7 +318,7 @@ func drawerHeader(title string) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 594, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 598, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -456,7 +456,7 @@ func ReaderSettingsDrawer() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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>")
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 & display (fixed-layout only) --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Layout &amp; Display</h3><label class=\"block mb-3\" x-show=\"isComic\">Reading Flow<div class=\"reader-seg w-full mt-1\" role=\"group\" aria-label=\"Reading flow\"><button type=\"button\" class=\"flex-1\" :class=\"comicFlow !== 'webtoon' ? 'active' : ''\" @click=\"setComicFlow('paged')\" title=\"One page or spread at a time\">Paged</button> <button type=\"button\" class=\"flex-1\" :class=\"comicFlow === 'webtoon' ? 'active' : ''\" @click=\"setComicFlow('webtoon')\" title=\"Continuous vertical scroll (long-strip)\">Webtoon</button></div></label> <label class=\"blLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}