diff --git a/vue/src/components/RssFeeds.vue b/vue/src/components/RssFeeds.vue index 7b6d46e..db275b1 100644 --- a/vue/src/components/RssFeeds.vue +++ b/vue/src/components/RssFeeds.vue @@ -34,27 +34,14 @@ function scrollToNextArticle() { } } -// Swipe-to-navigate in article view. The article visually tracks the finger -// while dragging (dragOffsetX/isDragging below), then either completes the -// page-turn or springs back to center on release. This is transient -// per-gesture state, not shared app state, so it's kept local to this -// component rather than in useFeeds() — nothing outside this component needs -// it. -const dragOffsetX = ref(0) // live translateX (px) applied to the current article while dragging or springing back -const isDragging = ref(false) // true only while actively tracking touchmove — disables the CSS transition so the article follows the finger with zero lag; the spring-back and the eventual page-turn both happen after this flips back to false, so they use the (slower, already-tuned) CSS transitions instead - +// Swipe-to-navigate in article view. Transient per-gesture state, not shared +// app state, so it's kept as plain component-local variables rather than in +// useFeeds() — nothing outside this component needs it. let touchStartX = null let touchStartY = null -let touchStartTime = null -let gestureAxis = null // 'horizontal' | 'vertical' | null (undecided) — locked in after DIRECTION_LOCK_THRESHOLD of movement, so a single gesture can't both page the article and scroll the page -let articleWidth = 0 // captured on touchstart; the live drag and the commit-distance check below are relative to it -let dragRafPending = false -let pendingDragOffset = 0 -const DIRECTION_LOCK_THRESHOLD = 10 // px of movement before an ambiguous drag commits to horizontal (swipe) or vertical (scroll) -const COMMIT_DISTANCE_RATIO = 0.35 // fraction of the article's width dragged that counts as "far enough" to complete the page-turn -const COMMIT_VELOCITY = 0.5 // px/ms — a flick at least this fast completes the page-turn even short of the distance ratio -const BOUNDARY_DAMPING = 0.35 // drag multiplier applied when dragging past the first/last article, so it still moves a little (feedback) but resists +const SWIPE_THRESHOLD_X = 50 // min horizontal travel (px) to count as a swipe +const SWIPE_RESTRAINT_Y = 75 // max vertical travel (px) before it's treated as a scroll, not a swipe // Drives which slide-in/slide-out transition ('slide-next' / 'slide-prev') the // article view uses — set right before the index actually changes, by both @@ -78,74 +65,22 @@ function onArticleTouchStart(event) { // — those should scroll the block, not page the article. if (event.touches.length !== 1 || event.target.closest('pre')) { touchStartX = null + touchStartY = null return } - const touch = event.touches[0] - touchStartX = touch.clientX - touchStartY = touch.clientY - touchStartTime = Date.now() - gestureAxis = null - const article = event.currentTarget.querySelector('.article-feature') - articleWidth = article ? article.offsetWidth : window.innerWidth -} - -function onArticleTouchMove(event) { - if (touchStartX === null || event.touches.length !== 1) return - const touch = event.touches[0] - const deltaX = touch.clientX - touchStartX - const deltaY = touch.clientY - touchStartY - - if (gestureAxis === null) { - if (Math.abs(deltaX) < DIRECTION_LOCK_THRESHOLD && Math.abs(deltaY) < DIRECTION_LOCK_THRESHOLD) return - gestureAxis = Math.abs(deltaX) > Math.abs(deltaY) ? 'horizontal' : 'vertical' - if (gestureAxis === 'horizontal') isDragging.value = true - } - if (gestureAxis !== 'horizontal') return // let the page scroll normally instead - - // Stop the page from also scrolling vertically (and the browser's own - // horizontal-overscroll gesture) while we're driving the drag ourselves. - // Requires this listener to not be passive. - event.preventDefault() - - const atBoundary = (deltaX < 0 && currentIndex.value === feeds.value.length - 1) || - (deltaX > 0 && currentIndex.value === 0) - pendingDragOffset = atBoundary ? deltaX * BOUNDARY_DAMPING : deltaX - - // rAF-throttled: touchmove can fire faster than the display refreshes. If a - // frame is already scheduled, later events in that window just update - // pendingDragOffset above — the callback always reads the latest value - // once it actually runs, rather than whatever triggered the first one. - if (dragRafPending) return - dragRafPending = true - requestAnimationFrame(() => { - dragOffsetX.value = pendingDragOffset - dragRafPending = false - }) + touchStartX = event.touches[0].clientX + touchStartY = event.touches[0].clientY } function onArticleTouchEnd(event) { if (touchStartX === null) return - const wasDraggingHorizontally = gestureAxis === 'horizontal' - // Computed fresh from the release position rather than read back from - // dragOffsetX, which is rAF-throttled and so can be a frame stale. - const deltaX = event.changedTouches[0].clientX - touchStartX - const elapsed = Date.now() - touchStartTime - + const touch = event.changedTouches[0] + const deltaX = touch.clientX - touchStartX + const deltaY = touch.clientY - touchStartY touchStartX = null touchStartY = null - gestureAxis = null - isDragging.value = false - if (!wasDraggingHorizontally) return - - // Date.now() only has ~1ms resolution, so a genuinely fast flick often - // measures as elapsed === 0 — treat that as "as fast as it gets" (clears - // the flick check) rather than dividing by zero into "no velocity at all". - const isFastFlick = elapsed > 0 ? Math.abs(deltaX / elapsed) > COMMIT_VELOCITY : deltaX !== 0 - const shouldComplete = Math.abs(deltaX) > articleWidth * COMMIT_DISTANCE_RATIO || isFastFlick - dragOffsetX.value = 0 // springs back to center — or, if completing, the (now off-screen-bound) article being replaced - - if (!shouldComplete) return + if (Math.abs(deltaX) < SWIPE_THRESHOLD_X || Math.abs(deltaY) > SWIPE_RESTRAINT_Y) return if (deltaX < 0) { navigateToNextArticle() @@ -246,13 +181,7 @@ onMounted(async () => { >↓ -
+