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 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:40:18 +07:00
parent 04359ea034
commit f2126733bd
+66
View File
@@ -1200,6 +1200,71 @@
setInterval(fire, 1500); // catch routes that bypass history 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 // Boot
// ============================================================ // ============================================================
@@ -1209,6 +1274,7 @@
state.page = detect(); state.page = detect();
render(); render();
installNavWatcher(); installNavWatcher();
installLongPress();
window.addEventListener("online", drain); // signal returned while the page stayed open window.addEventListener("online", drain); // signal returned while the page stayed open
// Sync first: both auto-record and the latest-chapter checks below need to // Sync first: both auto-record and the latest-chapter checks below need to
// know which series are bookmarked and how fresh they are. // know which series are bookmarked and how fresh they are.