feat(reader): wire double-page spread setting into web reader

The double_page_spread checkbox in the reader settings panel was inert:
it had no Alpine binding, no apply logic, and no persistence. Default
was also inconsistent (false in settings-manager, absent from server
defaults).

- Add doublePageSpread state to the reader Alpine component, loaded
  from saved settings (default true)
- Add applyDoublePageSpread() which sets the renderer's 'spread'
  attribute to auto/none and persists the setting via saveSettings
- Apply the spread attribute during fixed-layout renderer init
- Bind the settings checkbox with x-model and @change
- Add double_page_spread: true to ReaderService server defaults so
  new users get the same starting value the client expects
- Also improve the PDF pan/select toolbar button: distinct smart-
  select vs pan icons, highlighted state while pan mode is active,
  and dynamic tooltips/aria-labels explaining each mode
This commit is contained in:
2026-08-14 08:17:25 -04:00
parent 04e2a069d6
commit f5d9578375
5 changed files with 29 additions and 12 deletions
+1
View File
@@ -403,6 +403,7 @@ func (s *ReaderService) getDefaultSettings() map[string]interface{} {
"tap_zone_size": 30,
"auto_scroll": false,
"panel_zoom_enabled": true,
"double_page_spread": true,
// Dockable panel defaults
"panel_layout": map[string]interface{}{
+8 -3
View File
@@ -153,8 +153,13 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
</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">
<button x-show="isPDF" @click="toggleInteractionMode()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="interactionMode === 'pan' ? 'bg-blue-600 hover:bg-blue-700' : ''" :title="interactionMode === 'pan' ? 'Pan mode: drag anywhere to move. Click for Smart select.' : 'Smart select: drag text to select, drag blank to pan (Shift = force pan). Click for Pan mode.'" :aria-label="interactionMode === 'pan' ? 'Pan mode' : 'Smart select mode'">
<!-- Smart select (default): text cursor -->
<svg x-show="interactionMode !== 'pan'" class="reader-icon" width="20" height="20" aria-hidden="true">
<path d="M 10 3 L 10 17 M 7 5 L 10 4 L 13 5 M 7 15 L 10 16 L 13 15"></path>
</svg>
<!-- Force pan: move arrow -->
<svg x-show="interactionMode === 'pan'" 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>
@@ -306,7 +311,7 @@ templ ReaderSettingsPanel() {
<div class="mb-6" x-show="isFixedLayout">
<h3 class="font-semibold mb-2">Navigation</h3>
<label class="flex items-center mb-2">
<input type="checkbox" name="double_page_spread" class="mr-2"/>
<input type="checkbox" name="double_page_spread" x-model="doublePageSpread" @change="applyDoublePageSpread()" class="mr-2"/>
Double Page Spread
</label>
</div>
File diff suppressed because one or more lines are too long
+11
View File
@@ -371,6 +371,7 @@ document.addEventListener("alpine:init", () => {
isPDF: false,
interactionMode: "select" as string,
magnifierEnabled: false,
doublePageSpread: true as boolean,
progressText: "",
progressLabel: "",
progressMain: "",
@@ -454,6 +455,7 @@ document.addEventListener("alpine:init", () => {
this.readingFont = this.settings.reading_font || "literata";
this.fontSize = this.settings.font_size || 18;
this.lineHeight = this.settings.line_height || 1.6;
this.doublePageSpread = this.settings.double_page_spread ?? true;
if (this.settings.reading_mode) {
this.readingMode = this.settings.reading_mode;
} else {
@@ -492,6 +494,7 @@ document.addEventListener("alpine:init", () => {
this.zoomPercent = this.renderer.zoomPercent;
});
this.computeFixedLayoutChapterBoundaries();
this.applyDoublePageSpread();
} else {
this.renderer.setStyles?.(this.buildCSS());
}
@@ -692,6 +695,14 @@ document.addEventListener("alpine:init", () => {
this.renderer.setAttribute("interaction-mode", next);
this.interactionMode = next;
},
applyDoublePageSpread() {
if (!this.isFixedLayout || !this.renderer) return;
this.renderer.setAttribute(
"spread",
this.doublePageSpread ? "auto" : "none",
);
saveSettings({ double_page_spread: this.doublePageSpread });
},
goLeft() {
this.view?.goLeft?.();
},
+1 -1
View File
@@ -78,7 +78,7 @@ export function getDefaultSettings(): ReaderSettings {
font_size: 18,
line_height: 1.6,
margin_width: 20,
double_page_spread: false,
double_page_spread: true,
reading_direction: "ltr",
hardware_acceleration: true,
panel_layout: {