RSS: FreshRSS Gestures and Auto-Scroll

Gesture controls (swipe/double-tap) for FreshRSS: double-tap to close articles, edge swipe to jump to article end, and auto-scroll

目前為 2025-03-05 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         RSS: FreshRSS Gestures and Auto-Scroll
// @namespace    http://tampermonkey.net/
// @version      3.8
// @description  Gesture controls (swipe/double-tap) for FreshRSS: double-tap to close articles, edge swipe to jump to article end, and auto-scroll
// @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
    document.addEventListener('dblclick', function(event) {
        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) {
            closeActiveArticle(activeElement);
        }
    });

    // 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
    });
})();