From f2126733bd50a3e8afbb7432ab89e6df4da06aa6 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Tue, 28 Jul 2026 15:40:18 +0700 Subject: [PATCH] feat: two-finger long-press opens the panel A second way in that does not depend on finding a 7px tab. Two fingers rather than one because Chromium claims the one-finger long press for image menus and text selection, and a manga reader is all images. Passive listeners; a pinch-zoom cancels via the 15px slop check. Co-Authored-By: Claude Opus 5 --- userscript/manga-bookmark.user.js | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/userscript/manga-bookmark.user.js b/userscript/manga-bookmark.user.js index b518396..9ee6b7b 100644 --- a/userscript/manga-bookmark.user.js +++ b/userscript/manga-bookmark.user.js @@ -1200,6 +1200,71 @@ setInterval(fire, 1500); // catch routes that bypass history } + // ============================================================ + // Two-finger long-press — a way in that does not depend on finding a 7px tab + // + // Deliberately two fingers, not one: Chromium claims the one-finger long + // press for image context menus, text selection, and link previews, and on a + // manga reader the whole viewport is images. It assigns no two-finger long + // press, so these listeners stay passive and never preventDefault. + // ============================================================ + + const LONGPRESS_MS = 500; + const LONGPRESS_SLOP = 15; // px either finger may drift before it counts as a drag + + function installLongPress() { + let timer = null; + let start = null; // [{x, y}, {x, y}] — where each finger landed + + const cancel = () => { + if (timer) clearTimeout(timer); + timer = null; + start = null; + }; + + const opts = { passive: true, capture: true }; + + // Fires once per finger, so the first touch cancels and the second arms. + // A third finger cancels again. + document.addEventListener( + "touchstart", + (e) => { + cancel(); + if (e.touches.length !== 2) return; + start = [ + { x: e.touches[0].clientX, y: e.touches[0].clientY }, + { x: e.touches[1].clientX, y: e.touches[1].clientY }, + ]; + timer = setTimeout(() => { + cancel(); + togglePanel(); + }, LONGPRESS_MS); + }, + opts + ); + + // A pinch-zoom is two fingers held down, so the slop check is what keeps it + // from opening the panel mid-gesture. + document.addEventListener( + "touchmove", + (e) => { + if (!start || e.touches.length !== 2) return cancel(); + for (let i = 0; i < 2; i++) { + if ( + Math.abs(e.touches[i].clientX - start[i].x) > LONGPRESS_SLOP || + Math.abs(e.touches[i].clientY - start[i].y) > LONGPRESS_SLOP + ) { + return cancel(); + } + } + }, + opts + ); + + document.addEventListener("touchend", cancel, opts); + document.addEventListener("touchcancel", cancel, opts); + } + // ============================================================ // Boot // ============================================================ @@ -1209,6 +1274,7 @@ state.page = detect(); render(); installNavWatcher(); + installLongPress(); window.addEventListener("online", drain); // signal returned while the page stayed open // Sync first: both auto-record and the latest-chapter checks below need to // know which series are bookmarked and how fresh they are.