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"
|
"dev": "npm run build:ts:dev && npm run build:css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"alpinejs": "^3.15.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"highlight.js": "^11.11.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">
|
<a id="reader-back" href={ "/media/" + metadata.MediaItemID } class="text-base sm:text-lg hover:underline">
|
||||||
← Back
|
← Back
|
||||||
</a>
|
</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>
|
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
|
||||||
<div class="flex items-center gap-1">
|
<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>
|
<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 -->
|
<!-- 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">
|
<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 -->
|
<!-- 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">
|
<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">
|
||||||
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
<div class="flex items-center gap-0.5 grow min-w-[190px]">
|
||||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left" aria-label="Go left">
|
||||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||||
</svg>
|
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||||
</button>
|
</svg>
|
||||||
<input
|
</button>
|
||||||
id="progress-slider"
|
<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">
|
||||||
type="range"
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||||
min="0"
|
<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>
|
||||||
max="1"
|
<circle cx="12" cy="9.5" r="2"></circle>
|
||||||
step="any"
|
</svg>
|
||||||
list="tick-marks"
|
</button>
|
||||||
@input="goToFraction($event.target.value)"
|
<input
|
||||||
class="grow"
|
id="progress-slider"
|
||||||
/>
|
type="range"
|
||||||
<datalist id="tick-marks"></datalist>
|
min="0"
|
||||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
max="1"
|
||||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
step="any"
|
||||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
list="tick-marks"
|
||||||
</svg>
|
@input="goToFraction($event.target.value)"
|
||||||
</button>
|
class="grow"
|
||||||
<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">
|
<datalist id="tick-marks"></datalist>
|
||||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">
|
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right" aria-label="Go right">
|
||||||
if progress.FormatGroup == "reflowable" {
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||||
if metadata.EstimatedPages > 0 {
|
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||||
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
|
</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 {
|
} else {
|
||||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||||
}
|
}
|
||||||
} else {
|
</span>
|
||||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
</div>
|
||||||
}
|
<div class="w-px h-6 reader-sep"></div>
|
||||||
</span>
|
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<!-- Fixed-layout row (comics, PDFs, manga) -->
|
<!-- 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">
|
<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">
|
||||||
<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">
|
<div class="flex items-center gap-0.5">
|
||||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left (←)" aria-label="Go left">
|
||||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||||
</svg>
|
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||||
</button>
|
</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
|
<input
|
||||||
id="progress-slider-fx"
|
id="progress-slider-fx"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -319,70 +326,78 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
|||||||
max="1"
|
max="1"
|
||||||
step="any"
|
step="any"
|
||||||
@input="goToFraction($event.target.value)"
|
@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">
|
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
<div class="w-px h-6 mx-1 reader-sep"></div>
|
||||||
<!-- Fit mode -->
|
<!-- Fit mode + zoom cluster -->
|
||||||
<select
|
<div class="flex items-center gap-1">
|
||||||
@change="applyFitMode($event.target.value)"
|
<select
|
||||||
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
|
@change="applyFitMode($event.target.value)"
|
||||||
title="Fit mode"
|
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
|
||||||
aria-label="Fit mode"
|
title="Fit mode"
|
||||||
>
|
aria-label="Fit mode"
|
||||||
<option value="fit-page">Fit Page</option>
|
>
|
||||||
<option value="fit-width">Fit Width</option>
|
<option value="fit-page">Fit Page</option>
|
||||||
</select>
|
<option value="fit-width">Fit Width</option>
|
||||||
<!-- Zoom -->
|
</select>
|
||||||
<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">
|
<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">
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||||
<path d="M 5 10 L 15 10"></path>
|
<path d="M 5 10 L 15 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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">
|
<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>
|
<span x-text="zoomPercent + '%'">100%</span>
|
||||||
</button>
|
</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">
|
<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">
|
<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>
|
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Magnifier -->
|
<!-- Recenter: reset pan, keep zoom -->
|
||||||
<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">
|
<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">
|
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||||
<circle cx="9" cy="9" r="5"></circle>
|
<circle cx="12" cy="12" r="4"></circle>
|
||||||
<path d="M 13 13 L 18 18"></path>
|
<path d="M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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 -->
|
<!-- 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.">
|
<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 === '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 === '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>
|
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
|
||||||
<!-- Double page spread -->
|
<!-- 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">
|
<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>
|
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="w-px h-6 mx-1 shrink-0 reader-sep"></div>
|
<!-- Progress + TOC -->
|
||||||
<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">
|
<div class="flex items-center gap-1">
|
||||||
<span x-text="progressMain">
|
<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">
|
||||||
if progress.FormatGroup == "reflowable" {
|
<span x-text="progressMain">
|
||||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
if progress.FormatGroup == "reflowable" {
|
||||||
} 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>
|
||||||
|
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||||
</div>
|
</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>
|
</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;
|
this.interactionMode = mode;
|
||||||
saveSettings({ pdf_interaction_mode: 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) {
|
applyFitMode(mode: string) {
|
||||||
if (!this.isFixedLayout || !this.renderer) return;
|
if (!this.isFixedLayout || !this.renderer) return;
|
||||||
if (mode === "fit-width" || mode === "fit-page") {
|
if (mode === "fit-width" || mode === "fit-page") {
|
||||||
|
|||||||
Reference in New Issue
Block a user