RamiEjailat / Auto Scroll Down

// ==UserScript==
// @name         Auto Scroll Down
// @description  Toggles continuous auto-scrolling of the page or Gmail preview pane when Alt+S is pressed or using the floating button
// @author       Rami S Ejailat
// @copyright    RamiEjailat (https://openuserjs.org/users/RamiEjailat)
// @updateURL    https://openuserjs.org/meta/RamiEjailat/Auto_Scroll_Down.meta.js
// @downloadURL  https://openuserjs.org/install/RamiEjailat/Auto_Scroll_Down.user.js
// @license      MIT
// @version      20260727
// @namespace    http://tampermonkey.net/
// @match        *://*/*
// @exclude      *://challenges.cloudflare.com/*
// @grant        none
// @icon         https://cdn-icons-png.flaticon.com/64/11606/11606886.png
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const stepSize = 1;
    const fastSpeed = 20;
    const slowSpeed = 50;

    let scrolling = false;
    let scrollInterval;
    let currentSpeed;
    let dynamicContentTimeout;
    let pausedByFocus = false;

    // Universal Target Selector
    function getScrollTarget() {
        // Special handling if on Gmail
        if (window.location.hostname === 'mail.google.com') {
            const mainWrapper = document.querySelector('[role="main"]')?.lastElementChild?.lastElementChild;
            if (mainWrapper) {
                // If the wrapper itself handles the scrollbar
                if (mainWrapper.scrollHeight > mainWrapper.clientHeight) {
                    return mainWrapper;
                }
                // Look for the internal descendant that holds the active scroll overflow
                const internalPane = Array.from(mainWrapper.querySelectorAll('*')).find(el => {
                    const style = window.getComputedStyle(el);
                    const isScrollableStyle = style.overflowY === 'auto' || style.overflowY === 'scroll';
                    return isScrollableStyle && el.scrollHeight > el.clientHeight;
                });
                if (internalPane) return internalPane;
            }
        }

        // Universal fallback for all other sites (and Gmail main list view if window scrolls)
        return window;
    }

    // Helper to extract dimensions seamlessly whether target is Window or an Element
    function getScrollMetrics(target) {
        if (target === window) {
            return {
                scrollTop: window.scrollY,
                scrollHeight: document.documentElement.scrollHeight,
                clientHeight: window.innerHeight
            };
        } else {
            return {
                scrollTop: target.scrollTop,
                scrollHeight: target.scrollHeight,
                clientHeight: target.clientHeight
            };
        }
    }

    const btnStyle = `
        all: initial;
        position: fixed;
        bottom: 20px;
        left: 20px;
        height: 32px;
        width: 32px;
        padding: 10px;
        background-color: rgba(0, 0, 0, 0.7);
        border: none;
        outline: none;
        font-size: x-large;
        border-radius: 5px;
        cursor: pointer;
        z-index: 2147483646;
        display: none;
    `;

    const button = document.createElement('button');
    button.textContent = '▶️';
    button.style.cssText = btnStyle;
    button.classList.add('autoscrolldown');
    document.body.appendChild(button);

    const topButton = document.createElement('button');
    topButton.textContent = '⬆️';
    topButton.style.cssText = btnStyle;
    topButton.style.left = '80px';
    topButton.classList.add('autoscrolldown');
    document.body.appendChild(topButton);

    const progressBar = document.createElement('div');
    progressBar.classList.add('autoscrolldown');
    Object.assign(progressBar.style, {
        all: 'initial',
        position: 'fixed',
        top: '0',
        left: '0',
        width: '100%',
        height: '2px',
        zIndex: '2147483647',
        display: 'none',
        pointerEvents: 'none',
        backgroundColor: 'rgba(0, 0, 0, 0.08)',
    });
    progressBar.style.setProperty('direction', 'ltr', 'important');

    const progressFill = document.createElement('div');
    progressFill.classList.add('autoscrolldown-fill');
    Object.assign(progressFill.style, {
        height: '100%',
        width: '0%',
        backgroundColor: '#4a90d9',
        transition: 'width 0.1s linear',
    });
    progressFill.style.setProperty('direction', 'ltr', 'important');
    progressBar.appendChild(progressFill);
    document.body.appendChild(progressBar);

    function updateProgressBar() {
        const target = getScrollTarget();
        const metrics = getScrollMetrics(target);
        const scrollableHeight = metrics.scrollHeight - metrics.clientHeight;
        const progress = scrollableHeight > 0 ? (metrics.scrollTop / scrollableHeight) * 100 : 0;
        progressFill.style.width = progress + '%';
    }

    function updateButtonVisibility() {
        const target = getScrollTarget();
        const metrics = getScrollMetrics(target);

        const isScrollable = metrics.scrollHeight - metrics.clientHeight > 5;
        button.style.display = isScrollable ? 'block' : 'none';
        topButton.style.display = (isScrollable && metrics.scrollTop > 0) ? 'block' : 'none';
        progressBar.style.display = isScrollable ? 'block' : 'none';
        if (isScrollable) updateProgressBar();
    }

    function startScrolling(speed) {
        if (!scrolling) {
            scrolling = true;
            button.textContent = '🐰';
        }
        currentSpeed = speed;
        clearInterval(scrollInterval);

        scrollInterval = setInterval(() => {
            const target = getScrollTarget();
            const metrics = getScrollMetrics(target);
            const distanceToBottom = metrics.scrollHeight - metrics.scrollTop - metrics.clientHeight;

            if (distanceToBottom >= stepSize) {
                target.scrollBy(0, stepSize);
                if (dynamicContentTimeout) clearTimeout(dynamicContentTimeout);
                dynamicContentTimeout = setTimeout(stopIfNoDynamicContent, 1000);
            } else {
                stopScrolling();
            }
        }, speed);
        button.blur();
    }

    function stopScrolling() {
        clearInterval(scrollInterval);
        scrolling = false;
        button.textContent = '▶️';
    }

    function stopIfNoDynamicContent() {
        const target = getScrollTarget();
        const metrics = getScrollMetrics(target);
        const distanceToBottom = metrics.scrollHeight - metrics.scrollTop - metrics.clientHeight;
        if (distanceToBottom <= stepSize) {
            stopScrolling();
        }
    }

    function toggleSpeed() {
        if (button.textContent === '🐰') {
            button.textContent = '🐢';
            startScrolling(slowSpeed);
        } else {
            button.textContent = '🐰';
            startScrolling(fastSpeed);
        }
    }

    function pauseForFocus() {
        if (scrolling && !pausedByFocus) {
            pausedByFocus = true;
            clearInterval(scrollInterval);
        }
    }

    function resumeAfterFocus() {
        if (pausedByFocus) {
            pausedByFocus = false;
            if (scrolling) {
                startScrolling(currentSpeed);
            }
        }
    }

    // When switching to another tab in the same window
    document.addEventListener('visibilitychange', () => {
        if (document.hidden) pauseForFocus();
        else resumeAfterFocus();
    });

    // When switching focus to another window or app while the tab stays visible
    window.addEventListener('blur', pauseForFocus);
    window.addEventListener('focus', resumeAfterFocus);

    document.addEventListener('keydown', (event) => {
        if (event.altKey && event.code === 'KeyS') {
            if (scrolling) toggleSpeed();
            else startScrolling(fastSpeed);
            event.preventDefault();
        } else if (event.code === 'Escape' && scrolling) {
            stopScrolling();
            event.preventDefault();
        }
    });

    button.addEventListener('click', () => {
        if (!scrolling) startScrolling(fastSpeed);
        else toggleSpeed();
    });

    button.addEventListener('contextmenu', (event) => {
        stopScrolling();
        event.preventDefault();
    });

    topButton.addEventListener('click', () => {
        const target = getScrollTarget();
        target.scrollTo({ top: 0 });
    });

    // Monitor for changes on the page (essential for single page applications like Gmail)
    new MutationObserver(() => {
        updateButtonVisibility();
    }).observe(document.body, { childList: true, subtree: true });

    // Use capture phase (true) to intercept internal pane scrolls as well as window scrolls
    document.addEventListener('scroll', (event) => {
        const target = getScrollTarget();
        // Fire updates if scrolling the window, or if scrolling our internal targeted element
        if (target === window || event.target === target) {
            updateButtonVisibility();
            updateProgressBar();
        }
    }, true);

    document.addEventListener('mouseover', (event) => {
        if (scrolling && event.target.closest('a')) {
            clearInterval(scrollInterval);
        }
    });

    document.addEventListener('mouseout', (event) => {
        if (scrolling && event.target.closest('a')) {
            startScrolling(currentSpeed);
        }
    });

    window.addEventListener('load', () => {
        setTimeout(updateButtonVisibility, 300);
    });
})();