feat(reader): immersive chrome, slide-over drawers, tri-state PDF pointer mode
Phase 1 of the reader redesign: - Reading surface is edge-to-edge; top/bottom bars overlay translucently (backdrop-blur) instead of reserving insets, killing the inset-coordination bug class entirely. Chrome auto-hides after 2.5s of pointer inactivity (chrome_behavior setting finally wired: auto-hide / always-visible; legacy values map to auto-hide). Pointer activity inside page iframes keeps it awake; Esc toggles. - TOC / Settings / Bookmarks become slide-over drawers with a scrim (z-50, full-height, safe-area aware), replacing the dockable-panel system and its window-shade headers. Only one drawer opens at a time; Esc or scrim click closes. - Bottom bar is contextual: reflowable keeps nav/slider/progress/TOC; fixed-layout row adds Fit Page/Width select, zoom cluster, magnifier (now shows active state), Double Page Spread toggle, and a Smart | Pan | Text segmented control replacing the cryptic two-state icon. Smart = text-aware drag; Text = selection-only (manual smart-detect off); Pan = force pan. Choice persists via pdf_interaction_mode (new setting + foliate 29bc958 'text' mode). - Settings drawer: Behavior (chrome, progress mode), Appearance with 18 Kindle-style theme swatches (single source of truth from THEME_COLORS), Typography, Layout — each scoped by format. - Keyboard: t/s/b open TOC/settings/bookmark, Esc closes drawers before toggling chrome, shortcuts skip form inputs; both slider rows tracked correctly (no duplicate-ID lookups). - Topbar: Back, title, add-bookmark, bookmarks drawer, Aa settings; chrome follows user theme.
This commit is contained in:
+13
-12
@@ -392,18 +392,19 @@ func (s *ReaderService) UpdateSettings(
|
|||||||
|
|
||||||
func (s *ReaderService) getDefaultSettings() map[string]interface{} {
|
func (s *ReaderService) getDefaultSettings() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"chrome_behavior": "auto-hide",
|
"chrome_behavior": "auto-hide",
|
||||||
"progress_mode": "pages",
|
"progress_mode": "pages",
|
||||||
"chrome_theme": "tokyo-night",
|
"chrome_theme": "tokyo-night",
|
||||||
"reading_theme": "dark",
|
"reading_theme": "dark",
|
||||||
"reading_font": "literata",
|
"reading_font": "literata",
|
||||||
"font_size": 16,
|
"font_size": 16,
|
||||||
"line_height": 1.6,
|
"line_height": 1.6,
|
||||||
"margin_width": 20,
|
"margin_width": 20,
|
||||||
"tap_zone_size": 30,
|
"tap_zone_size": 30,
|
||||||
"auto_scroll": false,
|
"auto_scroll": false,
|
||||||
"panel_zoom_enabled": true,
|
"panel_zoom_enabled": true,
|
||||||
"double_page_spread": true,
|
"double_page_spread": true,
|
||||||
|
"pdf_interaction_mode": "select",
|
||||||
|
|
||||||
// Dockable panel defaults
|
// Dockable panel defaults
|
||||||
"panel_layout": map[string]interface{}{
|
"panel_layout": map[string]interface{}{
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"dev": "npm run build:ts:dev && npm run build:css"
|
"dev": "npm run build:ts:dev && npm run build:css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#d4d87a9",
|
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#29bc958",
|
||||||
"alpinejs": "^3.15.8",
|
"alpinejs": "^3.15.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
|||||||
+309
-271
@@ -67,59 +67,102 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
|||||||
x-init={ readerInitExpr(metadata, progress, bookmarks) }
|
x-init={ readerInitExpr(metadata, progress, bookmarks) }
|
||||||
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
|
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
|
||||||
>
|
>
|
||||||
@ReaderChrome(user, metadata, progress)
|
<!-- Reading surface: edge-to-edge. Chrome overlays translucently;
|
||||||
<!-- Dockable Panels Container -->
|
drawers and scrim float above everything. -->
|
||||||
<div id="reader-panels" class="fixed inset-0 pointer-events-none z-30">
|
<div id="reader-viewport" class="fixed inset-0">
|
||||||
<!-- Left Sidebar (TOC) -->
|
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
||||||
<div id="left-sidebar" class="absolute left-0 pointer-events-auto flex flex-col" style="top: 56px; bottom: 56px;">
|
|
||||||
<div id="toc-panel" x-show="tocOpen" x-transition class="panel-container pointer-events-auto" data-panel="toc">
|
|
||||||
@ReaderTOCPanel(metadata)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Right Sidebar (Settings, Bookmarks) -->
|
|
||||||
<div id="right-sidebar" class="absolute right-0 pointer-events-auto flex flex-col" style="top: 56px; bottom: 56px;">
|
@ReaderChrome(metadata, progress)
|
||||||
<div id="settings-panel" x-show="settingsOpen" x-transition class="panel-container pointer-events-auto" data-panel="settings">
|
|
||||||
@ReaderSettingsPanel()
|
<!-- Drawer scrim -->
|
||||||
</div>
|
<div
|
||||||
<div id="bookmarks-panel" x-show="bookmarksOpen" x-transition class="panel-container pointer-events-auto" data-panel="bookmarks">
|
x-show="tocOpen || settingsOpen || bookmarksOpen"
|
||||||
@ReaderBookmarksPanel()
|
x-transition.opacity.duration.200ms
|
||||||
</div>
|
@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"
|
||||||
|
>
|
||||||
|
@ReaderTOCDrawer()
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="reader-viewport" class="absolute inset-x-0" style="top: 56px; bottom: 56px;">
|
<!-- Settings drawer (right) -->
|
||||||
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
<div
|
||||||
</div>
|
id="settings-drawer"
|
||||||
</body>
|
x-show="settingsOpen"
|
||||||
|
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="Reader settings"
|
||||||
|
>
|
||||||
|
@ReaderSettingsDrawer()
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bookmarks drawer (right) -->
|
||||||
|
<div
|
||||||
|
id="bookmarks-drawer"
|
||||||
|
x-show="bookmarksOpen"
|
||||||
|
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="Bookmarks"
|
||||||
|
>
|
||||||
|
@ReaderBookmarksDrawer()
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) {
|
templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||||
<div id="reader-chrome" class="transition-opacity duration-300">
|
<div id="reader-chrome" class="transition-opacity duration-300" :class="chromeVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'">
|
||||||
<!-- Top bar -->
|
<!-- Top bar -->
|
||||||
<div id="reader-topbar" class="fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]" style="background-color: var(--bg-primary);">
|
<div id="reader-topbar" class="fixed top-0 left-0 right-0 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]" style="background-color: var(--bg-primary);">
|
||||||
<div class="flex items-center justify-between px-3 py-2 sm:px-4 sm:py-3">
|
<div class="flex items-center justify-between px-3 py-2 sm:px-4">
|
||||||
<a id="reader-back" href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
|
<a id="reader-back" href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
|
||||||
← Back
|
← Back
|
||||||
</a>
|
</a>
|
||||||
<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>
|
||||||
<button
|
<div class="flex items-center gap-1">
|
||||||
@click="toggleSettings()"
|
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
|
||||||
class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700"
|
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmarks">📝</button>
|
||||||
title="Settings"
|
<button @click="toggleSettings()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Settings (s)">Aa</button>
|
||||||
>
|
</div>
|
||||||
⚙️
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- Bottom bar -->
|
</div>
|
||||||
<div id="reader-bottombar" class="fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]" style="background-color: var(--bg-primary);">
|
<!-- Bottom bar -->
|
||||||
<div class="flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1">
|
<div id="reader-bottombar" class="fixed bottom-0 left-0 right-0 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]" style="background-color: var(--bg-primary);">
|
||||||
<!-- Left navigation -->
|
<!-- 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">
|
<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">
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Progress slider -->
|
|
||||||
<input
|
<input
|
||||||
id="progress-slider"
|
id="progress-slider"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -131,56 +174,12 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
class="grow"
|
class="grow"
|
||||||
/>
|
/>
|
||||||
<datalist id="tick-marks"></datalist>
|
<datalist id="tick-marks"></datalist>
|
||||||
<!-- Right navigation -->
|
|
||||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
<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">
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Separator -->
|
|
||||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||||
<!-- Zoom controls (fixed-layout only) -->
|
|
||||||
<template x-if="isFixedLayout">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<!-- Zoom out -->
|
|
||||||
<button @click="zoomOut()" class="p-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>
|
|
||||||
<!-- Zoom percentage -->
|
|
||||||
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom" aria-label="Reset zoom">
|
|
||||||
<span x-text="zoomPercent + '%'">100%</span>
|
|
||||||
</button>
|
|
||||||
<!-- Zoom in -->
|
|
||||||
<button @click="zoomIn()" class="p-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>
|
|
||||||
<!-- Magnifier -->
|
|
||||||
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle magnifier">
|
|
||||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
||||||
<circle cx="9" cy="9" r="5"></circle>
|
|
||||||
<path d="M 13 13 L 18 18"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<!-- Pan/Select mode (PDF only) -->
|
|
||||||
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="interactionMode === 'pan' ? 'bg-blue-600 hover:bg-blue-700' : ''" :title="interactionMode === 'pan' ? 'Pan mode: drag anywhere to move. Click for Smart select.' : 'Smart select: drag text to select, drag blank to pan (Shift = force pan). Click for Pan mode.'" :aria-label="interactionMode === 'pan' ? 'Pan mode' : 'Smart select mode'">
|
|
||||||
<!-- Smart select (default): text cursor -->
|
|
||||||
<svg x-show="interactionMode !== 'pan'" class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
||||||
<path d="M 10 3 L 10 17 M 7 5 L 10 4 L 13 5 M 7 15 L 10 16 L 13 15"></path>
|
|
||||||
</svg>
|
|
||||||
<!-- Force pan: move arrow -->
|
|
||||||
<svg x-show="interactionMode === 'pan'" class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
||||||
<path d="M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- Separator -->
|
|
||||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
|
||||||
<!-- Progress display -->
|
|
||||||
<div id="progress-display" @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">
|
<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">
|
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">
|
||||||
if progress.FormatGroup == "reflowable" {
|
if progress.FormatGroup == "reflowable" {
|
||||||
@@ -194,214 +193,253 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Separator -->
|
|
||||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||||
<!-- Action buttons -->
|
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||||
<div class="flex items-center gap-1">
|
</div>
|
||||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
<!-- Fixed-layout row (comics, PDFs, manga) -->
|
||||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position">🏷️</button>
|
<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="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmarks">📝</button>
|
<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 bg-gray-600 mx-1 shrink-0"></div>
|
||||||
|
<!-- Fit mode -->
|
||||||
|
<select
|
||||||
|
@change="applyFitMode($event.target.value)"
|
||||||
|
class="text-xs px-1.5 py-1 rounded-lg bg-transparent border shrink-0"
|
||||||
|
style="border-color: var(--border);"
|
||||||
|
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>
|
||||||
|
<div class="w-px h-6 bg-gray-600 mx-1 shrink-0"></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 bg-gray-600 mx-1 shrink-0"></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 progress.FormatGroup == "reflowable" {
|
||||||
|
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||||
|
} else {
|
||||||
|
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-6 bg-gray-600 mx-1 shrink-0"></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ReaderSettingsPanel() {
|
templ drawerHeader(title string) {
|
||||||
<div
|
<div class="reader-drawer-header">
|
||||||
class="dockable-panel"
|
<h3 class="font-semibold">{ title }</h3>
|
||||||
data-panel="settings"
|
<button @click="closeDrawers()" class="p-1.5 rounded-lg hover:bg-gray-700" title="Close (Esc)" aria-label="Close">✕</button>
|
||||||
data-side="left"
|
|
||||||
x-ref="settingsPanel"
|
|
||||||
>
|
|
||||||
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.settingsPanel)">
|
|
||||||
<h3 class="panel-title font-semibold">⚙️ Settings</h3>
|
|
||||||
<div class="panel-controls flex items-center gap-2">
|
|
||||||
<button class="window-shade-toggle">─</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-content p-4 overflow-y-auto">
|
|
||||||
<!-- Display settings -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<h3 class="font-semibold mb-2">Display</h3>
|
|
||||||
<label class="block mb-2">
|
|
||||||
Progress Mode
|
|
||||||
<select name="progress_mode" x-model="progressMode" @change="applyProgressMode()" class="w-full mt-1 px-3 py-2 rounded border">
|
|
||||||
<option value="pages">Pages</option>
|
|
||||||
<option value="chapter">Chapter</option>
|
|
||||||
<option value="percentage">Percentage</option>
|
|
||||||
<option value="time-left">Time Left</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<!-- Reading Theme (reflowable only) -->
|
|
||||||
<div class="mb-6" x-show="!isFixedLayout">
|
|
||||||
<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>
|
|
||||||
<label class="block mb-2">
|
|
||||||
<select name="reading_theme" x-model="readingTheme" @change="applyTheme()" class="w-full mt-1 px-3 py-2 rounded border">
|
|
||||||
<optgroup label="📖 Classic Reading">
|
|
||||||
<option value="light">Light</option>
|
|
||||||
<option value="paper">Paper</option>
|
|
||||||
<option value="sepia">Sepia</option>
|
|
||||||
<option value="parchment">Parchment</option>
|
|
||||||
<option value="warm">Warm</option>
|
|
||||||
<option value="candlelight">Candlelight</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="🌤️ Sky & Atmosphere">
|
|
||||||
<option value="azure">Azure</option>
|
|
||||||
<option value="sky">Sky</option>
|
|
||||||
<option value="arctic">Arctic</option>
|
|
||||||
<option value="frost">Frost</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="🌅 Sunset & Warmth">
|
|
||||||
<option value="dusk">Dusk</option>
|
|
||||||
<option value="sunset">Sunset</option>
|
|
||||||
<option value="twilight">Twilight</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="🌲 Nature & Earth">
|
|
||||||
<option value="forest">Forest</option>
|
|
||||||
<option value="moss">Moss</option>
|
|
||||||
<option value="slate">Slate</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="⚡ High Performance">
|
|
||||||
<option value="oled">OLED</option>
|
|
||||||
<option value="solarized">Solarized</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<!-- Typography (reflowable only) -->
|
|
||||||
<div class="mb-6" x-show="!isFixedLayout">
|
|
||||||
<h3 class="font-semibold mb-2">Typography</h3>
|
|
||||||
<label class="block mb-2">
|
|
||||||
Reading Font
|
|
||||||
<select name="reading_font" x-model="readingFont" @change="applyFont()" class="w-full mt-1 px-3 py-2 rounded border">
|
|
||||||
<option value="literata">Literata (Default)</option>
|
|
||||||
<option value="crimson">Crimson Text</option>
|
|
||||||
<option value="source-serif">Source Serif 4</option>
|
|
||||||
<option value="eb-garamond">EB Garamond</option>
|
|
||||||
<option value="libertinus">Libertinus Serif</option>
|
|
||||||
<option value="noto-serif">Noto Serif</option>
|
|
||||||
<option value="charis-sil">Charis SIL</option>
|
|
||||||
<option value="ibm-plex">IBM Plex Serif</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="block mb-2">
|
|
||||||
Font Size
|
|
||||||
<input type="range" name="font_size" x-model.number="fontSize" @change="applyFont()" min="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>
|
|
||||||
<!-- Navigation -->
|
|
||||||
<div class="mb-6" x-show="isFixedLayout">
|
|
||||||
<h3 class="font-semibold mb-2">Navigation</h3>
|
|
||||||
<label class="flex items-center mb-2">
|
|
||||||
<input type="checkbox" name="double_page_spread" 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="toggleSettings()" class="flex-1 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ReaderTOCPanel(_ ReaderMetadata) {
|
templ ReaderTOCDrawer() {
|
||||||
<div
|
@drawerHeader("Contents")
|
||||||
class="dockable-panel"
|
<div class="reader-drawer-body">
|
||||||
data-panel="toc"
|
<nav id="toc-list" class="space-y-1">
|
||||||
data-side="left"
|
<template x-for="item in tocItems" :key="item.href">
|
||||||
x-ref="tocPanel"
|
<a
|
||||||
>
|
href="#"
|
||||||
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.tocPanel)">
|
@click.prevent="goToTOCItem(item)"
|
||||||
<h3 class="panel-title font-semibold">📖 Table of Contents</h3>
|
class="block py-1 px-2 rounded hover:bg-gray-700 text-sm"
|
||||||
<div class="panel-controls flex items-center gap-2">
|
:style="'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'"
|
||||||
<button class="window-shade-toggle">─</button>
|
x-text="item.label"
|
||||||
</div>
|
></a>
|
||||||
</div>
|
</template>
|
||||||
<div class="panel-content p-4 overflow-y-auto">
|
<template x-if="tocItems.length === 0">
|
||||||
<nav id="toc-list" class="space-y-1">
|
<p class="text-sm py-2" style="color: var(--text-secondary)">No table of contents</p>
|
||||||
<template x-for="item in tocItems" :key="item.href">
|
</template>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ ReaderBookmarksDrawer() {
|
||||||
|
@drawerHeader("Bookmarks")
|
||||||
|
<div class="reader-drawer-body">
|
||||||
|
<div id="bookmarks-list" class="space-y-2">
|
||||||
|
<template x-for="bookmark in bookmarkItems" :key="bookmark.id">
|
||||||
|
<div class="flex items-center gap-1 group">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="goToTOCItem(item)"
|
@click.prevent="goToBookmark(bookmark)"
|
||||||
class="block py-1 px-2 rounded hover:bg-gray-700 text-sm"
|
class="flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2"
|
||||||
:style="'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'"
|
>
|
||||||
x-text="item.label"
|
<span class="font-medium block truncate" x-text="bookmark.title"></span>
|
||||||
></a>
|
<span class="text-xs block truncate" style="color: var(--text-secondary)" x-text="bookmark.positionLabel"></span>
|
||||||
</template>
|
</a>
|
||||||
</nav>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
<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 ReaderBookmarksPanel() {
|
templ ReaderSettingsDrawer() {
|
||||||
<div
|
@drawerHeader("Settings")
|
||||||
class="dockable-panel"
|
<div class="reader-drawer-body">
|
||||||
data-panel="bookmarks"
|
<!-- Behavior -->
|
||||||
data-side="right"
|
<div class="mb-6">
|
||||||
x-ref="bookmarksPanel"
|
<h3 class="font-semibold mb-2">Behavior</h3>
|
||||||
>
|
<label class="block mb-2">
|
||||||
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.bookmarksPanel)">
|
Chrome
|
||||||
<h3 class="panel-title font-semibold">🔖 Bookmarks</h3>
|
<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);">
|
||||||
<div class="panel-controls flex items-center gap-2">
|
<option value="auto-hide">Auto Hide</option>
|
||||||
<button class="window-shade-toggle">─</button>
|
<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>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-content p-4 overflow-y-auto">
|
<!-- Typography (reflowable only) -->
|
||||||
<div id="bookmarks-list" class="space-y-2">
|
<div class="mb-6" x-show="!isFixedLayout">
|
||||||
<template x-for="bookmark in bookmarkItems" :key="bookmark.id">
|
<h3 class="font-semibold mb-2">Typography</h3>
|
||||||
<div class="flex items-center gap-1 group">
|
<label class="block mb-2">
|
||||||
<a
|
Reading Font
|
||||||
href="#"
|
<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);">
|
||||||
@click.prevent="goToBookmark(bookmark)"
|
<option value="literata">Literata (Default)</option>
|
||||||
class="flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2"
|
<option value="crimson">Crimson Text</option>
|
||||||
>
|
<option value="source-serif">Source Serif 4</option>
|
||||||
<span class="font-medium block truncate" x-text="bookmark.title"></span>
|
<option value="eb-garamond">EB Garamond</option>
|
||||||
<span class="text-xs block truncate" style="color: var(--text-secondary)" x-text="bookmark.positionLabel"></span>
|
<option value="libertinus">Libertinus Serif</option>
|
||||||
</a>
|
<option value="noto-serif">Noto Serif</option>
|
||||||
<button
|
<option value="charis-sil">Charis SIL</option>
|
||||||
@click="deleteBookmark(bookmark.id)"
|
<option value="ibm-plex">IBM Plex Serif</option>
|
||||||
class="p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity"
|
</select>
|
||||||
title="Delete bookmark"
|
</label>
|
||||||
aria-label="Delete bookmark"
|
<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"/>
|
||||||
</button>
|
<span class="text-xs" x-text="fontSize + 'px'" style="color: var(--text-secondary)">18px</span>
|
||||||
</div>
|
</label>
|
||||||
</template>
|
<label class="block mb-2">
|
||||||
<template x-if="bookmarkItems.length === 0">
|
Line Height
|
||||||
<p class="text-sm" style="color: var(--text-secondary)">No bookmarks yet</p>
|
<input type="range" name="line_height" x-model.number="lineHeight" @change="applyFont()" min="0.8" max="3.0" step="0.1" class="w-full"/>
|
||||||
</template>
|
<span class="text-xs" x-text="lineHeight" style="color: var(--text-secondary)">1.6</span>
|
||||||
</div>
|
</label>
|
||||||
<button @click="addBookmark()" class="w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700">
|
</div>
|
||||||
+ Add Bookmark
|
<!-- 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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+157
-78
@@ -125,39 +125,39 @@ 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, 5, "\">")
|
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></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = ReaderChrome(user, metadata, progress).Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = ReaderChrome(metadata, progress).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, 6, "<!-- Dockable Panels Container --><div id=\"reader-panels\" class=\"fixed inset-0 pointer-events-none z-30\"><!-- Left Sidebar (TOC) --><div id=\"left-sidebar\" class=\"absolute left-0 pointer-events-auto flex flex-col\" style=\"top: 56px; bottom: 56px;\"><div id=\"toc-panel\" x-show=\"tocOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"toc\">")
|
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\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = ReaderTOCPanel(metadata).Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = ReaderTOCDrawer().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, 7, "</div></div><!-- Right Sidebar (Settings, Bookmarks) --><div id=\"right-sidebar\" class=\"absolute right-0 pointer-events-auto flex flex-col\" style=\"top: 56px; bottom: 56px;\"><div id=\"settings-panel\" x-show=\"settingsOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"settings\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><!-- Settings drawer (right) --><div id=\"settings-drawer\" x-show=\"settingsOpen\" 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=\"Reader settings\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = ReaderSettingsPanel().Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = ReaderSettingsDrawer().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, 8, "</div><div id=\"bookmarks-panel\" x-show=\"bookmarksOpen\" x-transition class=\"panel-container pointer-events-auto\" data-panel=\"bookmarks\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Bookmarks drawer (right) --><div id=\"bookmarks-drawer\" x-show=\"bookmarksOpen\" 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=\"Bookmarks\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = ReaderBookmarksPanel().Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = ReaderBookmarksDrawer().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, 9, "</div></div></div><div id=\"reader-viewport\" class=\"absolute inset-x-0\" style=\"top: 56px; bottom: 56px;\"><foliate-view id=\"reader-view\" class=\"block w-full h-full\"></foliate-view></div></body></html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></body></html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) templ.Component {
|
func ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) templ.Component {
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
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
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
@@ -186,14 +186,14 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
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\"><!-- Top bar --><div id=\"reader-topbar\" class=\"fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center justify-between px-3 py-2 sm:px-4 sm:py-3\"><a id=\"reader-back\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div id=\"reader-chrome\" class=\"transition-opacity duration-300\" :class=\"chromeVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'\"><!-- Top bar --><div id=\"reader-topbar\" class=\"fixed top-0 left-0 right-0 backdrop-blur border-b z-40 pt-[env(safe-area-inset-top)]\" style=\"background-color: var(--bg-primary);\"><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: 101, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 146, 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 {
|
||||||
@@ -206,13 +206,13 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
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: 104, Col: 95}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 149, 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><button @click=\"toggleSettings()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Settings\">⚙️</button></div></div><!-- Bottom bar --><div id=\"reader-bottombar\" class=\"fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1\"><!-- Left navigation --><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><!-- Progress slider --><input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" class=\"grow\"> <datalist id=\"tick-marks\"></datalist><!-- Right navigation --><button @click=\"goRight()\" class=\"p-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><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Zoom controls (fixed-layout only) --><template x-if=\"isFixedLayout\"><div class=\"flex items-center gap-1\"><!-- Zoom out --><button @click=\"zoomOut()\" class=\"p-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><!-- Zoom percentage --><button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]\" title=\"Reset Zoom\" aria-label=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button><!-- Zoom in --><button @click=\"zoomIn()\" class=\"p-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><!-- Magnifier --><button @click=\"toggleMagnifier()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Magnifier\" aria-label=\"Toggle magnifier\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"9\" cy=\"9\" r=\"5\"></circle> <path d=\"M 13 13 L 18 18\"></path></svg></button><!-- Pan/Select mode (PDF only) --><button x-show=\"isPDF\" @click=\"toggleInteractionMode()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" :class=\"interactionMode === 'pan' ? 'bg-blue-600 hover:bg-blue-700' : ''\" :title=\"interactionMode === 'pan' ? 'Pan mode: drag anywhere to move. Click for Smart select.' : 'Smart select: drag text to select, drag blank to pan (Shift = force pan). Click for Pan mode.'\" :aria-label=\"interactionMode === 'pan' ? 'Pan mode' : 'Smart select mode'\"><!-- Smart select (default): text cursor --><svg x-show=\"interactionMode !== 'pan'\" class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 10 3 L 10 17 M 7 5 L 10 4 L 13 5 M 7 15 L 10 16 L 13 15\"></path></svg><!-- Force pan: move arrow --><svg x-show=\"interactionMode === 'pan'\" class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3\"></path></svg></button></div></template><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Progress display --><div id=\"progress-display\" @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, 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=\"Bookmarks\">📝</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 backdrop-blur border-t z-40 pb-[env(safe-area-inset-bottom)]\" style=\"background-color: var(--bg-primary);\"><!-- 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 bg-gray-600 mx-1\"></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 +221,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
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: 188, Col: 113}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 187, 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 +231,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
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: 190, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 189, 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 +242,39 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
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: 193, Col: 72}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 192, 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><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Action buttons --><div class=\"flex items-center gap-1\"><button @click=\"toggleTOC()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents\">📖</button> <button @click=\"addBookmark()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark this position\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmarks\">📝</button></div></div></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></div><div class=\"w-px h-6 bg-gray-600 mx-1\"></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 bg-gray-600 mx-1 shrink-0\"></div><!-- Fit mode --><select @change=\"applyFitMode($event.target.value)\" class=\"text-xs px-1.5 py-1 rounded-lg bg-transparent border shrink-0\" style=\"border-color: var(--border);\" 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 bg-gray-600 mx-1 shrink-0\"></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 bg-gray-600 mx-1 shrink-0\"></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 {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if progress.FormatGroup == "reflowable" {
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 270, Col: 51}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 272, Col: 72}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></div><div class=\"w-px h-6 bg-gray-600 mx-1 shrink-0\"></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
|
||||||
}
|
}
|
||||||
@@ -257,65 +282,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReaderSettingsPanel() templ.Component {
|
func drawerHeader(title string) 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_Var12 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var12 == nil {
|
|
||||||
templ_7745c5c3_Var12 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"dockable-panel\" data-panel=\"settings\" data-side=\"left\" x-ref=\"settingsPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.settingsPanel)\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" x-model=\"progressMode\" @change=\"applyProgressMode()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Reading Theme (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><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><label class=\"block mb-2\"><select name=\"reading_theme\" x-model=\"readingTheme\" @change=\"applyTheme()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select></label></div><!-- Typography (reflowable only) --><div class=\"mb-6\" x-show=\"!isFixedLayout\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" x-model=\"readingFont\" @change=\"applyFont()\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default)</option> <option value=\"crimson\">Crimson Text</option> <option value=\"source-serif\">Source Serif 4</option> <option value=\"eb-garamond\">EB Garamond</option> <option value=\"libertinus\">Libertinus Serif</option> <option value=\"noto-serif\">Noto Serif</option> <option value=\"charis-sil\">Charis SIL</option> <option value=\"ibm-plex\">IBM Plex Serif</option></select></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" x-model.number=\"fontSize\" @change=\"applyFont()\" min=\"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><!-- Navigation --><div class=\"mb-6\" x-show=\"isFixedLayout\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" 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=\"toggleSettings()\" class=\"flex-1 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReaderTOCPanel(_ ReaderMetadata) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
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_Var13 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var13 == nil {
|
|
||||||
templ_7745c5c3_Var13 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"dockable-panel\" data-panel=\"toc\" data-side=\"left\" x-ref=\"tocPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.tocPanel)\"><h3 class=\"panel-title font-semibold\">📖 Table of Contents</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><nav id=\"toc-list\" class=\"space-y-1\"><template x-for=\"item in tocItems\" :key=\"item.href\"><a href=\"#\" @click.prevent=\"goToTOCItem(item)\" class=\"block py-1 px-2 rounded hover:bg-gray-700 text-sm\" :style=\"'padding-left: ' + ((item.depth || 0) * 1 + 0.5) + 'rem'\" x-text=\"item.label\"></a></template></nav></div></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReaderBookmarksPanel() templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
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
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
@@ -336,7 +303,119 @@ func ReaderBookmarksPanel() 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, 16, "<div class=\"dockable-panel\" data-panel=\"bookmarks\" data-side=\"right\" x-ref=\"bookmarksPanel\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" @click=\"toggleWindowShade($refs.bookmarksPanel)\"><h3 class=\"panel-title font-semibold\">🔖 Bookmarks</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"window-shade-toggle\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><div id=\"bookmarks-list\" 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></div><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, 15, "<div class=\"reader-drawer-header\"><h3 class=\"font-semibold\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
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: 285, Col: 35}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
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>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReaderTOCDrawer() 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_Var16 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var16 == nil {
|
||||||
|
templ_7745c5c3_Var16 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = drawerHeader("Contents").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
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>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReaderBookmarksDrawer() 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_Var17 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var17 == nil {
|
||||||
|
templ_7745c5c3_Var17 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = drawerHeader("Bookmarks").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"reader-drawer-body\"><div id=\"bookmarks-list\" 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></div><button @click=\"addBookmark()\" class=\"w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Bookmark</button></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReaderSettingsDrawer() 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("Settings").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
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></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
|
||||||
}
|
}
|
||||||
|
|||||||
+136
-62
@@ -372,6 +372,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
interactionMode: "select" as string,
|
interactionMode: "select" as string,
|
||||||
magnifierEnabled: false,
|
magnifierEnabled: false,
|
||||||
doublePageSpread: true as boolean,
|
doublePageSpread: true as boolean,
|
||||||
|
chromeVisible: true,
|
||||||
|
chromeBehavior: "auto-hide" as string,
|
||||||
|
hideTimer: null as ReturnType<typeof setTimeout> | null,
|
||||||
progressText: "",
|
progressText: "",
|
||||||
progressLabel: "",
|
progressLabel: "",
|
||||||
progressMain: "",
|
progressMain: "",
|
||||||
@@ -414,6 +417,23 @@ document.addEventListener("alpine:init", () => {
|
|||||||
label: string;
|
label: string;
|
||||||
startPage: number;
|
startPage: number;
|
||||||
}[],
|
}[],
|
||||||
|
get themeSwatches(): {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
bg: string;
|
||||||
|
fg: string;
|
||||||
|
}[] {
|
||||||
|
const dark = this.readingMode === "dark";
|
||||||
|
return Object.entries(THEME_COLORS).map(([value, set]) => {
|
||||||
|
const colors = dark ? set.dark : set.light;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: value.charAt(0).toUpperCase() + value.slice(1),
|
||||||
|
bg: colors.bg,
|
||||||
|
fg: colors.fg,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
const link = document.getElementById("reader-back");
|
const link = document.getElementById("reader-back");
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
@@ -470,6 +490,13 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.fontSize = this.settings.font_size || 18;
|
this.fontSize = this.settings.font_size || 18;
|
||||||
this.lineHeight = this.settings.line_height || 1.6;
|
this.lineHeight = this.settings.line_height || 1.6;
|
||||||
this.doublePageSpread = this.settings.double_page_spread ?? true;
|
this.doublePageSpread = this.settings.double_page_spread ?? true;
|
||||||
|
const behavior = this.settings.chrome_behavior || "auto-hide";
|
||||||
|
this.chromeBehavior =
|
||||||
|
behavior === "always-visible" ? "always-visible" : "auto-hide";
|
||||||
|
const mode = this.settings.pdf_interaction_mode;
|
||||||
|
if (mode === "pan" || mode === "text" || mode === "select") {
|
||||||
|
this.interactionMode = mode;
|
||||||
|
}
|
||||||
if (this.settings.reading_mode) {
|
if (this.settings.reading_mode) {
|
||||||
this.readingMode = this.settings.reading_mode;
|
this.readingMode = this.settings.reading_mode;
|
||||||
} else {
|
} else {
|
||||||
@@ -504,6 +531,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.book.dir = "rtl";
|
this.book.dir = "rtl";
|
||||||
}
|
}
|
||||||
this.isPDF = (this.renderer as any).isPDF;
|
this.isPDF = (this.renderer as any).isPDF;
|
||||||
|
if (this.isPDF) {
|
||||||
|
this.renderer.setAttribute("interaction-mode", this.interactionMode);
|
||||||
|
}
|
||||||
this.renderer.addEventListener("zoom", () => {
|
this.renderer.addEventListener("zoom", () => {
|
||||||
this.zoomPercent = this.renderer.zoomPercent;
|
this.zoomPercent = this.renderer.zoomPercent;
|
||||||
});
|
});
|
||||||
@@ -521,6 +551,11 @@ document.addEventListener("alpine:init", () => {
|
|||||||
doc.addEventListener("keydown", (ev: KeyboardEvent) =>
|
doc.addEventListener("keydown", (ev: KeyboardEvent) =>
|
||||||
this.handleKeydown(ev),
|
this.handleKeydown(ev),
|
||||||
);
|
);
|
||||||
|
// Pointer activity inside page iframes should keep the chrome awake
|
||||||
|
// (fixed-layout pages and EPUB sections are separate documents).
|
||||||
|
doc.addEventListener("pointermove", () => this.pokeChrome(), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
if (!this.isFixedLayout) {
|
if (!this.isFixedLayout) {
|
||||||
this.computeChapterPageBoundaries(doc);
|
this.computeChapterPageBoundaries(doc);
|
||||||
doc.fonts.ready.then(() => this.computeChapterPageBoundaries(doc));
|
doc.fonts.ready.then(() => this.computeChapterPageBoundaries(doc));
|
||||||
@@ -545,10 +580,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
);
|
);
|
||||||
this.setProgress(progressParts);
|
this.setProgress(progressParts);
|
||||||
this.sliderValue = fraction;
|
this.sliderValue = fraction;
|
||||||
const slider = document.getElementById(
|
for (const slider of this.progressSliders()) {
|
||||||
"progress-slider",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
if (slider) {
|
|
||||||
slider.value = fraction;
|
slider.value = fraction;
|
||||||
}
|
}
|
||||||
const range = e.detail.range as Range | undefined;
|
const range = e.detail.range as Range | undefined;
|
||||||
@@ -558,11 +590,10 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
this.debouncedSaveProgress(fraction, location, cfi);
|
this.debouncedSaveProgress(fraction, location, cfi);
|
||||||
});
|
});
|
||||||
const slider = document.getElementById(
|
if (this.book.dir) {
|
||||||
"progress-slider",
|
for (const slider of this.progressSliders()) {
|
||||||
) as HTMLInputElement;
|
slider.dir = this.book.dir;
|
||||||
if (slider && this.book.dir) {
|
}
|
||||||
slider.dir = this.book.dir;
|
|
||||||
}
|
}
|
||||||
if (this.view.getSectionFractions) {
|
if (this.view.getSectionFractions) {
|
||||||
this.sectionFractionsArr = this.view.getSectionFractions();
|
this.sectionFractionsArr = this.view.getSectionFractions();
|
||||||
@@ -593,45 +624,43 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
this.initTime = Date.now();
|
this.initTime = Date.now();
|
||||||
this.fetchReadingSpeed();
|
this.fetchReadingSpeed();
|
||||||
this.setupViewportInsets();
|
this.setupChrome();
|
||||||
},
|
},
|
||||||
updateViewportInsets() {
|
// Chrome visibility: bars overlay the edge-to-edge reading surface.
|
||||||
const top = document.getElementById("reader-topbar");
|
// 'auto-hide' keeps them up while the pointer is active and fades them
|
||||||
const bot = document.getElementById("reader-bottombar");
|
// out after a quiet period; 'always-visible' pins them.
|
||||||
const vp = document.getElementById("reader-viewport");
|
setupChrome() {
|
||||||
if (!top || !bot || !vp) return;
|
document.addEventListener("pointermove", () => this.pokeChrome(), {
|
||||||
const margin = 6;
|
passive: true,
|
||||||
const topInset = `${top.offsetHeight + margin}px`;
|
});
|
||||||
const bottomInset = `${bot.offsetHeight + margin}px`;
|
if (this.chromeBehavior === "always-visible") {
|
||||||
vp.style.setProperty("top", topInset);
|
this.chromeVisible = true;
|
||||||
vp.style.setProperty("bottom", bottomInset);
|
return;
|
||||||
// Sidebars (TOC/settings/bookmarks panels) must sit below/above the
|
|
||||||
// chrome bars, tracking their measured heights (safe-area insets, wrap).
|
|
||||||
for (const id of ["left-sidebar", "right-sidebar"]) {
|
|
||||||
const sidebar = document.getElementById(id);
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.setProperty("top", topInset);
|
|
||||||
sidebar.style.setProperty("bottom", bottomInset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.pokeChrome();
|
||||||
},
|
},
|
||||||
setupViewportInsets() {
|
pokeChrome() {
|
||||||
const top = document.getElementById("reader-topbar");
|
this.chromeVisible = true;
|
||||||
const bot = document.getElementById("reader-bottombar");
|
if (this.chromeBehavior !== "auto-hide") return;
|
||||||
this.updateViewportInsets();
|
if (this.hideTimer) clearTimeout(this.hideTimer);
|
||||||
window.addEventListener("resize", () => this.updateViewportInsets());
|
this.hideTimer = setTimeout(() => {
|
||||||
window.addEventListener("orientationchange", () =>
|
if (this.anyDrawerOpen() || this.chromeHasFocus()) return;
|
||||||
setTimeout(() => this.updateViewportInsets(), 250),
|
this.chromeVisible = false;
|
||||||
|
}, 2500);
|
||||||
|
},
|
||||||
|
chromeHasFocus(): boolean {
|
||||||
|
const el = document.activeElement;
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = el.tagName;
|
||||||
|
return (
|
||||||
|
(tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") &&
|
||||||
|
!!el.closest("#reader-topbar, #reader-bottombar")
|
||||||
);
|
);
|
||||||
if (
|
},
|
||||||
typeof ResizeObserver !== "undefined" &&
|
applyChromeBehavior() {
|
||||||
top &&
|
if (this.chromeBehavior !== "auto-hide") this.chromeVisible = true;
|
||||||
bot
|
else this.pokeChrome();
|
||||||
) {
|
saveSettings({ chrome_behavior: this.chromeBehavior });
|
||||||
const ro = new ResizeObserver(() => this.updateViewportInsets());
|
|
||||||
ro.observe(top);
|
|
||||||
ro.observe(bot);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
debouncedSaveProgress(fraction: number, location: any, cfi: string) {
|
debouncedSaveProgress(fraction: number, location: any, cfi: string) {
|
||||||
if (Date.now() - this.initTime < 5000) return;
|
if (Date.now() - this.initTime < 5000) return;
|
||||||
@@ -712,13 +741,23 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.renderer.toggleMagnifier();
|
this.renderer.toggleMagnifier();
|
||||||
this.magnifierEnabled = this.renderer.zoomMagnifierEnabled;
|
this.magnifierEnabled = this.renderer.zoomMagnifierEnabled;
|
||||||
},
|
},
|
||||||
toggleInteractionMode() {
|
setInteractionMode(mode: string) {
|
||||||
if (!this.isFixedLayout) return;
|
if (!this.isFixedLayout || !this.isPDF) return;
|
||||||
const current =
|
if (mode !== "select" && mode !== "pan" && mode !== "text") return;
|
||||||
this.renderer.getAttribute("interaction-mode") || "select";
|
this.renderer.setAttribute("interaction-mode", mode);
|
||||||
const next = current === "select" ? "pan" : "select";
|
this.interactionMode = mode;
|
||||||
this.renderer.setAttribute("interaction-mode", next);
|
saveSettings({ pdf_interaction_mode: mode });
|
||||||
this.interactionMode = next;
|
},
|
||||||
|
applyFitMode(mode: string) {
|
||||||
|
if (!this.isFixedLayout || !this.renderer) return;
|
||||||
|
if (mode === "fit-width" || mode === "fit-page") {
|
||||||
|
this.renderer.setAttribute("zoom", mode);
|
||||||
|
this.renderer.dragOffset = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleSpread() {
|
||||||
|
this.doublePageSpread = !this.doublePageSpread;
|
||||||
|
this.applyDoublePageSpread();
|
||||||
},
|
},
|
||||||
applyDoublePageSpread() {
|
applyDoublePageSpread() {
|
||||||
if (!this.isFixedLayout || !this.renderer) return;
|
if (!this.isFixedLayout || !this.renderer) return;
|
||||||
@@ -744,19 +783,30 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.view?.goToFraction?.(parseFloat(value));
|
this.view?.goToFraction?.(parseFloat(value));
|
||||||
},
|
},
|
||||||
toggleTOC() {
|
toggleTOC() {
|
||||||
this.tocOpen = !this.tocOpen;
|
const opening = !this.tocOpen;
|
||||||
if (this.tocOpen && this.tocItems.length === 0 && this.book?.toc) {
|
this.closeDrawers();
|
||||||
|
this.tocOpen = opening;
|
||||||
|
if (opening && this.tocItems.length === 0 && this.book?.toc) {
|
||||||
this.tocItems = this.flattenTOC(this.book.toc);
|
this.tocItems = this.flattenTOC(this.book.toc);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleSettings() {
|
toggleSettings() {
|
||||||
this.settingsOpen = !this.settingsOpen;
|
const opening = !this.settingsOpen;
|
||||||
|
this.closeDrawers();
|
||||||
|
this.settingsOpen = opening;
|
||||||
},
|
},
|
||||||
toggleBookmarks() {
|
toggleBookmarks() {
|
||||||
this.bookmarksOpen = !this.bookmarksOpen;
|
const opening = !this.bookmarksOpen;
|
||||||
|
this.closeDrawers();
|
||||||
|
this.bookmarksOpen = opening;
|
||||||
},
|
},
|
||||||
toggleWindowShade(panelEl: HTMLElement) {
|
anyDrawerOpen(): boolean {
|
||||||
panelEl.classList.toggle("panel-collapsed");
|
return this.tocOpen || this.settingsOpen || this.bookmarksOpen;
|
||||||
|
},
|
||||||
|
closeDrawers() {
|
||||||
|
this.tocOpen = false;
|
||||||
|
this.settingsOpen = false;
|
||||||
|
this.bookmarksOpen = false;
|
||||||
},
|
},
|
||||||
flattenTOC(items: any[], depth = 0): any[] {
|
flattenTOC(items: any[], depth = 0): any[] {
|
||||||
const result: any[] = [];
|
const result: any[] = [];
|
||||||
@@ -846,6 +896,10 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.lineHeight = 1.6;
|
this.lineHeight = 1.6;
|
||||||
this.justify = true;
|
this.justify = true;
|
||||||
this.hyphenate = true;
|
this.hyphenate = true;
|
||||||
|
this.chromeBehavior = "auto-hide";
|
||||||
|
this.applyChromeBehavior();
|
||||||
|
this.interactionMode = "select";
|
||||||
|
this.setInteractionMode("select");
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
this.applyFont();
|
this.applyFont();
|
||||||
},
|
},
|
||||||
@@ -1072,14 +1126,19 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// The reflowable and fixed-layout bottom rows each have their own slider
|
||||||
|
// (only one is displayed); helpers below target whichever exists.
|
||||||
|
progressSliders(): HTMLInputElement[] {
|
||||||
|
const ids = ["progress-slider", "progress-slider-fx"];
|
||||||
|
return ids
|
||||||
|
.map((id) => document.getElementById(id) as HTMLInputElement | null)
|
||||||
|
.filter((el): el is HTMLInputElement => !!el);
|
||||||
|
},
|
||||||
setProgress(parts: { label: string; main: string }, sliderTitle?: string) {
|
setProgress(parts: { label: string; main: string }, sliderTitle?: string) {
|
||||||
this.progressLabel = parts.label;
|
this.progressLabel = parts.label;
|
||||||
this.progressMain = parts.main;
|
this.progressMain = parts.main;
|
||||||
this.progressText = parts.label + parts.main;
|
this.progressText = parts.label + parts.main;
|
||||||
const slider = document.getElementById(
|
for (const slider of this.progressSliders()) {
|
||||||
"progress-slider",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
if (slider) {
|
|
||||||
slider.title = sliderTitle ?? this.progressText;
|
slider.title = sliderTitle ?? this.progressText;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1213,15 +1272,30 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
handleKeydown(event: KeyboardEvent) {
|
handleKeydown(event: KeyboardEvent) {
|
||||||
const k = event.key;
|
const k = event.key;
|
||||||
|
// Never hijack keys while the user is typing in a form control.
|
||||||
|
const tag = (event.target as HTMLElement)?.tagName;
|
||||||
|
const typing =
|
||||||
|
tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA";
|
||||||
|
this.pokeChrome();
|
||||||
if (k === "ArrowLeft" || k === "h") this.goLeft();
|
if (k === "ArrowLeft" || k === "h") this.goLeft();
|
||||||
else if (k === "ArrowRight" || k === "l") this.goRight();
|
else if (k === "ArrowRight" || k === "l") this.goRight();
|
||||||
else if (k === "+" || k === "=") this.zoomIn();
|
else if (k === "+" || k === "=") this.zoomIn();
|
||||||
else if (k === "-" || k === "_") this.zoomOut();
|
else if (k === "-" || k === "_") this.zoomOut();
|
||||||
else if (k === "0") this.resetZoom();
|
else if (k === "0") this.resetZoom();
|
||||||
else if (k === "Escape") {
|
else if (k === "Escape") {
|
||||||
if (this.isFixedLayout && this.renderer?.zoomMagnifierEnabled) {
|
if (this.anyDrawerOpen()) {
|
||||||
|
this.closeDrawers();
|
||||||
|
} else if (this.isFixedLayout && this.renderer?.zoomMagnifierEnabled) {
|
||||||
this.toggleMagnifier();
|
this.toggleMagnifier();
|
||||||
|
} else {
|
||||||
|
// Toggle chrome without a fresh auto-hide timer.
|
||||||
|
this.chromeVisible = !this.chromeVisible;
|
||||||
|
if (this.hideTimer) clearTimeout(this.hideTimer);
|
||||||
}
|
}
|
||||||
|
} else if (!typing) {
|
||||||
|
if (k === "t") this.toggleTOC();
|
||||||
|
else if (k === "s") this.toggleSettings();
|
||||||
|
else if (k === "b") this.addBookmark();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export function getDefaultSettings(): ReaderSettings {
|
|||||||
line_height: 1.6,
|
line_height: 1.6,
|
||||||
margin_width: 20,
|
margin_width: 20,
|
||||||
double_page_spread: true,
|
double_page_spread: true,
|
||||||
|
pdf_interaction_mode: "select",
|
||||||
reading_direction: "ltr",
|
reading_direction: "ltr",
|
||||||
hardware_acceleration: true,
|
hardware_acceleration: true,
|
||||||
panel_layout: {
|
panel_layout: {
|
||||||
|
|||||||
Vendored
+2
-1
@@ -189,7 +189,7 @@ interface ReaderSettings {
|
|||||||
progress_mode: "pages" | "chapter" | "percentage" | "time-left";
|
progress_mode: "pages" | "chapter" | "percentage" | "time-left";
|
||||||
|
|
||||||
chrome_theme: string;
|
chrome_theme: string;
|
||||||
reading_theme: "light" | "sepia" | "dark" | "night" | "high-contrast";
|
reading_theme: string;
|
||||||
|
|
||||||
reading_font:
|
reading_font:
|
||||||
| "literata"
|
| "literata"
|
||||||
@@ -208,6 +208,7 @@ interface ReaderSettings {
|
|||||||
auto_scroll: boolean;
|
auto_scroll: boolean;
|
||||||
|
|
||||||
double_page_spread: boolean;
|
double_page_spread: boolean;
|
||||||
|
pdf_interaction_mode: "select" | "pan" | "text";
|
||||||
reading_direction: "ltr" | "rtl" | "vertical";
|
reading_direction: "ltr" | "rtl" | "vertical";
|
||||||
reading_mode: "dark" | "light";
|
reading_mode: "dark" | "light";
|
||||||
|
|
||||||
|
|||||||
+59
-20
@@ -725,7 +725,7 @@
|
|||||||
border: 1px solid var(--wood-border);
|
border: 1px solid var(--wood-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Reader ---------- */
|
/* ---------- Reader chrome & drawers ---------- */
|
||||||
.reader-icon {
|
.reader-icon {
|
||||||
display: block;
|
display: block;
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -735,32 +735,71 @@
|
|||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dockable-panel {
|
.drawer-scrim {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 45;
|
||||||
|
}
|
||||||
|
.reader-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 340px;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
.reader-drawer.left {
|
||||||
|
left: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.reader-drawer.right {
|
||||||
|
right: 0;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.reader-drawer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
transition: max-height 0.2s ease-out;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.reader-drawer-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.reader-seg {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.dockable-panel.panel-collapsed .panel-content {
|
.reader-seg button {
|
||||||
display: none;
|
padding: 0.25rem 0.6rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
.panel-header {
|
.reader-seg button.active {
|
||||||
user-select: none;
|
background-color: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
.panel-header:hover {
|
.theme-swatch {
|
||||||
background-color: var(--surface-hover);
|
height: 2.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.panel-container {
|
.theme-swatch:hover {
|
||||||
background-color: var(--bg-secondary);
|
border-color: #3b82f6;
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
width: 320px;
|
|
||||||
max-width: calc(100vw - 1rem);
|
|
||||||
max-height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
#right-sidebar .panel-container {
|
|
||||||
border-right: none;
|
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Series stacked covers ---------- */
|
/* ---------- Series stacked covers ---------- */
|
||||||
|
|||||||
Reference in New Issue
Block a user