Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5c2270007 | ||
|
|
94a4facc6c | ||
|
|
11617c1860 |
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user