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-02-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         RSS: FreshRSS Gestures and Auto-Scroll
// @namespace    http://tampermonkey.net/
// @version      3.3
// @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 = 50;    // 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 - 25; // 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 });

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