From 84c2a82c2c8b7727b9a4415e1052af280115f193 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Mon, 27 Apr 2026 13:31:10 -0500 Subject: [PATCH] Feat: Touch Gestures (Pinch Zoom) for Reader (#29) --- .../reader/components/PageView.svelte | 65 ++++- src/features/reader/components/Reader.svelte | 13 +- .../components/ReaderPresetPanel.svelte | 23 ++ src/features/reader/lib/pinchZoom.ts | 78 +++++ src/store/state.svelte.ts | 270 ++++-------------- src/types/history.ts | 35 +++ src/types/index.ts | 2 + src/types/settings.ts | 156 ++++++++++ 8 files changed, 415 insertions(+), 227 deletions(-) create mode 100644 src/features/reader/lib/pinchZoom.ts create mode 100644 src/types/history.ts diff --git a/src/features/reader/components/PageView.svelte b/src/features/reader/components/PageView.svelte index 14b6b1a..a06e30c 100644 --- a/src/features/reader/components/PageView.svelte +++ b/src/features/reader/components/PageView.svelte @@ -3,6 +3,8 @@ import { store } from "@store/state.svelte"; import { readerState } from "../store/readerState.svelte"; import type { StripChapter } from "../lib/scrollHandler"; + import { createPinchTracker } from "../lib/pinchZoom"; + import type { PinchTracker } from "../lib/pinchZoom"; interface Props { style: string; @@ -16,6 +18,9 @@ stripToRender: StripChapter[]; fadingOut: boolean; tapToToggleBar: boolean; + pinchZoomEnabled: boolean; + onGetZoom: () => number; + onSetZoom: (z: number) => void; resolveUrl: (url: string, priority?: number) => Promise; onTap: (e: MouseEvent) => void; onWheel: (e: WheelEvent) => void; @@ -26,7 +31,8 @@ const { style, imgCls, effectiveWidth, loading, error, pageReady, pageGroups, currentGroup, stripToRender, fadingOut, - tapToToggleBar, resolveUrl, onTap, onWheel, onToggleUi, bindContainer, + tapToToggleBar, pinchZoomEnabled, onGetZoom, onSetZoom, + resolveUrl, onTap, onWheel, onToggleUi, bindContainer, }: Props = $props(); const INSPECT_ZOOM_STEP = 0.15; @@ -57,12 +63,28 @@ let inspectPanStartX = 0; let inspectPanStartY = 0; - // Drag-to-scroll state for longstrip mode let stripDragging = false; let stripDragMoved = false; let stripDragStartY = 0; let stripScrollStart = 0; + let pinch: PinchTracker | null = null; + + $effect(() => { + if (pinchZoomEnabled) { + pinch = createPinchTracker({ + getZoom: onGetZoom, + setZoom: onSetZoom, + getInspectScale: () => readerState.inspectScale, + setInspectScale: (s) => { readerState.inspectScale = s; }, + resetInspectPan: () => { readerState.inspectPanX = 0; readerState.inspectPanY = 0; }, + isLongstrip: () => style === "longstrip", + }); + } else { + pinch = null; + } + }); + export function onInspectMouseDown(e: MouseEvent) { if (style === "longstrip") { stripDragging = true; @@ -103,13 +125,43 @@ inspectDragging = false; } + export function onPointerDown(e: PointerEvent) { + pinch?.onPointerDown(e); + } + + export function onPointerMove(e: PointerEvent) { + if (pinch?.isPinching()) { + pinch.onPointerMove(e); + return; + } + if (stripDragging) { + const dy = e.clientY - stripDragStartY; + if (!stripDragMoved && Math.abs(dy) > 4) stripDragMoved = true; + if (containerEl) containerEl.scrollTop = stripScrollStart - dy; + } + if (inspectDragging) { + if (!inspectDragMoved && Math.abs(e.clientX - inspectDragStartX) + Math.abs(e.clientY - inspectDragStartY) > 4) inspectDragMoved = true; + const rawX = inspectPanStartX + (e.clientX - inspectDragStartX); + const rawY = inspectPanStartY + (e.clientY - inspectDragStartY); + const [cx, cy] = clampInspectPan(readerState.inspectScale, rawX, rawY); + readerState.inspectPanX = cx; + readerState.inspectPanY = cy; + } + } + + export function onPointerUp(e: PointerEvent) { + pinch?.onPointerUp(e); + if (!pinch?.isPinching()) { + stripDragging = false; + inspectDragging = false; + } + } + export function handleWheel(e: WheelEvent) { if (style === "longstrip") { - // In longstrip, Ctrl+scroll drives reader-level zoom; plain scroll scrolls naturally. if (e.ctrlKey) { onWheel(e); } return; } - // In paged modes, Ctrl+scroll drives inspect-zoom (magnify); plain scroll pages forward/back. if (!e.ctrlKey) { onWheel(e); return; } e.preventDefault(); const delta = e.deltaY < 0 ? INSPECT_ZOOM_STEP : -INSPECT_ZOOM_STEP; @@ -154,6 +206,7 @@ onclick={handleTap} ondblclick={(e) => { if (tapToToggleBar) { const x = e.clientX / window.innerWidth; if (x >= 0.3 && x <= 0.7) onToggleUi(); } }} onmousedown={onInspectMouseDown} + onpointerdown={pinchZoomEnabled ? onPointerDown : undefined} onwheel={(e) => { if (e.ctrlKey || style !== "longstrip") e.preventDefault(); }} style:cursor={style === "longstrip" ? (stripDragging ? "grabbing" : "grab") : undefined} onkeydown={(e) => { if (e.key === " " && style === "longstrip") { e.preventDefault(); containerEl?.scrollBy({ top: containerEl.clientHeight * 0.85, behavior: "smooth" }); } }} @@ -217,12 +270,14 @@ \ No newline at end of file diff --git a/src/features/reader/components/ReaderPresetPanel.svelte b/src/features/reader/components/ReaderPresetPanel.svelte index 6e4f252..00a213f 100644 --- a/src/features/reader/components/ReaderPresetPanel.svelte +++ b/src/features/reader/components/ReaderPresetPanel.svelte @@ -266,6 +266,16 @@ aria-checked={effectiveSettings.optimizeContrast} > +