feat(reader): relocate back-to-location, add recenter control, wrap bottom bar on small windows
- Back-to-location moves from the topbar (where it sat between Back and the title, too subtle and disconnected from navigation) into both bottom-bar rows, beside the page-back arrow — the natural 'go back' cluster. New icon: a location pin, clearly distinct from the back arrow and page controls. Appears only when the stack has a return target; Alt+← unchanged. - New recenter button in the fixed-layout row (crosshair icon, next to zoom): resets pan offsets while keeping the current zoom — backed by foliate's new recenter() (1c812e8), which zeroes the wrapper translate and re-syncs the spread side. - Both bottom-bar rows wrap gracefully on narrow windows instead of overflowing/h-scrolling: controls are grouped (paging+back | slider | fit+zoom+magnifier+recenter | pointer mode | spread | progress+TOC) so groups flow to a second line at small widths; the slider shrinks first (grow + min-width), everything else stays whole. Fixed-layout row drops its overflow-x-auto.
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
"dev": "npm run build:ts:dev && npm run build:css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#86e234d",
|
||||
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#1c812e8",
|
||||
"alpinejs": "^3.15.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
|
||||
+114
-99
@@ -243,17 +243,6 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
<a id="reader-back" href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
|
||||
← Back
|
||||
</a>
|
||||
<button
|
||||
x-show="backStack.length > 0"
|
||||
@click="goBackToLocation()"
|
||||
class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700"
|
||||
title="Return to previous location (Alt+←)"
|
||||
aria-label="Return to previous location"
|
||||
>
|
||||
<svg class="reader-icon" width="18" height="18" aria-hidden="true">
|
||||
<path d="M 9 4 A 6 6 0 1 1 4.5 14 M 4.5 14 L 2 11 M 4.5 14 L 8 15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
|
||||
<div class="flex items-center gap-1">
|
||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
|
||||
@@ -266,52 +255,70 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
<!-- Bottom bar -->
|
||||
<div id="reader-bottombar" class="fixed bottom-0 left-0 right-0 border-t z-40 pb-[env(safe-area-inset-bottom)] reader-glass">
|
||||
<!-- Reflowable row -->
|
||||
<div x-show="!isFixedLayout" class="flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1">
|
||||
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
id="progress-slider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
list="tick-marks"
|
||||
@input="goToFraction($event.target.value)"
|
||||
class="grow"
|
||||
/>
|
||||
<datalist id="tick-marks"></datalist>
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 mx-1 reader-sep"></div>
|
||||
<div id="progress-display" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
if metadata.EstimatedPages > 0 {
|
||||
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
|
||||
<div x-show="!isFixedLayout" class="flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1">
|
||||
<div class="flex items-center gap-0.5 grow min-w-[190px]">
|
||||
<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>
|
||||
<button x-show="backStack.length > 0" @click="goBackToLocation()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Return to previous location (Alt+←)" aria-label="Return to previous location">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z"></path>
|
||||
<circle cx="12" cy="9.5" r="2"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
id="progress-slider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
list="tick-marks"
|
||||
@input="goToFraction($event.target.value)"
|
||||
class="grow"
|
||||
/>
|
||||
<datalist id="tick-marks"></datalist>
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-px h-6 reader-sep"></div>
|
||||
<div id="progress-display" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">
|
||||
if 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>
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-px h-6 reader-sep"></div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
<div class="w-px h-6 mx-1 reader-sep"></div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
<!-- Fixed-layout row (comics, PDFs, manga) -->
|
||||
<div x-show="isFixedLayout" class="flex items-center px-1.5 py-1.5 gap-0.5 sm:px-2 sm:py-2 sm:gap-1 overflow-x-auto">
|
||||
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Go Left (←)" aria-label="Go left">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="isFixedLayout" class="flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<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>
|
||||
<button x-show="backStack.length > 0" @click="goBackToLocation()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Return to previous location (Alt+←)" aria-label="Return to previous location">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z"></path>
|
||||
<circle cx="12" cy="9.5" r="2"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
id="progress-slider-fx"
|
||||
type="range"
|
||||
@@ -319,70 +326,78 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
max="1"
|
||||
step="any"
|
||||
@input="goToFraction($event.target.value)"
|
||||
class="grow min-w-[4rem]"
|
||||
class="grow min-w-[3rem]"
|
||||
/>
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Go Right (→)" aria-label="Go right">
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right (→)" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
||||
<!-- Fit mode -->
|
||||
<select
|
||||
@change="applyFitMode($event.target.value)"
|
||||
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
|
||||
title="Fit mode"
|
||||
aria-label="Fit mode"
|
||||
>
|
||||
<option value="fit-page">Fit Page</option>
|
||||
<option value="fit-width">Fit Width</option>
|
||||
</select>
|
||||
<!-- Zoom -->
|
||||
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Zoom Out (-)" aria-label="Zoom out">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem] shrink-0" title="Reset Zoom (0)" aria-label="Reset zoom">
|
||||
<span x-text="zoomPercent + '%'">100%</span>
|
||||
</button>
|
||||
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Zoom In (+)" aria-label="Zoom in">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Magnifier -->
|
||||
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5"></circle>
|
||||
<path d="M 13 13 L 18 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 mx-1 reader-sep"></div>
|
||||
<!-- Fit mode + zoom cluster -->
|
||||
<div class="flex items-center gap-1">
|
||||
<select
|
||||
@change="applyFitMode($event.target.value)"
|
||||
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
|
||||
title="Fit mode"
|
||||
aria-label="Fit mode"
|
||||
>
|
||||
<option value="fit-page">Fit Page</option>
|
||||
<option value="fit-width">Fit Width</option>
|
||||
</select>
|
||||
<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>
|
||||
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom (0)" aria-label="Reset zoom">
|
||||
<span x-text="zoomPercent + '%'">100%</span>
|
||||
</button>
|
||||
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" 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>
|
||||
<!-- Recenter: reset pan, keep zoom -->
|
||||
<button @click="recenterView()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Recenter page (reset pan, keep zoom)" aria-label="Recenter page">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Magnifier -->
|
||||
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5"></circle>
|
||||
<path d="M 13 13 L 18 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Pointer mode (PDF only): Smart / Pan / Text -->
|
||||
<div class="reader-seg shrink-0" x-show="isPDF" role="group" aria-label="Pointer mode" title="Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.">
|
||||
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')" title="Smart: drag text to select, drag blank to pan">Smart</button>
|
||||
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')" title="Pan: drag anywhere to move">Pan</button>
|
||||
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
|
||||
</div>
|
||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
||||
<!-- Double page spread -->
|
||||
<button @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
|
||||
<button @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
||||
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden shrink-0">
|
||||
<span x-text="progressMain">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||
} else {
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
}
|
||||
</span>
|
||||
<!-- Progress + TOC -->
|
||||
<div class="flex items-center gap-1">
|
||||
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span x-text="progressMain">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||
} else {
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 shrink-0" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-11
File diff suppressed because one or more lines are too long
@@ -1417,6 +1417,12 @@ document.addEventListener("alpine:init", () => {
|
||||
this.interactionMode = mode;
|
||||
saveSettings({ pdf_interaction_mode: mode });
|
||||
},
|
||||
// Reset pan offsets while keeping the current zoom level.
|
||||
recenterView() {
|
||||
if (!this.isFixedLayout || !this.renderer) return;
|
||||
this.renderer.recenter?.();
|
||||
this.renderer.dragOffset = { x: 0, y: 0 };
|
||||
},
|
||||
applyFitMode(mode: string) {
|
||||
if (!this.isFixedLayout || !this.renderer) return;
|
||||
if (mode === "fit-width" || mode === "fit-page") {
|
||||
|
||||
Reference in New Issue
Block a user