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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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://greasyfork.org/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
    });
})();