refactor(reader): rewrite reader module for foliate-js pan/zoom integration
Major rewrite of the web reader to properly interface with
@bookhoard/foliate-js, replacing the abandoned panel-detection
architecture with direct pan and zoom support built into the
foliate-js FixedLayout renderer.
Template (reader.templ):
- Fix critical bug: x-init config was using literal strings
'{ readerData.X }' inside a quoted attribute, which templ
treated as raw text and never interpolated. Values were never
actually passed to JavaScript. Now uses fmt.Sprintf() with
templ's expression attribute syntax ={ }.
- Pass fileUrl from server so foliate-js can open books directly.
- Redesign bottom bar with foliate-js parity: left/right navigation
buttons, progress slider with tick marks, and zoom controls
(zoom out, percentage display, zoom in, magnifier, pan/select
mode toggle for PDFs).
- Remove panel editor button and enablePanelDetection config.
- Add SVG icon styles for consistent reader controls.
Go types (templates/types.go):
- Expand ReaderMetadata with FormatGroup, MangaType,
ReadingDirection, FileURL, and LibraryID fields needed by
the reader frontend.
Router (internal/router/reader.go):
- Populate new ReaderMetadata fields from database values.
- Construct FileURL from library ID and file path for the
/uploads/library-{id}/* file serving route.
Reader JS (reader.ts):
- Full rewrite modeled on foliate-js Reader class, adapted for
Alpine.js. Opens books via view.open(fileUrl), accesses
view.renderer for zoom/pan/navigation, and wires up keyboard
shortcuts (+/-/0 for zoom, arrows for nav, Escape for magnifier).
- Uses view.isFixedLayout instead of importing FixedLayout class,
avoiding a TypeScript module resolution issue with the Vite alias.
Settings manager (settings-manager.ts):
- Remove dependency on deleted ReaderContext event bus.
- Export loadSettings/saveSettings/syncSettings directly as
standalone async functions.
Cleanup:
- Delete reader-context.ts and reader-events.ts (over-engineered
event system replaced by direct function calls).
- Remove panel_zoom_enabled from ReaderSettings type.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"bookhoard/internal/services"
|
||||
"bookhoard/templates"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -82,16 +83,22 @@ func registerReaderRoutes(cfg *Config) {
|
||||
})
|
||||
// Convert to template types
|
||||
mediaUUID, _ := uuid.FromBytes(mediaItem.ID.Bytes[0:16])
|
||||
libUUID, _ := uuid.FromBytes(mediaItem.LibraryID.Bytes[0:16])
|
||||
metadata := templates.ReaderMetadata{
|
||||
MediaItemID: mediaUUID.String(),
|
||||
Title: mediaItem.Title,
|
||||
Author: textToString(mediaItem.Author),
|
||||
CoverImagePath: textToString(mediaItem.CoverImagePath),
|
||||
LibraryType: mediaItem.FormatGroup,
|
||||
MimeType: textToString(mediaItem.MimeType),
|
||||
FilePath: mediaItem.FilePath,
|
||||
TotalPages: int(mediaItem.PageCount.Int32),
|
||||
ChapterCount: int(mediaItem.ChapterCount.Int32),
|
||||
MediaItemID: mediaUUID.String(),
|
||||
Title: mediaItem.Title,
|
||||
Author: textToString(mediaItem.Author),
|
||||
CoverImagePath: textToString(mediaItem.CoverImagePath),
|
||||
LibraryType: mediaItem.FormatGroup,
|
||||
MimeType: textToString(mediaItem.MimeType),
|
||||
FilePath: mediaItem.FilePath,
|
||||
TotalPages: int(mediaItem.PageCount.Int32),
|
||||
ChapterCount: int(mediaItem.ChapterCount.Int32),
|
||||
FormatGroup: mediaItem.FormatGroup,
|
||||
MangaType: textToString(mediaItem.MangaType),
|
||||
ReadingDirection: textToString(mediaItem.ReadingDirection),
|
||||
LibraryID: libUUID.String(),
|
||||
FileURL: fmt.Sprintf("/uploads/library-%s/%s", libUUID.String(), mediaItem.FilePath),
|
||||
}
|
||||
// Progress conversion (inline)
|
||||
progressUUID, _ := uuid.FromBytes(progress.ID.Bytes[0:16])
|
||||
|
||||
+87
-31
@@ -15,18 +15,23 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/reader.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.reader-icon {
|
||||
display: block;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#progress-slider {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
x-data="readerShell"
|
||||
x-init="initReader({
|
||||
mediaItemId: '{ readerData.MediaItemID }',
|
||||
title: '{ readerData.Title }',
|
||||
enablePanelDetection: { readerData.EnablePanelDetection },
|
||||
libraryType: '{ readerData.LibraryType }',
|
||||
formatGroup: '{ readerData.FormatGroup }',
|
||||
mangaType: '{ readerData.MangaType }',
|
||||
readingDirection: '{ readerData.ReadingDirection }'
|
||||
})"
|
||||
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"
|
||||
>
|
||||
@ReaderChrome(user, metadata, progress)
|
||||
@@ -77,25 +82,80 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
</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 justify-between px-4 py-3">
|
||||
<div id="progress-display" data-progress-mode="pages">
|
||||
<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="flex-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>
|
||||
<div class="flex items-center gap-4">
|
||||
<button data-action="toggle-toc" title="Table of Contents">📖</button>
|
||||
<button data-action="add-bookmark" title="Bookmark">🏷️</button>
|
||||
<button data-action="add-note" title="Note">📝</button>
|
||||
<!-- Panel editor - shown for comics/manga -->
|
||||
<button
|
||||
x-data="panelEditor"
|
||||
x-show="isComicOrManga"
|
||||
@click="openPanelEditor(readerShell.currentPage)"
|
||||
data-action="edit-panels"
|
||||
title="Edit Panels"
|
||||
class="p-2 rounded-lg hover:bg-gray-700"
|
||||
>
|
||||
🎨
|
||||
</button>
|
||||
<!-- Separator -->
|
||||
<div class="w-px h-6 bg-gray-600 mx-1"></div>
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button data-action="toggle-toc" class="p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
||||
<button data-action="add-bookmark" class="p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
|
||||
<button data-action="add-note" class="p-2 rounded-lg hover:bg-gray-700" title="Note">📝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +249,7 @@ templ ReaderSettingsPanel() {
|
||||
<option value="charis-sil">Charis SIL (Multilingual)</option>
|
||||
<option value="ibm-plex">IBM Plex Serif (Modern)</option>
|
||||
</select>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">8 libre fonts bundled with Bookhoard</p>
|
||||
<p class="text-xs ml-2" style="color: var(--text-secondary)">8 libre fonts bundled with Bookhoard</p>
|
||||
</label>
|
||||
<label class="block mb-2">
|
||||
Font Size
|
||||
@@ -205,10 +265,6 @@ templ ReaderSettingsPanel() {
|
||||
<!-- Navigation -->
|
||||
<div class="mb-6">
|
||||
<h3 class="font-semibold mb-2">Navigation</h3>
|
||||
<label class="flex items-center mb-2">
|
||||
<input type="checkbox" name="panel_zoom_enabled" class="mr-2"/>
|
||||
Panel Zoom (Comics/Manga)
|
||||
</label>
|
||||
<label class="flex items-center mb-2">
|
||||
<input type="checkbox" name="double_page_spread" class="mr-2"/>
|
||||
Double Page Spread (Comics/Manga)
|
||||
|
||||
+84
-71
@@ -44,7 +44,20 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Reader</title><link rel=\"manifest\" href=\"/static/manifest.json\"><link href=\"/static/reader-fonts.css\" rel=\"stylesheet\"><link href=\"/static/foliate-themes.css\" rel=\"stylesheet\"><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/reader.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"readerShell\" x-init=\"initReader({\n\t\t\t\tmediaItemId: '{ readerData.MediaItemID }',\n\t\t\t\ttitle: '{ readerData.Title }',\n\t\t\t\tenablePanelDetection: { readerData.EnablePanelDetection },\n\t\t\t\tlibraryType: '{ readerData.LibraryType }',\n\t\t\t\tformatGroup: '{ readerData.FormatGroup }',\n\t\t\t\tmangaType: '{ readerData.MangaType }',\n\t\t\t\treadingDirection: '{ readerData.ReadingDirection }'\n\t\t\t })\" class=\"theme-tokyo-night\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Reader</title><link rel=\"manifest\" href=\"/static/manifest.json\"><link href=\"/static/reader-fonts.css\" rel=\"stylesheet\"><link href=\"/static/foliate-themes.css\" rel=\"stylesheet\"><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/reader.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><style>\n\t\t\t\t.reader-icon {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tfill: none;\n\t\t\t\t\tstroke: currentColor;\n\t\t\t\t\tstroke-width: 2px;\n\t\t\t\t\tstroke-linecap: round;\n\t\t\t\t\tstroke-linejoin: round;\n\t\t\t\t}\n\t\t\t\t#progress-slider {\n\t\t\t\t\tflex-grow: 1;\n\t\t\t\t}\n\t\t\t</style></head><body x-data=\"readerShell\" x-init=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`initReader({mediaItemId:'%s',fileUrl:'%s',formatGroup:'%s',readingDirection:'%s',mangaType:'%s'})`, metadata.MediaItemID, metadata.FileURL, metadata.FormatGroup, metadata.ReadingDirection, metadata.MangaType))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 34, Col: 233}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"theme-tokyo-night\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -52,7 +65,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<!-- Dockable Panels Container --><div id=\"reader-panels\" class=\"fixed inset-0 pointer-events-none z-30\"><!-- Left Sidebar (TOC, Settings) --><div id=\"left-sidebar\" class=\"absolute left-0 top-0 bottom-0 pointer-events-auto flex flex-col\"><div id=\"toc-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"toc\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Dockable Panels Container --><div id=\"reader-panels\" class=\"fixed inset-0 pointer-events-none z-30\"><!-- Left Sidebar (TOC, Settings) --><div id=\"left-sidebar\" class=\"absolute left-0 top-0 bottom-0 pointer-events-auto flex flex-col\"><div id=\"toc-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"toc\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -60,7 +73,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div id=\"settings-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"settings\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div id=\"settings-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"settings\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -68,7 +81,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div><!-- Right Sidebar (Navigator, Bookmarks) --><div id=\"right-sidebar\" class=\"absolute right-0 top-0 bottom-0 pointer-events-auto flex flex-col\"><div id=\"navigator-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"navigator\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div><!-- Right Sidebar (Navigator, Bookmarks) --><div id=\"right-sidebar\" class=\"absolute right-0 top-0 bottom-0 pointer-events-auto flex flex-col\"><div id=\"navigator-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"navigator\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -76,7 +89,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div id=\"bookmarks-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"bookmarks\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div id=\"bookmarks-panel\" class=\"panel-container pointer-events-auto\" data-panel=\"bookmarks\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -84,7 +97,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div></div><foliate-view id=\"reader-view\"></foliate-view>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div><foliate-view id=\"reader-view\"></foliate-view>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -92,7 +105,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -116,38 +129,38 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 68, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</h1><button data-action=\"open-settings\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Settings\">⚙️</button></div></div><!-- Bottom bar --><div class=\"fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center justify-between px-4 py-3\"><div id=\"progress-display\" data-progress-mode=\"pages\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 82, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 73, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div><div class=\"flex items-center gap-4\"><button data-action=\"toggle-toc\" title=\"Table of Contents\">📖</button> <button data-action=\"add-bookmark\" title=\"Bookmark\">🏷️</button> <button data-action=\"add-note\" title=\"Note\">📝</button><!-- Panel editor - shown for comics/manga --><button x-data=\"panelEditor\" x-show=\"isComicOrManga\" @click=\"openPanelEditor(readerShell.currentPage)\" data-action=\"edit-panels\" title=\"Edit Panels\" class=\"p-2 rounded-lg hover:bg-gray-700\">🎨</button></div></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h1><button data-action=\"open-settings\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Settings\">⚙️</button></div></div><!-- Bottom bar --><div class=\"fixed bottom-0 left-0 right-0 bg-opacity-95 backdrop-blur border-t z-40\" style=\"background-color: var(--bg-primary);\"><div class=\"flex items-center px-2 py-2 gap-1\"><!-- Left navigation --><button @click=\"goLeft()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Go Left\" aria-label=\"Go left\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 15 6 L 9 12 L 15 18\"></path></svg></button><!-- Progress slider --><input id=\"progress-slider\" type=\"range\" min=\"0\" max=\"1\" step=\"any\" list=\"tick-marks\" @input=\"goToFraction($event.target.value)\" class=\"flex-grow\"> <datalist id=\"tick-marks\"></datalist><!-- Right navigation --><button @click=\"goRight()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Go Right\" aria-label=\"Go right\"><svg class=\"reader-icon\" width=\"24\" height=\"24\" aria-hidden=\"true\"><path d=\"M 9 6 L 15 12 L 9 18\"></path></svg></button><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Zoom controls (fixed-layout only) --><template x-if=\"isFixedLayout\"><div class=\"flex items-center gap-1\"><!-- Zoom out --><button @click=\"zoomOut()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Zoom Out\" aria-label=\"Zoom out\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 10 L 15 10\"></path></svg></button><!-- Zoom percentage --><button @click=\"resetZoom()\" class=\"text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]\" title=\"Reset Zoom\" aria-label=\"Reset zoom\"><span x-text=\"zoomPercent + '%'\">100%</span></button><!-- Zoom in --><button @click=\"zoomIn()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Zoom In\" aria-label=\"Zoom in\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 10 5 L 10 15 M 5 10 L 15 10\"></path></svg></button><!-- Magnifier --><button @click=\"toggleMagnifier()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Magnifier\" aria-label=\"Toggle magnifier\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><circle cx=\"9\" cy=\"9\" r=\"5\"></circle> <path d=\"M 13 13 L 18 18\"></path></svg></button><!-- Pan/Select mode (PDF only) --><button x-show=\"isPDF\" @click=\"toggleInteractionMode()\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Pan/Select Mode\" aria-label=\"Toggle pan/select mode\"><svg class=\"reader-icon\" width=\"20\" height=\"20\" aria-hidden=\"true\"><path d=\"M 5 3 v 12 M 5 15 l -2 2 M 5 15 l 2 2 M 5 3 l 3 3\"></path></svg></button></div></template><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Progress display --><div id=\"progress-display\" x-text=\"progressText\" data-progress-mode=\"pages\" class=\"text-sm min-w-[4rem] text-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 150, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><!-- Separator --><div class=\"w-px h-6 bg-gray-600 mx-1\"></div><!-- Action buttons --><div class=\"flex items-center gap-1\"><button data-action=\"toggle-toc\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Table of Contents\">📖</button> <button data-action=\"add-bookmark\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Bookmark\">🏷️</button> <button data-action=\"add-note\" class=\"p-2 rounded-lg hover:bg-gray-700\" title=\"Note\">📝</button></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -171,12 +184,12 @@ func ReaderSettingsPanel() templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div id=\"settings-panel\" class=\"dockable-panel panel-collapsed\" data-panel=\"settings\" data-side=\"left\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Chrome Behavior <select name=\"chrome_behavior\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option> <option value=\"hide-on-scroll\">Hide on Scroll</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Typography (ebooks only) --><div class=\"mb-6\" data-visible-for=\"ebook\"><h3 class=\"font-semibold mb-2\">Reading Theme</h3><label class=\"block mb-2\"><select name=\"reading_theme\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">18 themes organized by category</p></label></div><div class=\"mb-6\" data-visible-for=\"ebook\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default - Designed for ebooks)</option> <option value=\"crimson\">Crimson Text (Screen-optimized)</option> <option value=\"source-serif\">Source Serif 4 (Adobe quality)</option> <option value=\"eb-garamond\">EB Garamond (Classic)</option> <option value=\"libertinus\">Libertinus Serif (Technical)</option> <option value=\"noto-serif\">Noto Serif (All languages)</option> <option value=\"charis-sil\">Charis SIL (Multilingual)</option> <option value=\"ibm-plex\">IBM Plex Serif (Modern)</option></select><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">8 libre fonts bundled with Bookhoard</p></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" min=\"12\" max=\"24\" value=\"16\" class=\"w-full\"> <span class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">12-24px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" min=\"1.0\" max=\"2.5\" step=\"0.1\" value=\"1.6\" class=\"w-full\"> <span class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">1.0-2.5</span></label></div><!-- Navigation --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"panel_zoom_enabled\" class=\"mr-2\"> Panel Zoom (Comics/Manga)</label> <label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread (Comics/Manga)</label></div><button data-action=\"close-settings\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div id=\"settings-panel\" class=\"dockable-panel panel-collapsed\" data-panel=\"settings\" data-side=\"left\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">⚙️ Settings</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><!-- Display settings --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Display</h3><label class=\"block mb-2\">Chrome Behavior <select name=\"chrome_behavior\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"auto-hide\">Auto Hide</option> <option value=\"always-visible\">Always Visible</option> <option value=\"hide-on-scroll\">Hide on Scroll</option></select></label> <label class=\"block mb-2\">Progress Mode <select name=\"progress_mode\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"pages\">Pages</option> <option value=\"chapter\">Chapter</option> <option value=\"percentage\">Percentage</option> <option value=\"time-left\">Time Left</option></select></label></div><!-- Typography (ebooks only) --><div class=\"mb-6\" data-visible-for=\"ebook\"><h3 class=\"font-semibold mb-2\">Reading Theme</h3><label class=\"block mb-2\"><select name=\"reading_theme\" class=\"w-full mt-1 px-3 py-2 rounded border\"><optgroup label=\"📖 Classic Reading\"><option value=\"light\">Light</option> <option value=\"paper\">Paper</option> <option value=\"sepia\">Sepia</option> <option value=\"parchment\">Parchment</option> <option value=\"warm\">Warm</option> <option value=\"candlelight\">Candlelight</option></optgroup> <optgroup label=\"🌤️ Sky & Atmosphere\"><option value=\"azure\">Azure</option> <option value=\"sky\">Sky</option> <option value=\"arctic\">Arctic</option> <option value=\"frost\">Frost</option></optgroup> <optgroup label=\"🌅 Sunset & Warmth\"><option value=\"dusk\">Dusk</option> <option value=\"sunset\">Sunset</option> <option value=\"twilight\">Twilight</option></optgroup> <optgroup label=\"🌲 Nature & Earth\"><option value=\"forest\">Forest</option> <option value=\"moss\">Moss</option> <option value=\"slate\">Slate</option></optgroup> <optgroup label=\"⚡ High Performance\"><option value=\"oled\">OLED</option> <option value=\"solarized\">Solarized</option></optgroup></select><p class=\"text-xs mt-1\" style=\"color: var(--text-secondary)\">18 themes organized by category</p></label></div><div class=\"mb-6\" data-visible-for=\"ebook\"><h3 class=\"font-semibold mb-2\">Typography</h3><label class=\"block mb-2\">Reading Font <select name=\"reading_font\" class=\"w-full mt-1 px-3 py-2 rounded border\"><option value=\"literata\">Literata (Default - Designed for ebooks)</option> <option value=\"crimson\">Crimson Text (Screen-optimized)</option> <option value=\"source-serif\">Source Serif 4 (Adobe quality)</option> <option value=\"eb-garamond\">EB Garamond (Classic)</option> <option value=\"libertinus\">Libertinus Serif (Technical)</option> <option value=\"noto-serif\">Noto Serif (All languages)</option> <option value=\"charis-sil\">Charis SIL (Multilingual)</option> <option value=\"ibm-plex\">IBM Plex Serif (Modern)</option></select><p class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">8 libre fonts bundled with Bookhoard</p></label> <label class=\"block mb-2\">Font Size <input type=\"range\" name=\"font_size\" min=\"12\" max=\"24\" value=\"16\" class=\"w-full\"> <span class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">12-24px</span></label> <label class=\"block mb-2\">Line Height <input type=\"range\" name=\"line_height\" min=\"1.0\" max=\"2.5\" step=\"0.1\" value=\"1.6\" class=\"w-full\"> <span class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">1.0-2.5</span></label></div><!-- Navigation --><div class=\"mb-6\"><h3 class=\"font-semibold mb-2\">Navigation</h3><label class=\"flex items-center mb-2\"><input type=\"checkbox\" name=\"double_page_spread\" class=\"mr-2\"> Double Page Spread (Comics/Manga)</label></div><button data-action=\"close-settings\" class=\"w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700\">Done</button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -200,12 +213,12 @@ func ReaderTOCPanel(metadata ReaderMetadata) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div id=\"toc-panel\" class=\"dockable-panel\" data-panel=\"toc\" data-side=\"left\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">📖 Table of Contents</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><nav id=\"toc-list\" class=\"space-y-2\"><!-- TOC items populated by JavaScript --></nav></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div id=\"toc-panel\" class=\"dockable-panel\" data-panel=\"toc\" data-side=\"left\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">📖 Table of Contents</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\"><nav id=\"toc-list\" class=\"space-y-2\"><!-- TOC items populated by JavaScript --></nav></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -229,12 +242,12 @@ func ReaderNavigatorPanel() templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div id=\"navigator-panel\" class=\"dockable-panel\" data-panel=\"navigator\" data-side=\"right\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">🗺️ Navigator</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-2 overflow-hidden\"><div id=\"navigator-viewport\" class=\"relative w-full h-full\"><!-- Full page preview with draggable viewport box --><!-- JavaScript renders current page as scaled thumbnail with draggable viewport --></div></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div id=\"navigator-panel\" class=\"dockable-panel\" data-panel=\"navigator\" data-side=\"right\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">🗺️ Navigator</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-2 overflow-hidden\"><div id=\"navigator-viewport\" class=\"relative w-full h-full\"><!-- Full page preview with draggable viewport box --><!-- JavaScript renders current page as scaled thumbnail with draggable viewport --></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -258,76 +271,76 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div id=\"bookmarks-panel\" class=\"dockable-panel panel-collapsed\" data-panel=\"bookmarks\" data-side=\"right\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">🔖 Bookmarks</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div id=\"bookmarks-panel\" class=\"dockable-panel panel-collapsed\" data-panel=\"bookmarks\" data-side=\"right\"><div class=\"panel-header flex items-center justify-between p-3 cursor-pointer\" data-action=\"toggle-panel\"><h3 class=\"panel-title font-semibold\">🔖 Bookmarks</h3><div class=\"panel-controls flex items-center gap-2\"><button class=\"panel-lock\" data-action=\"lock-panel\" title=\"Lock position\">🔓</button> <button class=\"window-shade-toggle\" data-action=\"window-shade\">─</button></div></div><div class=\"panel-content p-4 overflow-y-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(bookmarks) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div id=\"bookmarks-list\" class=\"space-y-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div id=\"bookmarks-list\" class=\"space-y-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, bookmark := range bookmarks {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a href=\"#\" data-bookmark-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 289, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" class=\"block py-2 hover:bg-gray-700 rounded px-2\"><span class=\"font-medium\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a href=\"#\" data-bookmark-id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 292, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 345, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</span> <span class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"block py-2 hover:bg-gray-700 rounded px-2\"><span class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 294, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 348, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span> <span class=\"text-xs ml-2\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 350, Col: 27}
|
||||
}
|
||||
_, 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, 21, "</span></a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">No bookmarks yet</p>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">No bookmarks yet</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button data-action=\"add-bookmark\" class=\"w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Bookmark</button></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<button data-action=\"add-bookmark\" class=\"w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700\">+ Add Bookmark</button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -351,12 +364,12 @@ func DictionaryPopup() templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var13 == nil {
|
||||
templ_7745c5c3_Var13 = templ.NopComponent
|
||||
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var14 == nil {
|
||||
templ_7745c5c3_Var14 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<div id=\"dictionary-popup\" class=\"hidden fixed bg-white text-black p-4 rounded-lg shadow-xl max-w-md z-50\"></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div id=\"dictionary-popup\" class=\"hidden fixed bg-white text-black p-4 rounded-lg shadow-xl max-w-md z-50\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+14
-9
@@ -147,15 +147,20 @@ func (u UnsafeHTML) ToComponent() templ.Component {
|
||||
|
||||
// Reader types
|
||||
type ReaderMetadata struct {
|
||||
MediaItemID string `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
LibraryType string `json:"library_type"`
|
||||
MimeType string `json:"mime_type"`
|
||||
FilePath string `json:"file_path"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
ChapterCount int `json:"chapter_count"`
|
||||
MediaItemID string `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
LibraryType string `json:"library_type"`
|
||||
MimeType string `json:"mime_type"`
|
||||
FilePath string `json:"file_path"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
ChapterCount int `json:"chapter_count"`
|
||||
FormatGroup string `json:"format_group"`
|
||||
MangaType string `json:"manga_type"`
|
||||
ReadingDirection string `json:"reading_direction"`
|
||||
FileURL string `json:"file_url"`
|
||||
LibraryID string `json:"library_id"`
|
||||
}
|
||||
|
||||
type ReadingProgress struct {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { readerEvents, ReaderEventType } from "./reader-events";
|
||||
import { UniversalReader } from "../reader-shell";
|
||||
import { PDFDocumentProxy } from "pdfjs-dist";
|
||||
|
||||
interface PDFReader {
|
||||
type: "pdf";
|
||||
doc: PDFDocumentProxy;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
interface ComicReader {
|
||||
type: "comic";
|
||||
images: Blob[];
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
interface MangaReader {
|
||||
type: "manga";
|
||||
images: Blob[];
|
||||
currentPage: number;
|
||||
readingDirection: "rtl" | "vertical";
|
||||
}
|
||||
|
||||
export type CurrentReader =
|
||||
| UniversalReader
|
||||
| PDFReader
|
||||
| ComicReader
|
||||
| MangaReader;
|
||||
|
||||
export interface ReaderContext {
|
||||
getState: () => {
|
||||
currentReader: CurrentReader | null;
|
||||
readerMetadata: ReaderMetadata | null;
|
||||
};
|
||||
setState: (updates: Partial<ReaderState>) => void;
|
||||
navigation: {
|
||||
nextPage: () => void;
|
||||
previousPage: () => void;
|
||||
goToPage: (page: number) => void;
|
||||
goToChapter: (chapterIndex: number) => void;
|
||||
};
|
||||
render: () => void;
|
||||
elements: {
|
||||
readerContent: HTMLElement;
|
||||
chrome: HTMLElement | null;
|
||||
progressDisplay: HTMLElement | null;
|
||||
};
|
||||
events: {
|
||||
on: (event: string, handler: Function) => void;
|
||||
emit: (event: string, data?: any) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReaderState {
|
||||
currentReader: CurrentReader | null;
|
||||
readerMetadata: ReaderMetadata | null;
|
||||
}
|
||||
|
||||
export function createReaderContext(
|
||||
getState: () => ReaderState,
|
||||
setState: (updates: Partial<ReaderState>) => void,
|
||||
navigation: ReaderContext["navigation"],
|
||||
render: () => void,
|
||||
): ReaderContext {
|
||||
return {
|
||||
getState,
|
||||
setState,
|
||||
navigation,
|
||||
render,
|
||||
elements: {
|
||||
readerContent: document.getElementById("reader-content")!,
|
||||
chrome: document.getElementById("reader-chrome"),
|
||||
progressDisplay: document.getElementById("progress-display"),
|
||||
},
|
||||
events: {
|
||||
on: (event: ReaderEventType, handler: Function) =>
|
||||
readerEvents.on(event, handler),
|
||||
emit: (event: ReaderEventType, data?: any) =>
|
||||
readerEvents.emit(event, data),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
type ReaderEventType =
|
||||
| "readerReady"
|
||||
| "pageChanged"
|
||||
| "chapterChanged"
|
||||
| "zoomChanged"
|
||||
| "themeChanged"
|
||||
| "progressUpdated"
|
||||
| "beforePageChange"
|
||||
| "afterPageChange"
|
||||
| "settings:changed";
|
||||
|
||||
class EventBus {
|
||||
private listeners = new Map<ReaderEventType, Function[]>();
|
||||
|
||||
on(event: ReaderEventType, handler: Function): void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event)!.push(handler);
|
||||
}
|
||||
|
||||
off(event: ReaderEventType, handler: Function): void {
|
||||
const handlers = this.listeners.get(event);
|
||||
if (handlers) {
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit<T = unknown>(event: ReaderEventType, data?: T): void {
|
||||
this.listeners.get(event)?.forEach((handler) => handler(data));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const readerEvents = new EventBus();
|
||||
export type { ReaderEventType };
|
||||
+177
-42
@@ -1,57 +1,192 @@
|
||||
import "foliate-js/view.js";
|
||||
import { Alpine } from "../alpine";
|
||||
|
||||
import { loadSettings, saveSettings } from "./settings-manager";
|
||||
const getCSS = ({
|
||||
spacing,
|
||||
justify,
|
||||
hyphenate,
|
||||
}: {
|
||||
spacing: number;
|
||||
justify: boolean;
|
||||
hyphenate: boolean;
|
||||
}) => `
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a:link {
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
p, li, blockquote, dd {
|
||||
line-height: ${spacing};
|
||||
text-align: ${justify ? "justify" : "start"};
|
||||
-webkit-hyphens: ${hyphenate ? "auto" : "manual"};
|
||||
hyphens: ${hyphenate ? "auto" : "manual"};
|
||||
-webkit-hyphenate-limit-before: 3;
|
||||
-webkit-hyphenate-limit-after: 2;
|
||||
-webkit-hyphenate-limit-lines: 2;
|
||||
hanging-punctuation: allow-end last;
|
||||
widows: 2;
|
||||
}
|
||||
[align="left"] { text-align: left; }
|
||||
[align="right"] { text-align: right; }
|
||||
[align="center"] { text-align: center; }
|
||||
[align="justify"] { text-align: justify; }
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
aside[epub|type~="endnote"],
|
||||
aside[epub|type~="footnote"],
|
||||
aside[epub|type~="note"],
|
||||
aside[epub|type~="rearnote"] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("readerShell", () => ({
|
||||
enablePanelDetection: false,
|
||||
libraryType: "",
|
||||
formatGroup: "",
|
||||
mangaType: "",
|
||||
readingDirection: "",
|
||||
panelDetector: null,
|
||||
|
||||
initReader(config: any) {
|
||||
this.enablePanelDetection = config.enablePanelDetection;
|
||||
this.libraryType = config.libraryType;
|
||||
this.formatGroup = config.formatGroup;
|
||||
this.mangaType = config.mangaType;
|
||||
this.readingDirection = config.readingDirection;
|
||||
|
||||
console.log("Reader initialized with:", {
|
||||
panelDetection: this.enablePanelDetection,
|
||||
library: this.libraryType,
|
||||
format: this.formatGroup,
|
||||
view: null as any,
|
||||
renderer: null as any,
|
||||
book: null as any,
|
||||
zoomPercent: 100,
|
||||
isFixedLayout: false,
|
||||
isPDF: false,
|
||||
interactionMode: "select" as string,
|
||||
magnifierEnabled: false,
|
||||
progressText: "",
|
||||
sliderValue: 0,
|
||||
settings: null as ReaderSettings | null,
|
||||
style: {
|
||||
spacing: 1.4,
|
||||
justify: true,
|
||||
hyphenate: true,
|
||||
},
|
||||
async initReader(config: {
|
||||
mediaItemId: string;
|
||||
fileUrl: string;
|
||||
formatGroup: string;
|
||||
readingDirection: string;
|
||||
mangaType: string;
|
||||
}) {
|
||||
this.settings = await loadSettings();
|
||||
this.view = document.getElementById("reader-view") as any;
|
||||
await this.view.open(config.fileUrl);
|
||||
this.renderer = this.view.renderer;
|
||||
this.book = this.view.book;
|
||||
this.isFixedLayout = this.view.isFixedLayout;
|
||||
if (this.isFixedLayout) {
|
||||
this.isPDF = (this.renderer as any).isPDF;
|
||||
this.renderer.addEventListener("zoom", () => {
|
||||
this.zoomPercent = this.renderer.zoomPercent;
|
||||
});
|
||||
} else {
|
||||
this.renderer.setStyles?.(getCSS(this.style));
|
||||
}
|
||||
this.view.addEventListener("load", (e: any) => {
|
||||
const { doc } = e.detail;
|
||||
doc.addEventListener("keydown", (ev: KeyboardEvent) =>
|
||||
this.handleKeydown(ev),
|
||||
);
|
||||
});
|
||||
|
||||
// Only load panel detection if enabled
|
||||
if (this.enablePanelDetection) {
|
||||
this.loadPanelDetection();
|
||||
this.view.addEventListener("relocate", (e: any) => {
|
||||
const { fraction, location, tocItem, pageItem } = e.detail;
|
||||
const percent = new Intl.NumberFormat("en", {
|
||||
style: "percent",
|
||||
}).format(fraction);
|
||||
const loc = pageItem
|
||||
? `Page ${pageItem.label}`
|
||||
: `Loc ${location.current}`;
|
||||
this.progressText = `${percent} · ${loc}`;
|
||||
this.sliderValue = fraction;
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.value = fraction;
|
||||
slider.title = `${percent} · ${loc}`;
|
||||
}
|
||||
});
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider && this.book.dir) {
|
||||
slider.dir = this.book.dir;
|
||||
}
|
||||
},
|
||||
|
||||
async loadPanelDetection() {
|
||||
try {
|
||||
// Dynamic import to only load when needed
|
||||
const { PanelDetector } = await import("foliate-js/panel-detection.js");
|
||||
this.panelDetector = new PanelDetector();
|
||||
console.log("Panel detection loaded successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to load panel detection:", error);
|
||||
if (this.view.getSectionFractions) {
|
||||
const tickMarks = document.getElementById("tick-marks");
|
||||
if (tickMarks) {
|
||||
for (const fraction of this.view.getSectionFractions()) {
|
||||
const option = document.createElement("option");
|
||||
option.value = fraction;
|
||||
tickMarks.append(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", (ev: KeyboardEvent) =>
|
||||
this.handleKeydown(ev),
|
||||
);
|
||||
this.renderer.next();
|
||||
},
|
||||
zoomIn() {
|
||||
if (!this.isFixedLayout) return;
|
||||
const newScale = Math.min(10, this.renderer.currentScale * 1.2);
|
||||
this.renderer.setAttribute("zoom", newScale);
|
||||
this.zoomPercent = this.renderer.zoomPercent;
|
||||
},
|
||||
zoomOut() {
|
||||
if (!this.isFixedLayout) return;
|
||||
const newScale = Math.max(0.1, this.renderer.currentScale / 1.2);
|
||||
this.renderer.setAttribute("zoom", newScale);
|
||||
this.zoomPercent = this.renderer.zoomPercent;
|
||||
},
|
||||
resetZoom() {
|
||||
if (!this.isFixedLayout) return;
|
||||
this.renderer.resetZoom();
|
||||
this.renderer.dragOffset = { x: 0, y: 0 };
|
||||
this.zoomPercent = 100;
|
||||
},
|
||||
toggleMagnifier() {
|
||||
if (!this.isFixedLayout) return;
|
||||
this.renderer.toggleMagnifier();
|
||||
this.magnifierEnabled = this.renderer.zoomMagnifierEnabled;
|
||||
},
|
||||
toggleInteractionMode() {
|
||||
if (!this.isFixedLayout) return;
|
||||
const current =
|
||||
this.renderer.getAttribute("interaction-mode") || "select";
|
||||
const next = current === "select" ? "pan" : "select";
|
||||
this.renderer.setAttribute("interaction-mode", next);
|
||||
this.interactionMode = next;
|
||||
},
|
||||
goLeft() {
|
||||
this.view?.goLeft?.();
|
||||
},
|
||||
goRight() {
|
||||
this.view?.goRight?.();
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
const view = document.querySelector("#reader-view");
|
||||
// @ts-ignore - foliate custom element
|
||||
view?.next?.();
|
||||
this.view?.next?.();
|
||||
},
|
||||
|
||||
previousPage() {
|
||||
const view = document.querySelector("#reader-view");
|
||||
// @ts-ignore - foliate custom element
|
||||
view?.prev?.();
|
||||
this.view?.prev?.();
|
||||
},
|
||||
goToFraction(value: string) {
|
||||
this.view?.goToFraction?.(parseFloat(value));
|
||||
},
|
||||
handleKeydown(event: KeyboardEvent) {
|
||||
const k = event.key;
|
||||
if (k === "ArrowLeft" || k === "h") this.goLeft();
|
||||
else if (k === "ArrowRight" || k === "l") this.goRight();
|
||||
else if (k === "+" || k === "=") this.zoomIn();
|
||||
else if (k === "-" || k === "_") this.zoomOut();
|
||||
else if (k === "0") this.resetZoom();
|
||||
else if (k === "Escape") {
|
||||
if (this.isFixedLayout && this.renderer?.zoomMagnifierEnabled) {
|
||||
this.toggleMagnifier();
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
});
|
||||
|
||||
@@ -1,64 +1,12 @@
|
||||
// Per-user settings with localStorage fallback
|
||||
// Feature Registration Pattern implementation
|
||||
|
||||
import type { ReaderContext } from "./core/reader-context";
|
||||
import { apiGet, apiPut } from "../api";
|
||||
import { getToken } from "../storage";
|
||||
|
||||
const LOCALSTORAGE_KEY = "reader_settings_local";
|
||||
|
||||
export function init(context: ReaderContext): void {
|
||||
let currentSettings: ReaderSettings | null = null;
|
||||
|
||||
context.events.on("reader:init", async () => {
|
||||
currentSettings = await loadSettings();
|
||||
context.events.emit("settings:loaded", currentSettings);
|
||||
});
|
||||
|
||||
context.events.on(
|
||||
"settings:save",
|
||||
async (detail: { settings: Partial<ReaderSettings> }) => {
|
||||
await saveSettings(detail.settings);
|
||||
currentSettings = await loadSettings();
|
||||
context.events.emit("settings:changed", currentSettings);
|
||||
},
|
||||
);
|
||||
|
||||
context.events.on(
|
||||
"settings:get",
|
||||
(detail: { key?: keyof ReaderSettings }) => {
|
||||
if (currentSettings) {
|
||||
const value = detail.key
|
||||
? currentSettings[detail.key]
|
||||
: currentSettings;
|
||||
context.events.emit("settings:current", { value });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
context.events.on(
|
||||
"settings:set",
|
||||
async (detail: { key: keyof ReaderSettings; value: any }) => {
|
||||
await saveSettings({ [detail.key]: detail.value });
|
||||
currentSettings = await loadSettings();
|
||||
context.events.emit("settings:changed", currentSettings);
|
||||
},
|
||||
);
|
||||
|
||||
context.events.on("settings:sync", async () => {
|
||||
await syncSettings();
|
||||
currentSettings = await loadSettings();
|
||||
context.events.emit("settings:synced", currentSettings);
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadSettings(): Promise<ReaderSettings> {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
const local = localStorage.getItem(LOCALSTORAGE_KEY);
|
||||
return local ? JSON.parse(local) : getDefaultSettings();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiGet("/readers/settings");
|
||||
const settings = await response.json();
|
||||
@@ -69,17 +17,16 @@ export async function loadSettings(): Promise<ReaderSettings> {
|
||||
return local ? JSON.parse(local) : getDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(settings: Partial<ReaderSettings>): Promise<void> {
|
||||
export async function saveSettings(
|
||||
settings: Partial<ReaderSettings>,
|
||||
): Promise<void> {
|
||||
const token = getToken();
|
||||
const current = await loadSettings();
|
||||
const updated = { ...current, ...settings };
|
||||
|
||||
if (!token) {
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiPut("/readers/settings", settings);
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
||||
@@ -87,14 +34,11 @@ async function saveSettings(settings: Partial<ReaderSettings>): Promise<void> {
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSettings(): Promise<void> {
|
||||
export async function syncSettings(): Promise<void> {
|
||||
const local = localStorage.getItem(LOCALSTORAGE_KEY);
|
||||
if (!local) return;
|
||||
|
||||
const settings = JSON.parse(local);
|
||||
const token = getToken();
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await apiPut("/readers/settings", settings);
|
||||
@@ -103,7 +47,6 @@ async function syncSettings(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultSettings(): ReaderSettings {
|
||||
return {
|
||||
chrome_behavior: "auto-hide",
|
||||
@@ -113,7 +56,6 @@ export function getDefaultSettings(): ReaderSettings {
|
||||
reading_font: "literata",
|
||||
tap_zone_size: 30,
|
||||
auto_scroll: false,
|
||||
panel_zoom_enabled: true,
|
||||
font_size: 16,
|
||||
line_height: 1.6,
|
||||
margin_width: 20,
|
||||
|
||||
Vendored
-1
@@ -206,7 +206,6 @@ interface ReaderSettings {
|
||||
|
||||
tap_zone_size: number;
|
||||
auto_scroll: boolean;
|
||||
panel_zoom_enabled: boolean;
|
||||
|
||||
double_page_spread: boolean;
|
||||
reading_direction: "ltr" | "rtl" | "vertical";
|
||||
|
||||
Reference in New Issue
Block a user