3 Commits
Author SHA1 Message Date
john-okeefe c5c2270007 fix(reader): keep footer compact on mobile and clear text under chrome
Release / build-and-push (push) Successful in 2m39s
On phones the footer's progress cell rendered the full chapter title (e.g. 'Long Chapter Name · 5 / 12') in a div with no max-width or nowrap, so the text wrapped to multiple lines and ballooned the bottom bar. Combined with viewport offsets that were computed from assumed pixel heights with ~0px margin, the book text slipped underneath the bars.

Chapter label in footer: split #progress-display into two spans (progressLabel hidden on mobile via 'hidden sm:inline', progressMain always shown) and cap it with 'truncate whitespace-nowrap max-w-[5rem] sm:max-w-none' so it can never wrap or grow the bar. Phones now show just '5 / 12'; larger screens keep 'Chapter · 5 / 12'.

Viewport offset: replace the fragile hardcoded calc() with runtime measurement. Gave the chrome bars ids (reader-topbar/reader-bottombar) and added updateViewportInsets(), which sets #reader-viewport top/bottom from each bar's real offsetHeight (which already includes env(safe-area-inset-*) padding) plus a 6px margin. It runs on init and refreshes on resize, orientationchange, and via a ResizeObserver, so the content area tracks the actual chrome height on any DPI, notch, home-indicator, or zoom level instead of guessing.

Refactored formatProgress into formatProgressParts (returns {label, main}; only chapter mode sets a label) with a setProgress() helper wiring progressLabel/progressMain/progressText across the relocate, cycleProgressMode, and applyProgressMode call sites. Rebuilt reader_templ.go and style.css.
2026-08-05 21:48:28 -04:00
john-okeefe 94a4facc6c chore(deps): promote golang.org/x/net to direct dependency
Release / build-and-push (push) Successful in 3m2s
Pulled in as a direct requirement by templ v0.3.1020 code generation; it was previously only an indirect dependency.
2026-08-05 15:31:48 -04:00
john-okeefe 11617c1860 fix(reader): tighten mobile header/footer and add safe-area margins
The reader chrome used the same dimensions at every screen size, and the book viewport offset was hardcoded to 52px. This made the header/footer oversized on phones and left the body text flush against (or overlapping) the bars, with no handling for notched-device safe areas.

- Add viewport-fit=cover so notched devices expose safe-area insets.
- Shrink the top bar on mobile (px-3 py-2 / text-base, scaling up at sm:) and hide the chapter title on phones (hidden sm:block sm:truncate).
- Shrink the bottom bar on mobile (tighter padding/gap, p-1.5 sm:p-2 on buttons) while keeping all controls visible.
- Replace the hardcoded top-[52px] bottom-[52px] viewport offsets with responsive calc() values (44/48px mobile, 60px at sm:) that fold in env(safe-area-inset-*), plus matching safe-area padding on the bars, so the book content always clears the chrome with a visible margin.

Regenerates reader_templ.go and rebuilds style.css.
2026-08-05 15:31:40 -04:00
5 changed files with 136 additions and 92 deletions
+1 -1
View File
@@ -29,6 +29,7 @@ require (
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
golang.org/x/crypto v0.50.0
golang.org/x/net v0.53.0
golang.org/x/text v0.36.0
)
@@ -57,7 +58,6 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/xyproto/randomstring v1.2.0 // indirect
golang.org/x/image v0.39.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
+28 -26
View File
@@ -36,7 +36,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>{ metadata.Title } - Bookhoard Reader</title>
<link rel="manifest" href="/static/manifest.json"/>
<link href="/static/reader-fonts.css" rel="stylesheet"/>
@@ -71,7 +71,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
</div>
</div>
</div>
<div id="reader-viewport" class="absolute inset-x-0 top-[52px] bottom-[52px]">
<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>
@DictionaryPopup()
@@ -82,25 +82,25 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
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/" + metadata.MediaItemID } class="text-lg hover:underline">
<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 href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
Back
</a>
<h1 class="text-lg font-semibold">{ metadata.Title }</h1>
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
<button
@click="toggleSettings()"
class="p-2 rounded-lg hover:bg-gray-700"
class="p-1.5 sm: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">
<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-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">
<path d="M 15 6 L 9 12 L 15 18"></path>
</svg>
@@ -118,7 +118,7 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
/>
<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">
<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>
@@ -129,7 +129,7 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
<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">
<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>
@@ -139,20 +139,20 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
<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">
<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-2 rounded-lg hover:bg-gray-700" title="Magnifier" aria-label="Toggle 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-2 rounded-lg hover:bg-gray-700" title="Pan/Select Mode" aria-label="Toggle pan/select mode">
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-1.5 sm: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>
@@ -162,24 +162,26 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
<!-- Separator -->
<div class="w-px h-6 bg-gray-600 mx-1"></div>
<!-- Progress display -->
<div id="progress-display" x-text="progressText" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] text-center cursor-pointer">
if progress.FormatGroup == "reflowable" {
if metadata.EstimatedPages > 0 {
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
<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 progress.FormatGroup == "reflowable" {
if metadata.EstimatedPages > 0 {
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
} else {
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
}
} else {
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
}
} else {
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
}
</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-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>
<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">🏷️</button>
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Notes">📝</button>
</div>
</div>
</div>
+13 -13
View File
@@ -60,7 +60,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -126,7 +126,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, "</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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -163,7 +163,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
templ_7745c5c3_Var4 = templ.NopComponent
}
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 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=\"")
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 href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -176,20 +176,20 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"text-lg hover:underline\">← Back</a><h1 class=\"text-lg font-semibold\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"text-base sm:text-lg hover:underline\">← Back</a><h1 class=\"text-base sm:text-lg font-semibold hidden sm:block sm:truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 90, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 90, Col: 95}
}
_, 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, "</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\" @click=\"cycleProgressMode()\" :title=\"progressTooltip()\" class=\"text-sm min-w-[4rem] text-center cursor-pointer\">")
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\" 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\" @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 {
return templ_7745c5c3_Err
}
@@ -198,7 +198,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 168, Col: 112}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 169, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -208,7 +208,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, 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: 170, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 171, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -219,14 +219,14 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, 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: 173, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 174, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</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_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\">🏷️</button> <button @click=\"toggleBookmarks()\" class=\"p-1.5 sm:p-2 rounded-lg hover:bg-gray-700\" title=\"Notes\">📝</button></div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -359,7 +359,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(bookmark.CfiPosition)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 390, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 392, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -372,7 +372,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 393, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 395, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -385,7 +385,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 395, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 397, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
+93 -51
View File
@@ -372,6 +372,8 @@ document.addEventListener("alpine:init", () => {
interactionMode: "select" as string,
magnifierEnabled: false,
progressText: "",
progressLabel: "",
progressMain: "",
sliderValue: 0,
settings: null as ReaderSettings | null,
justify: true,
@@ -489,21 +491,20 @@ document.addEventListener("alpine:init", () => {
tocItem,
section,
};
const progressStr = this.formatProgress(
const progressParts = this.formatProgressParts(
fraction,
location,
pageItem,
tocItem,
section,
);
this.progressText = progressStr;
this.setProgress(progressParts);
this.sliderValue = fraction;
const slider = document.getElementById(
"progress-slider",
) as HTMLInputElement;
if (slider) {
slider.value = fraction;
slider.title = progressStr;
}
const range = e.detail.range as Range | undefined;
if (range) {
@@ -547,6 +548,34 @@ document.addEventListener("alpine:init", () => {
}
this.initTime = Date.now();
this.fetchReadingSpeed();
this.setupViewportInsets();
},
updateViewportInsets() {
const top = document.getElementById("reader-topbar");
const bot = document.getElementById("reader-bottombar");
const vp = document.getElementById("reader-viewport");
if (!top || !bot || !vp) return;
const margin = 6;
vp.style.setProperty("top", `${top.offsetHeight + margin}px`);
vp.style.setProperty("bottom", `${bot.offsetHeight + margin}px`);
},
setupViewportInsets() {
const top = document.getElementById("reader-topbar");
const bot = document.getElementById("reader-bottombar");
this.updateViewportInsets();
window.addEventListener("resize", () => this.updateViewportInsets());
window.addEventListener("orientationchange", () =>
setTimeout(() => this.updateViewportInsets(), 250),
);
if (
typeof ResizeObserver !== "undefined" &&
top &&
bot
) {
const ro = new ResizeObserver(() => this.updateViewportInsets());
ro.observe(top);
ro.observe(bot);
}
},
debouncedSaveProgress(fraction: number, location: any, cfi: string) {
if (Date.now() - this.initTime < 5000) return;
@@ -774,19 +803,20 @@ document.addEventListener("alpine:init", () => {
/* ignore bookmark errors for now */
}
},
formatProgress(
formatProgressParts(
fraction: number,
location: { current: number; next: number; total: number },
pageItem: { id: number; label: string; href: string },
tocItem: FoliateTocItem | null,
section: { current: number; total: number },
): string {
): { label: string; main: string } {
const percent = new Intl.NumberFormat("en", { style: "percent" }).format(
fraction,
);
const chapterLabel = tocItem?.label ? `${tocItem.label} · ` : "";
switch (this.progressMode) {
case "percentage":
return percent;
return { label: "", main: percent };
case "chapter": {
if (this.isFixedLayout && tocItem && this.chapterBoundaries.length > 0) {
const totalSections = this.book?.sections?.filter(
@@ -809,10 +839,10 @@ document.addEventListener("alpine:init", () => {
1,
Math.min(currentPage - chapterStart + 1, chapterPages),
);
const label = tocItem.label
? `${tocItem.label} · `
: "";
return `${label}${currentInChapter} / ${chapterPages}`;
return {
label: chapterLabel,
main: `${currentInChapter} / ${chapterPages}`,
};
}
}
if (tocItem && this.chapterBoundaries.length > 0) {
@@ -836,10 +866,10 @@ document.addEventListener("alpine:init", () => {
1,
Math.min(currentPage - chapterStart + 1, chapterPages),
);
const label = tocItem.label
? `${tocItem.label} · `
: "";
return `${label}${currentInChapter} / ${chapterPages}`;
return {
label: chapterLabel,
main: `${currentInChapter} / ${chapterPages}`,
};
}
}
if (section && this.sectionFractionsArr.length > 1) {
@@ -859,16 +889,19 @@ document.addEventListener("alpine:init", () => {
Math.round((fraction - startFrac) * pageBase),
);
const clamped = Math.min(currentInSec, totalInSec);
const label = tocItem?.label
? `${tocItem.label} · `
: "";
return `${label}${clamped} / ${totalInSec}`;
return {
label: chapterLabel,
main: `${clamped} / ${totalInSec}`,
};
}
}
if (location.total > 0) {
return `${percent} · ${location.current} / ${location.total}`;
return {
label: "",
main: `${percent} · ${location.current} / ${location.total}`,
};
}
return percent;
return { label: "", main: percent };
}
case "time-left": {
if (this.readingSpeedPpm > 0 && location.total > 0) {
@@ -877,30 +910,47 @@ document.addEventListener("alpine:init", () => {
if (mins >= 60) {
const hrs = Math.floor(mins / 60);
const m = mins % 60;
return `${percent} · ~${hrs}h ${m}m left`;
return { label: "", main: `${percent} · ~${hrs}h ${m}m left` };
}
return `${percent} · ~${mins} min left`;
return { label: "", main: `${percent} · ~${mins} min left` };
}
return `${percent} · ~-- min left`;
return { label: "", main: `${percent} · ~-- min left` };
}
default: {
if (this.isFixedLayout) {
const pageInfo = this.getRenderedPageInfo();
if (pageInfo) {
return `${pageInfo.current} / ${pageInfo.total}`;
return { label: "", main: `${pageInfo.current} / ${pageInfo.total}` };
}
}
if (pageItem) {
return `${percent} · Page ${pageItem.label}`;
return { label: "", main: `${percent} · Page ${pageItem.label}` };
}
const pageInfoReflow = this.getRenderedPageInfo();
if (pageInfoReflow) {
return `${percent} · ${pageInfoReflow.current + 1} / ${pageInfoReflow.total}`;
return {
label: "",
main: `${percent} · ${pageInfoReflow.current + 1} / ${pageInfoReflow.total}`,
};
}
return `${percent} · ${location.current} / ${location.total}`;
return {
label: "",
main: `${percent} · ${location.current} / ${location.total}`,
};
}
}
},
setProgress(parts: { label: string; main: string }, sliderTitle?: string) {
this.progressLabel = parts.label;
this.progressMain = parts.main;
this.progressText = parts.label + parts.main;
const slider = document.getElementById(
"progress-slider",
) as HTMLInputElement;
if (slider) {
slider.title = sliderTitle ?? this.progressText;
}
},
cycleProgressMode() {
const modes = ["pages", "chapter", "percentage", "time-left"];
const idx = modes.indexOf(this.progressMode);
@@ -909,19 +959,15 @@ document.addEventListener("alpine:init", () => {
if (this.lastRelocateDetail) {
const { fraction, location, pageItem, tocItem, section } =
this.lastRelocateDetail;
this.progressText = this.formatProgress(
fraction,
location,
pageItem,
tocItem,
section,
this.setProgress(
this.formatProgressParts(
fraction,
location,
pageItem,
tocItem,
section,
),
);
const slider = document.getElementById(
"progress-slider",
) as HTMLInputElement;
if (slider) {
slider.title = this.progressText;
}
}
},
async fetchReadingSpeed() {
@@ -944,19 +990,15 @@ document.addEventListener("alpine:init", () => {
if (this.lastRelocateDetail) {
const { fraction, location, pageItem, tocItem, section } =
this.lastRelocateDetail;
this.progressText = this.formatProgress(
fraction,
location,
pageItem,
tocItem,
section,
this.setProgress(
this.formatProgressParts(
fraction,
location,
pageItem,
tocItem,
section,
),
);
const slider = document.getElementById(
"progress-slider",
) as HTMLInputElement;
if (slider) {
slider.title = this.progressText;
}
}
},
computeFixedLayoutChapterBoundaries() {
+1 -1
View File
File diff suppressed because one or more lines are too long