fix(reader): harden swipe/long-press coexistence and next-chapter prefetch
All checks were successful
deploy / test-backend (push) Successful in 32m21s
deploy / test-frontend (push) Successful in 10m33s
deploy / build-and-push (push) Successful in 11m40s
deploy / deploy (push) Successful in 18s

- A long-press now cancels any pending swipe, so a hold-then-drag gesture
  opens the action sheet without also turning the page (regression-tested).
- The next-chapter prefetch no longer retries on every page flip when the
  endpoint fails (guard clears only when the next chapter changes), and it
  clears a previous chapter's warmed images on navigation so stale hidden
  <img>s don't linger.
- Tap zones set touch-action: pan-y so vertical panning stays native while
  horizontal swipes reach our handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 21:57:36 +02:00
parent bfaa166e0a
commit 9c4a93c058
8 changed files with 72 additions and 9 deletions

View File

@@ -69,6 +69,10 @@
const startY = e.clientY;
pressTimer = setTimeout(() => {
pressTimer = null;
// A long-press consumed the gesture — cancel any swipe so a
// subsequent drag-then-lift (finger held, then moved) doesn't
// also turn the page on top of opening the action sheet.
swipeStart = null;
suppressNextClick = true;
if (suppressExpiryTimer != null) clearTimeout(suppressExpiryTimer);
suppressExpiryTimer = setTimeout(() => {
@@ -200,6 +204,9 @@
padding: 0;
margin: 0;
cursor: pointer;
/* Let vertical panning through to the page but keep horizontal
gestures for our swipe handler rather than browser scroll. */
touch-action: pan-y;
/* No visible style — these are interaction surfaces, not
controls. Focus-visible is honored so keyboard users get
a ring if they tab into one. */

View File

@@ -973,11 +973,18 @@
$effect(() => {
const nc = nextChapter;
// Drop a previous chapter's warmed images once the next chapter
// changes (e.g. after navigating), so stale <img>s don't linger.
if (preloadedForChapterId !== null && (!nc || preloadedForChapterId !== nc.id)) {
preloadedForChapterId = null;
nextPreloadUrls = [];
}
if (!nc || pages.length === 0) return;
if (pages.length - furthestPage > PRELOAD_WITHIN_PAGES) return;
if (preloadedForChapterId === nc.id) return;
// Guard set before the await so overlapping effect runs don't
// double-fetch; cleared on failure so a later pass can retry.
// Marked before the await and NOT reset on failure, so a broken
// endpoint can't re-fetch on every page flip near the end — the
// guard clears only when the next chapter itself changes (above).
preloadedForChapterId = nc.id;
getChapterPages(manga.id, nc.id)
.then((ps) => {
@@ -987,7 +994,7 @@
.map((p) => fileUrl(p.storage_key));
})
.catch(() => {
if (preloadedForChapterId === nc.id) preloadedForChapterId = null;
// Best-effort prefetch — swallow and don't retry this chapter.
});
});