RSS: FreshRSS Gestures and Auto-Scroll

Gesture controls (swipe/double-tap) for FreshRSS: double-tap top half to close articles; double-tap bottom half and edge swipe to jump to article end

// ==UserScript==
// @name         RSS: FreshRSS Gestures and Auto-Scroll
// @namespace    http://tampermonkey.net/
// @version      4.1
// @description  Gesture controls (swipe/double-tap) for FreshRSS: double-tap top half to close articles; double-tap bottom half and edge swipe to jump to article end
// @author       Your Name
// @homepage     https://gf.qytechs.cn/en/scripts/525912
// @match        http://192.168.1.2:1030/*
// @grant        none
// ==/UserScript==
(function() {
    'use strict';

    // Debug mode
    const DEBUG = false;

    // Swipe detection configuration
    const EDGE_THRESHOLD = 10;    // Distance from edge to start swipe
    const SWIPE_THRESHOLD = 50;   // Minimum distance for a swipe

    let touchStartX = 0;
    let touchStartY = 0;

    function debugLog(message) {
        if (DEBUG) {
            console.log(`[FreshRSS Script]: ${message}`);
        }
    }

    debugLog('Script loaded');

    // Function to scroll to element
    function scrollToElement(element) {
        if (element) {
            const header = document.querySelector('header');
            const headerHeight = header ? header.offsetHeight : 0;
            const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
            const scrollTarget = elementPosition - headerHeight - 20; // 20px padding from header

            window.scrollTo({
                top: scrollTarget,
                behavior: 'smooth'
            });
            debugLog('Scrolling element near top: ' + element.id);
        }
    }

    // Function to scroll to next element with peek
    function scrollToNextElement(element) {
        if (element) {
            const nextElement = element.nextElementSibling;
            if (nextElement) {
                const header = document.querySelector('header');
                const headerHeight = header ? header.offsetHeight : 0;
                const nextElementPosition = nextElement.getBoundingClientRect().top + window.pageYOffset;
                const scrollTarget = nextElementPosition - headerHeight - 200; // px padding from header

                window.scrollTo({
                    top: scrollTarget,
                    behavior: 'smooth'
                });
                debugLog('Scrolled to show next element near top');
            }
        }
    }

    // Function to close active article
    function closeActiveArticle(element) {
        if (element) {
            element.classList.remove('active');
            debugLog('Closed article');
            element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }
    }

    // Handle double-tap to close or jump to end
    document.addEventListener('dblclick', function(event) {
        // Check if the device is a mobile device
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        const isSmallScreen = window.innerWidth <= 768; // Adjust the screen width threshold as needed
        if (!isMobile || !isSmallScreen) {
            return; // Exit if not on a mobile device or if the screen is too large
        }

        const interactiveElements = ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT', 'LABEL'];
        if (interactiveElements.includes(event.target.tagName)) {
            debugLog('Ignored double-tap on interactive element');
            return;
        }

        const activeElement = event.target.closest('.flux.active');
        if (activeElement) {
            const screenMidpoint = window.innerHeight / 2; // Vertical midpoint of the screen
            const tapY = event.clientY; // Y-coordinate of the double-tap

            if (tapY < screenMidpoint) {
                // Double-tap on the top half: close the article
                event.preventDefault();
                closeActiveArticle(activeElement);
                debugLog('Double-tap on top half: closed article');
            } else {
                // Double-tap on the bottom half: jump to the end of the article
                event.preventDefault();
                scrollToNextElement(activeElement);
                debugLog('Double-tap on bottom half: jumped to end of article');
            }
        }
    });

    // Touch event handlers for swipe detection
    document.addEventListener('touchstart', function(event) {
        touchStartX = event.touches[0].clientX;
        touchStartY = event.touches[0].clientY;

        // If touch starts from near either edge, prevent default
        if (touchStartX <= EDGE_THRESHOLD ||
            touchStartX >= window.innerWidth - EDGE_THRESHOLD) {
            event.preventDefault();
            debugLog('Touch started near edge');
        }
    }, { passive: false });

    document.addEventListener('touchmove', function(event) {
        const currentX = event.touches[0].clientX;
        const deltaX = currentX - touchStartX;

        // Prevent default during edge swipes
        if ((touchStartX <= EDGE_THRESHOLD && deltaX > 0) ||
            (touchStartX >= window.innerWidth - EDGE_THRESHOLD && deltaX < 0)) {
            event.preventDefault();
            debugLog('Preventing default during edge swipe');
        }
    }, { passive: false });

    document.addEventListener('touchend', function(event) {
        if (!touchStartX) return;

        const touchEndX = event.changedTouches[0].clientX;
        const deltaX = touchEndX - touchStartX;

        const activeElement = document.querySelector('.flux.active');

        if (activeElement) {
            // Left-to-right swipe from left edge
            if (touchStartX <= EDGE_THRESHOLD && deltaX >= SWIPE_THRESHOLD) {
                event.preventDefault();
                scrollToNextElement(activeElement);
                debugLog('Left edge swipe detected');
            }
            // Right-to-left swipe from right edge
            else if (touchStartX >= window.innerWidth - EDGE_THRESHOLD &&
                    deltaX <= -SWIPE_THRESHOLD) {
                event.preventDefault();
                scrollToNextElement(activeElement);
                debugLog('Right edge swipe detected');
            }
        }

        // Reset touch tracking
        touchStartX = 0;
        touchStartY = 0;
    }, { passive: false });

    let lastSpacePressTime = 0;
    const doublePressThreshold = 300; // Time in milliseconds (adjust as needed)
    document.addEventListener('keydown', function(event) {
        // Check if the pressed key is ' ' (space) and not in an input field or similar
        if (event.key === ' ' && !isInputField(event.target)) {
            const currentTime = Date.now();

            // Check if the time between two spacebar presses is within the threshold
            if (currentTime - lastSpacePressTime <= doublePressThreshold) {
                event.preventDefault(); // Prevent the default spacebar behavior
                const activeElement = document.querySelector('.flux.active');
                if (activeElement) {
                    scrollToNextElement(activeElement);
                    debugLog('Double spacebar shortcut triggered scroll to next element');
                }
            }

            // Update the last spacebar press time
            lastSpacePressTime = currentTime;
        }
    });

    // Add keyboard shortcut key to scroll to next element with peek
    document.addEventListener('keydown', function(event) {
        // Check if the pressed key is 'v' and not in an input field or similar
        if (event.key === 'b' && !isInputField(event.target)) {
            const activeElement = document.querySelector('.flux.active');
            if (activeElement) {
                event.preventDefault();
                scrollToNextElement(activeElement);
                debugLog('Keyboard shortcut "v" triggered scroll to next element');
            }
        }
    });

    // Function to check if the target element is an input field or similar
    function isInputField(element) {
        const inputTypes = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'];
        return inputTypes.includes(element.tagName) || element.isContentEditable;
    }

    // Mutation observer to catch programmatic changes
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.target.classList && mutation.target.classList.contains('flux')) {
                if (mutation.target.classList.contains('active')) {
                    debugLog('Article became active via mutation');
                    scrollToElement(mutation.target);
                }
            }
        });
    });

    // Start observing the document with the configured parameters
    observer.observe(document.body, {
        attributes: true,
        attributeFilter: ['class'],
        subtree: true
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址