Two root causes fixed: 1. Reading theme CSS variables were on document.body, leaking font/color into chrome UI. Now scoped to #reader-viewport so chrome keeps its own theme (system font, --text-primary colors) while the reading area uses reading theme colors/background. 2. getCSS() never received font family, font size, or line height settings. The settings UI (dropdowns, sliders) saved values but they were never injected into the book's shadow DOM. Now getCSS() accepts all four settings and generates proper CSS rules for them. Changes: - Wrap foliate-view in #reader-viewport div (absolute positioned between chrome bars) - getCSS() reads computed style from #reader-viewport, not document.body - getCSS() params expanded: fontFamily, fontSize, lineHeight, justify, hyphenate (removed unused 'spacing') - Added FONT_MAP to translate setting keys to CSS font-family values - applyTheme() targets #reader-viewport instead of document.body - Removed dead #reader-viewport typography rules from foliate-themes.css (shadow DOM doesn't inherit outer styles), kept only background-color
374 lines
15 KiB
Templ
374 lines
15 KiB
Templ
package templates
|
|
|
|
import "fmt"
|
|
|
|
templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookmarks []Bookmark) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>{ metadata.Title } - Bookhoard Reader</title>
|
|
<link rel="manifest" href="/static/manifest.json"/>
|
|
<link href="/static/reader-fonts.css" rel="stylesheet"/>
|
|
<link href="/static/foliate-themes.css" rel="stylesheet"/>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script type="module" src="/static/reader.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body
|
|
x-data="readerShell"
|
|
x-init={ fmt.Sprintf(`initReader({mediaItemId:'%s',fileUrl:'%s',formatGroup:'%s',readingDirection:'%s',mangaType:'%s'})`, metadata.MediaItemID, metadata.FileURL, metadata.FormatGroup, metadata.ReadingDirection, metadata.MangaType) }
|
|
class="theme-tokyo-night h-screen overflow-hidden"
|
|
>
|
|
@ReaderChrome(user, metadata, progress)
|
|
<!-- Dockable Panels Container -->
|
|
<div id="reader-panels" class="fixed inset-0 pointer-events-none z-30">
|
|
<!-- Left Sidebar (TOC, Settings) -->
|
|
<div id="left-sidebar" class="absolute left-0 top-0 bottom-0 pointer-events-auto flex flex-col">
|
|
<div id="toc-panel" x-show="tocOpen" x-transition class="panel-container pointer-events-auto" data-panel="toc">
|
|
@ReaderTOCPanel(metadata)
|
|
</div>
|
|
<div id="settings-panel" x-show="settingsOpen" x-transition class="panel-container pointer-events-auto" data-panel="settings">
|
|
@ReaderSettingsPanel()
|
|
</div>
|
|
</div>
|
|
<!-- Right Sidebar (Navigator, Bookmarks) -->
|
|
<div id="right-sidebar" class="absolute right-0 top-0 bottom-0 pointer-events-auto flex flex-col">
|
|
<div id="navigator-panel" x-show="navigatorOpen" x-transition class="panel-container pointer-events-auto" data-panel="navigator">
|
|
@ReaderNavigatorPanel()
|
|
</div>
|
|
<div id="bookmarks-panel" x-show="bookmarksOpen" x-transition class="panel-container pointer-events-auto" data-panel="bookmarks">
|
|
@ReaderBookmarksPanel(bookmarks)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="reader-viewport" class="absolute inset-x-0 top-[52px] bottom-[52px]">
|
|
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
|
</div>
|
|
@DictionaryPopup()
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) {
|
|
<div id="reader-chrome" class="transition-opacity duration-300">
|
|
<!-- Top bar -->
|
|
<div class="fixed top-0 left-0 right-0 bg-opacity-95 backdrop-blur border-b z-40" style="background-color: var(--bg-primary);">
|
|
<div class="flex items-center justify-between px-4 py-3">
|
|
<a href="/media-items/{ metadata.MediaItemID }" class="text-lg hover:underline">
|
|
← Back
|
|
</a>
|
|
<h1 class="text-lg font-semibold">{ metadata.Title }</h1>
|
|
<button
|
|
@click="toggleSettings()"
|
|
class="p-2 rounded-lg hover:bg-gray-700"
|
|
title="Settings"
|
|
>
|
|
⚙️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Bottom bar -->
|
|
<div class="fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40" style="background-color: var(--bg-primary);">
|
|
<div class="flex items-center px-2 py-2 gap-1">
|
|
<!-- Left navigation -->
|
|
<button @click="goLeft()" class="p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
|
<path d="M 15 6 L 9 12 L 15 18"></path>
|
|
</svg>
|
|
</button>
|
|
<!-- Progress slider -->
|
|
<input
|
|
id="progress-slider"
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="any"
|
|
list="tick-marks"
|
|
@input="goToFraction($event.target.value)"
|
|
class="grow"
|
|
/>
|
|
<datalist id="tick-marks"></datalist>
|
|
<!-- Right navigation -->
|
|
<button @click="goRight()" class="p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
|
<path d="M 9 6 L 15 12 L 9 18"></path>
|
|
</svg>
|
|
</button>
|
|
<!-- Separator -->
|
|
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
|
<!-- Zoom controls (fixed-layout only) -->
|
|
<template x-if="isFixedLayout">
|
|
<div class="flex items-center gap-1">
|
|
<!-- Zoom out -->
|
|
<button @click="zoomOut()" class="p-2 rounded-lg hover:bg-gray-700" title="Zoom Out" aria-label="Zoom out">
|
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
<path d="M 5 10 L 15 10"></path>
|
|
</svg>
|
|
</button>
|
|
<!-- Zoom percentage -->
|
|
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom" aria-label="Reset zoom">
|
|
<span x-text="zoomPercent + '%'">100%</span>
|
|
</button>
|
|
<!-- Zoom in -->
|
|
<button @click="zoomIn()" class="p-2 rounded-lg hover:bg-gray-700" title="Zoom In" aria-label="Zoom in">
|
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
|
</svg>
|
|
</button>
|
|
<!-- Magnifier -->
|
|
<button @click="toggleMagnifier()" class="p-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle magnifier">
|
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
<circle cx="9" cy="9" r="5"></circle>
|
|
<path d="M 13 13 L 18 18"></path>
|
|
</svg>
|
|
</button>
|
|
<!-- Pan/Select mode (PDF only) -->
|
|
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-2 rounded-lg hover:bg-gray-700" title="Pan/Select Mode" aria-label="Toggle pan/select mode">
|
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
|
<path d="M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<!-- Separator -->
|
|
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
|
<!-- Progress display -->
|
|
<div id="progress-display" x-text="progressText" data-progress-mode="pages" class="text-sm min-w-[4rem] text-center">
|
|
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
|
</div>
|
|
<!-- Separator -->
|
|
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
|
<!-- Action buttons -->
|
|
<div class="flex items-center gap-1">
|
|
<button @click="toggleTOC()" class="p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
|
<button @click="addBookmark()" class="p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
|
|
<button @click="toggleBookmarks()" class="p-2 rounded-lg hover:bg-gray-700" title="Notes">📝</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ ReaderSettingsPanel() {
|
|
<div
|
|
class="dockable-panel"
|
|
data-panel="settings"
|
|
data-side="left"
|
|
x-ref="settingsPanel"
|
|
>
|
|
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.settingsPanel)">
|
|
<h3 class="panel-title font-semibold">⚙️ Settings</h3>
|
|
<div class="panel-controls flex items-center gap-2">
|
|
<button class="window-shade-toggle">─</button>
|
|
</div>
|
|
</div>
|
|
<div class="panel-content p-4 overflow-y-auto">
|
|
<!-- Display settings -->
|
|
<div class="mb-6">
|
|
<h3 class="font-semibold mb-2">Display</h3>
|
|
<label class="block mb-2">
|
|
Chrome Behavior
|
|
<select name="chrome_behavior" class="w-full mt-1 px-3 py-2 rounded border">
|
|
<option value="auto-hide">Auto Hide</option>
|
|
<option value="always-visible">Always Visible</option>
|
|
<option value="hide-on-scroll">Hide on Scroll</option>
|
|
</select>
|
|
</label>
|
|
<label class="block mb-2">
|
|
Progress Mode
|
|
<select name="progress_mode" class="w-full mt-1 px-3 py-2 rounded border">
|
|
<option value="pages">Pages</option>
|
|
<option value="chapter">Chapter</option>
|
|
<option value="percentage">Percentage</option>
|
|
<option value="time-left">Time Left</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<!-- Reading Theme (reflowable only) -->
|
|
<div class="mb-6" x-show="!isFixedLayout">
|
|
<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="12" max="24" class="w-full"/>
|
|
<span class="text-xs" x-text="fontSize + 'px'" style="color: var(--text-secondary)">16px</span>
|
|
</label>
|
|
<label class="block mb-2">
|
|
Line Height
|
|
<input type="range" name="line_height" x-model.number="lineHeight" @change="applyFont()" min="1.0" max="2.5" step="0.1" class="w-full"/>
|
|
<span class="text-xs" x-text="lineHeight" style="color: var(--text-secondary)">1.6</span>
|
|
</label>
|
|
</div>
|
|
<!-- Navigation -->
|
|
<div class="mb-6" x-show="isFixedLayout">
|
|
<h3 class="font-semibold mb-2">Navigation</h3>
|
|
<label class="flex items-center mb-2">
|
|
<input type="checkbox" name="double_page_spread" class="mr-2"/>
|
|
Double Page Spread
|
|
</label>
|
|
</div>
|
|
<button @click="toggleSettings()" class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
|
Done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ ReaderTOCPanel(_ ReaderMetadata) {
|
|
<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>
|
|
}
|
|
|
|
templ ReaderNavigatorPanel() {
|
|
<div
|
|
class="dockable-panel"
|
|
data-panel="navigator"
|
|
data-side="right"
|
|
x-ref="navigatorPanel"
|
|
>
|
|
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.navigatorPanel)">
|
|
<h3 class="panel-title font-semibold">🗺️ Navigator</h3>
|
|
<div class="panel-controls flex items-center gap-2">
|
|
<button class="window-shade-toggle">─</button>
|
|
</div>
|
|
</div>
|
|
<div class="panel-content p-2 overflow-hidden">
|
|
<div id="navigator-viewport" class="relative w-full h-full">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ ReaderBookmarksPanel(bookmarks []Bookmark) {
|
|
<div
|
|
class="dockable-panel"
|
|
data-panel="bookmarks"
|
|
data-side="right"
|
|
x-ref="bookmarksPanel"
|
|
>
|
|
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.bookmarksPanel)">
|
|
<h3 class="panel-title font-semibold">🔖 Bookmarks</h3>
|
|
<div class="panel-controls flex items-center gap-2">
|
|
<button class="window-shade-toggle">─</button>
|
|
</div>
|
|
</div>
|
|
<div class="panel-content p-4 overflow-y-auto">
|
|
if len(bookmarks) > 0 {
|
|
<div id="bookmarks-list" class="space-y-2">
|
|
for _, bookmark := range bookmarks {
|
|
<a
|
|
href="#"
|
|
@click.prevent="goToBookmarkTarget($el.dataset.cfi)"
|
|
data-cfi={ bookmark.CfiPosition }
|
|
class="block py-2 hover:bg-gray-700 rounded px-2"
|
|
>
|
|
<span class="font-medium">{ bookmark.Title }</span>
|
|
<span class="text-xs ml-2" style="color: var(--text-secondary)">
|
|
{ bookmark.Position }
|
|
</span>
|
|
</a>
|
|
}
|
|
</div>
|
|
} else {
|
|
<p class="text-sm" style="color: var(--text-secondary)">No bookmarks yet</p>
|
|
}
|
|
<button @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 DictionaryPopup() {
|
|
<div id="dictionary-popup" class="hidden fixed bg-white text-black p-4 rounded-lg shadow-xl max-w-md z-50"></div>
|
|
}
|