Auto Scroll to Bottom with Dynamic Loading

Automatically scroll to the bottom of the page, continue scrolling to trigger dynamic content loading, and stop after a configurable timeout if no new content is loaded. Supports multiple languages for better usability and searchability.

// ==UserScript==
// @name         Auto Scroll to Bottom with Dynamic Loading
// @name:zh-CN   自动滚动到底部(支持动态加载)
// @name:en      Auto Scroll to Bottom with Dynamic Loading
// @name:es      Desplazamiento Automático al Final con Carga Dinámica
// @name:ja      自動スクロール(動的読み込み対応)
// @namespace    https://github.com/strangeZombies/PractiseCode
// @version      1.5
// @description  Automatically scroll to the bottom of the page, continue scrolling to trigger dynamic content loading, and stop after a configurable timeout if no new content is loaded. Supports multiple languages for better usability and searchability.
// @description:zh-CN  自动滚动到页面底部,持续滚动以触发动态内容加载,若无新内容则在可配置的超时时间后停止。支持多语言,提升使用和检索便捷性。
// @description:en     Automatically scroll to the bottom of the page, continue scrolling to trigger dynamic content loading, and stop after a configurable timeout if no new content is loaded. Supports multiple languages for better usability and searchability.
// @description:es     Desplaza automáticamente hasta el final de la página, continúa desplazándose para activar la carga de contenido dinámico y se detiene tras un tiempo configurable si no se carga nuevo contenido. Soporta múltiples idiomas para mejor usabilidad y búsqueda.
// @description:ja     ページの最下部まで自動でスクロールし、動的コンテンツの読み込みをトリガーするためにスクロールを継続し、新しいコンテンツが読み込まれない場合は設定可能なタイムアウト後に停止します。多言語対応で使いやすさと検索性を向上。
// @author       strangezombies
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @keyword      auto scroll, infinite scroll, dynamic loading, 自动滚动, 无限滚动, 动态加载, desplazamiento automático, carga dinámica, 自動スクロール, 動的読み込み
// ==/UserScript==

// 由Grok自动生成
(function() {
    'use strict';

    // Language translations
    const translations = {
        'zh-CN': {
            startScroll: '开始滚动',
            stopScroll: '停止滚动',
            openConfig: '打开设置',
            closeConfig: '关闭',
            saveConfig: '保存',
            configTitle: '滚动设置',
            scrollStep: '滚动步长(像素)',
            scrollInterval: '滚动间隔(毫秒)',
            scrollBehavior: '滚动行为',
            scrollBehaviorSmooth: '平滑',
            scrollBehaviorAuto: '即时',
            panelPosition: '面板位置',
            panelPositionTopRight: '右上',
            panelPositionTopLeft: '左上',
            panelPositionBottomRight: '右下',
            panelPositionBottomLeft: '左下',
            maxScrollTimes: '最大滚动次数(0 为无限制)',
            timeoutSeconds: '底部超时(秒)',
            language: '语言',
            languageZhCN: '中文(简体)',
            languageEn: 'English',
            languageEs: 'Español',
            languageJa: '日本語',
            saveSuccess: '配置保存成功!',
            invalidScrollStep: '滚动步长至少为 1。',
            invalidScrollInterval: '滚动间隔至少为 10 毫秒。',
            invalidScrollBehavior: '无效的滚动行为。',
            invalidPanelPosition: '无效的面板位置。',
            invalidMaxScrollTimes: '最大滚动次数必须为 0 或更大。',
            invalidTimeoutSeconds: '超时时间至少为 1 秒。'
        },
        'en': {
            startScroll: 'Start Scroll',
            stopScroll: 'Stop Scroll',
            openConfig: 'Open Config',
            closeConfig: 'Close',
            saveConfig: 'Save',
            configTitle: 'Scroll Settings',
            scrollStep: 'Scroll Step (px)',
            scrollInterval: 'Scroll Interval (ms)',
            scrollBehavior: 'Scroll Behavior',
            scrollBehaviorSmooth: 'Smooth',
            scrollBehaviorAuto: 'Auto',
            panelPosition: 'Panel Position',
            panelPositionTopRight: 'Top Right',
            panelPositionTopLeft: 'Top Left',
            panelPositionBottomRight: 'Bottom Right',
            panelPositionBottomLeft: 'Bottom Left',
            maxScrollTimes: 'Max Scroll Times (0 for unlimited)',
            timeoutSeconds: 'Timeout at Bottom (seconds)',
            language: 'Language',
            languageZhCN: 'Chinese (Simplified)',
            languageEn: 'English',
            languageEs: 'Spanish',
            languageJa: 'Japanese',
            saveSuccess: 'Configuration saved successfully!',
            invalidScrollStep: 'Scroll Step must be at least 1.',
            invalidScrollInterval: 'Scroll Interval must be at least 10ms.',
            invalidScrollBehavior: 'Invalid Scroll Behavior.',
            invalidPanelPosition: 'Invalid Panel Position.',
            invalidMaxScrollTimes: 'Max Scroll Times must be 0 or greater.',
            invalidTimeoutSeconds: 'Timeout must be at least 1 second.'
        },
        'es': {
            startScroll: 'Iniciar Desplazamiento',
            stopScroll: 'Detener Desplazamiento',
            openConfig: 'Abrir Configuración',
            closeConfig: 'Cerrar',
            saveConfig: 'Guardar',
            configTitle: 'Configuración de Desplazamiento',
            scrollStep: 'Paso de Desplazamiento (px)',
            scrollInterval: 'Intervalo de Desplazamiento (ms)',
            scrollBehavior: 'Comportamiento de Desplazamiento',
            scrollBehaviorSmooth: 'Suave',
            scrollBehaviorAuto: 'Instantáneo',
            panelPosition: 'Posición del Panel',
            panelPositionTopRight: 'Arriba Derecha',
            panelPositionTopLeft: 'Arriba Izquierda',
            panelPositionBottomRight: 'Abajo Derecha',
            panelPositionBottomLeft: 'Abajo Izquierda',
            maxScrollTimes: 'Máximo de Desplazamientos (0 para ilimitado)',
            timeoutSeconds: 'Tiempo de Espera en el Fondo (segundos)',
            language: 'Idioma',
            languageZhCN: 'Chino (Simplificado)',
            languageEn: 'Inglés',
            languageEs: 'Español',
            languageJa: 'Japonés',
            saveSuccess: '¡Configuración guardada con éxito!',
            invalidScrollStep: 'El paso de desplazamiento debe ser al menos 1.',
            invalidScrollInterval: 'El intervalo de desplazamiento debe ser al menos 10 ms.',
            invalidScrollBehavior: 'Comportamiento de desplazamiento inválido.',
            invalidPanelPosition: 'Posición del panel inválida.',
            invalidMaxScrollTimes: 'El máximo de desplazamientos debe ser 0 o mayor.',
            invalidTimeoutSeconds: 'El tiempo de espera debe ser al menos 1 segundo.'
        },
        'ja': {
            startScroll: 'スクロール開始',
            stopScroll: 'スクロール停止',
            openConfig: '設定を開く',
            closeConfig: '閉じる',
            saveConfig: '保存',
            configTitle: 'スクロール設定',
            scrollStep: 'スクロールステップ(ピクセル)',
            scrollInterval: 'スクロール間隔(ミリ秒)',
            scrollBehavior: 'スクロール動作',
            scrollBehaviorSmooth: 'スムーズ',
            scrollBehaviorAuto: '即時',
            panelPosition: 'パネル位置',
            panelPositionTopRight: '右上',
            panelPositionTopLeft: '左上',
            panelPositionBottomRight: '右下',
            panelPositionBottomLeft: '左下',
            maxScrollTimes: '最大スクロール回数(0で無制限)',
            timeoutSeconds: '底部でのタイムアウト(秒)',
            language: '言語',
            languageZhCN: '中国語(簡体)',
            languageEn: '英語',
            languageEs: 'スペイン語',
            languageJa: '日本語',
            saveSuccess: '設定が正常に保存されました!',
            invalidScrollStep: 'スクロールステップは1以上でなければなりません。',
            invalidScrollInterval: 'スクロール間隔は10ミリ秒以上でなければなりません。',
            invalidScrollBehavior: '無効なスクロール動作です。',
            invalidPanelPosition: '無効なパネル位置です。',
            invalidMaxScrollTimes: '最大スクロール回数は0以上でなければなりません。',
            invalidTimeoutSeconds: 'タイムアウトは1秒以上でなければなりません。'
        }
    };

    // Default configuration
    const defaultConfig = {
        scrollStep: 100, // Pixels per scroll
        scrollInterval: 50, // Interval between scrolls (ms)
        scrollBehavior: 'smooth', // Scroll behavior: smooth or auto
        panelPosition: 'top-right', // Panel position
        maxScrollTimes: 0, // Max scroll actions, 0 for unlimited
        timeoutSeconds: 10, // Timeout in seconds before stopping at bottom
        language: 'zh-CN' // Default language
    };

    // Load saved configuration or use defaults
    let config = {
        scrollStep: GM_getValue('scrollStep', defaultConfig.scrollStep),
        scrollInterval: GM_getValue('scrollInterval', defaultConfig.scrollInterval),
        scrollBehavior: GM_getValue('scrollBehavior', defaultConfig.scrollBehavior),
        panelPosition: GM_getValue('panelPosition', defaultConfig.panelPosition),
        maxScrollTimes: GM_getValue('maxScrollTimes', defaultConfig.maxScrollTimes),
        timeoutSeconds: GM_getValue('timeoutSeconds', defaultConfig.timeoutSeconds),
        language: GM_getValue('language', defaultConfig.language)
    };

    let isScrolling = false;
    let scrollTimer = null;
    let scrollCount = 0;
    let lastScrollTime = 0;
    let atBottomSince = null;
    let lastScrollHeight = 0;

    // Inject styles
    GM_addStyle(`
        #scrollControlPanel {
            position: fixed;
            ${getPanelPositionStyles(config.panelPosition)}
            background: #fff;
            border: 1px solid #ccc;
            padding: 10px;
            z-index: 10000;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            border-radius: 5px;
        }
        #scrollConfigPanel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #fff;
            border: 1px solid #ccc;
            padding: 20px;
            z-index: 10001;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3);
            border-radius: 5px;
            display: none;
            width: 300px;
            color: #333;
            font-family: Arial, sans-serif;
        }
        #scrollControlPanel button, #scrollConfigPanel button {
            margin: 5px;
            padding: 5px 10px;
            cursor: pointer;
            background: #007bff;
            color: #fff;
            border: none;
            border-radius: 3px;
            font-size: 14px;
        }
        #scrollControlPanel button:hover, #scrollConfigPanel button:hover {
            background: #0056b3;
        }
        #scrollControlPanel .toggle-btn {
            background: #28a745;
        }
        #scrollControlPanel .toggle-btn.stop {
            background: #dc3545;
        }
        #scrollConfigPanel .close-btn {
            background: #6c757d;
        }
        #scrollConfigPanel label {
            display: block;
            margin: 10px 0;
            font-size: 14px;
            color: #333;
        }
        #scrollConfigPanel input, #scrollConfigPanel select {
            width: 100px;
            padding: 3px;
            margin-left: 5px;
            border: 1px solid #ccc;
            border-radius: 3px;
            color: #333;
            background: #f9f9f9;
        }
        #scrollConfigPanel input:hover, #scrollConfigPanel select:hover {
            background: #e0e0e0;
        }
        #scrollConfigPanel h3 {
            margin: 0 0 10px;
            font-size: 16px;
            color: #333;
        }
    `);

    // Get styles for panel position
    function getPanelPositionStyles(position) {
        switch (position) {
            case 'top-left':
                return 'top: 20px; left: 20px;';
            case 'top-right':
                return 'top: 20px; right: 20px;';
            case 'bottom-left':
                return 'bottom: 20px; left: 20px;';
            case 'bottom-right':
                return 'bottom: 20px; right: 20px;';
            default:
                return 'top: 20px; right: 20px;';
        }
    }

    // Get translation for current language
    function t(key) {
        return translations[config.language][key] || translations['en'][key];
    }

    // Create control panel
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'scrollControlPanel';
        panel.innerHTML = `
            <button id="toggleScrollBtn" class="toggle-btn">${t('startScroll')}</button>
            <button id="openConfigBtn">${t('openConfig')}</button>
        `;
        document.body.appendChild(panel);

        // Bind events
        document.getElementById('toggleScrollBtn').addEventListener('click', toggleScroll);
        document.getElementById('openConfigBtn').addEventListener('click', openConfigPanel);
    }

    // Create configuration panel
    function createConfigPanel() {
        const configPanel = document.createElement('div');
        configPanel.id = 'scrollConfigPanel';
        configPanel.innerHTML = `
            <h3>${t('configTitle')}</h3>
            <label>${t('scrollStep')}: <input type="number" id="scrollStepInput" value="${config.scrollStep}" min="1"></label>
            <label>${t('scrollInterval')}: <input type="number" id="scrollIntervalInput" value="${config.scrollInterval}" min="10"></label>
            <label>${t('scrollBehavior')}: 
                <select id="scrollBehaviorSelect">
                    <option value="smooth" ${config.scrollBehavior === 'smooth' ? 'selected' : ''}>${t('scrollBehaviorSmooth')}</option>
                    <option value="auto" ${config.scrollBehavior === 'auto' ? 'selected' : ''}>${t('scrollBehaviorAuto')}</option>
                </select>
            </label>
            <label>${t('panelPosition')}: 
                <select id="panelPositionSelect">
                    <option value="top-right" ${config.panelPosition === 'top-right' ? 'selected' : ''}>${t('panelPositionTopRight')}</option>
                    <option value="top-left" ${config.panelPosition === 'top-left' ? 'selected' : ''}>${t('panelPositionTopLeft')}</option>
                    <option value="bottom-right" ${config.panelPosition === 'bottom-right' ? 'selected' : ''}>${t('panelPositionBottomRight')}</option>
                    <option value="bottom-left" ${config.panelPosition === 'bottom-left' ? 'selected' : ''}>${t('panelPositionBottomLeft')}</option>
                </select>
            </label>
            <label>${t('maxScrollTimes')}: <input type="number" id="maxScrollTimesInput" value="${config.maxScrollTimes}" min="0"></label>
            <label>${t('timeoutSeconds')}: <input type="number" id="timeoutSecondsInput" value="${config.timeoutSeconds}" min="1"></label>
            <label>${t('language')}: 
                <select id="languageSelect">
                    <option value="zh-CN" ${config.language === 'zh-CN' ? 'selected' : ''}>${t('languageZhCN')}</option>
                    <option value="en" ${config.language === 'en' ? 'selected' : ''}>${t('languageEn')}</option>
                    <option value="es" ${config.language === 'es' ? 'selected' : ''}>${t('languageEs')}</option>
                    <option value="ja" ${config.language === 'ja' ? 'selected' : ''}>${t('languageJa')}</option>
                </select>
            </label>
            <button id="saveConfigBtn">${t('saveConfig')}</button>
            <button id="closeConfigBtn" class="close-btn">${t('closeConfig')}</button>
        `;
        document.body.appendChild(configPanel);

        // Bind events
        document.getElementById('saveConfigBtn').addEventListener('click', saveConfig);
        document.getElementById('closeConfigBtn').addEventListener('click', closeConfigPanel);
    }

    // Open configuration panel
    function openConfigPanel() {
        const configPanel = document.getElementById('scrollConfigPanel');
        configPanel.style.display = 'block';
    }

    // Close configuration panel
    function closeConfigPanel() {
        const configPanel = document.getElementById('scrollConfigPanel');
        configPanel.style.display = 'none';
    }

    // Toggle scrolling
    function toggleScroll() {
        const toggleBtn = document.getElementById('toggleScrollBtn');
        if (isScrolling) {
            clearInterval(scrollTimer);
            isScrolling = false;
            atBottomSince = null;
            toggleBtn.textContent = t('startScroll');
            toggleBtn.classList.remove('stop');
        } else {
            scrollCount = 0;
            lastScrollHeight = document.documentElement.scrollHeight;
            atBottomSince = null;
            startScroll();
            isScrolling = true;
            toggleBtn.textContent = t('stopScroll');
            toggleBtn.classList.add('stop');
        }
    }

    // Start scrolling
    function startScroll() {
        if (scrollTimer) clearInterval(scrollTimer);

        scrollTimer = setInterval(() => {
            const now = Date.now();
            if (now - lastScrollTime < config.scrollInterval) return;

            lastScrollTime = now;
            window.scrollBy({ top: config.scrollStep, behavior: config.scrollBehavior });
            scrollCount++;

            // Check if max scroll times reached
            if (config.maxScrollTimes > 0 && scrollCount >= config.maxScrollTimes) {
                clearInterval(scrollTimer);
                isScrolling = false;
                atBottomSince = null;
                document.getElementById('toggleScrollBtn').textContent = t('startScroll');
                document.getElementById('toggleScrollBtn').classList.remove('stop');
                console.log('Reached max scroll times');
                return;
            }

            // Check if at page bottom
            const isAtBottom = window.innerHeight + Math.ceil(window.scrollY) >= document.documentElement.scrollHeight;
            if (isAtBottom) {
                const currentScrollHeight = document.documentElement.scrollHeight;
                if (currentScrollHeight > lastScrollHeight) {
                    // New content loaded, reset timeout
                    lastScrollHeight = currentScrollHeight;
                    atBottomSince = null;
                    console.log('New content loaded, continuing scroll');
                } else {
                    // No new content, start or continue timeout
                    if (!atBottomSince) {
                        atBottomSince = now;
                    }
                    if (now - atBottomSince >= config.timeoutSeconds * 1000) {
                        // Timeout reached, stop scrolling
                        clearInterval(scrollTimer);
                        isScrolling = false;
                        atBottomSince = null;
                        document.getElementById('toggleScrollBtn').textContent = t('startScroll');
                        document.getElementById('toggleScrollBtn').classList.remove('stop');
                        console.log('Timeout reached, no new content, stopping scroll');
                        return;
                    }
                    // Continue trying to scroll to trigger loading
                    console.log('At bottom, attempting to trigger more content');
                }
            } else {
                // Not at bottom, reset timeout
                atBottomSince = null;
            }
        }, config.scrollInterval / 2);
    }

    // Detect manual scrolling to pause auto-scroll
    let manualScrollTimeout = null;
    window.addEventListener('scroll', () => {
        if (isScrolling && Date.now() - lastScrollTime > config.scrollInterval * 2) {
            clearInterval(scrollTimer);
            clearTimeout(manualScrollTimeout);
            manualScrollTimeout = setTimeout(() => {
                if (isScrolling) startScroll();
            }, 1000);
        }
    });

    // Save configuration
    function saveConfig() {
        const scrollStep = parseInt(document.getElementById('scrollStepInput').value);
        const scrollInterval = parseInt(document.getElementById('scrollIntervalInput').value);
        const scrollBehavior = document.getElementById('scrollBehaviorSelect').value;
        const panelPosition = document.getElementById('panelPositionSelect').value;
        const maxScrollTimes = parseInt(document.getElementById('maxScrollTimesInput').value);
        const timeoutSeconds = parseInt(document.getElementById('timeoutSecondsInput').value);
        const language = document.getElementById('languageSelect').value;

        // Validate inputs
        if (isNaN(scrollStep) || scrollStep < 1) {
            alert(t('invalidScrollStep'));
            return;
        }
        if (isNaN(scrollInterval) || scrollInterval < 10) {
            alert(t('invalidScrollInterval'));
            return;
        }
        if (!['smooth', 'auto'].includes(scrollBehavior)) {
            alert(t('invalidScrollBehavior'));
            return;
        }
        if (!['top-right', 'top-left', 'bottom-right', 'bottom-left'].includes(panelPosition)) {
            alert(t('invalidPanelPosition'));
            return;
        }
        if (isNaN(maxScrollTimes) || maxScrollTimes < 0) {
            alert(t('invalidMaxScrollTimes'));
            return;
        }
        if (isNaN(timeoutSeconds) || timeoutSeconds < 1) {
            alert(t('invalidTimeoutSeconds'));
            return;
        }
        if (!['zh-CN', 'en', 'es', 'ja'].includes(language)) {
            alert('Invalid language.');
            return;
        }

        // Update config
        config.scrollStep = scrollStep;
        config.scrollInterval = scrollInterval;
        config.scrollBehavior = scrollBehavior;
        config.panelPosition = panelPosition;
        config.maxScrollTimes = maxScrollTimes;
        config.timeoutSeconds = timeoutSeconds;
        config.language = language;

        // Save to storage
        GM_setValue('scrollStep', config.scrollStep);
        GM_setValue('scrollInterval', config.scrollInterval);
        GM_setValue('scrollBehavior', config.scrollBehavior);
        GM_setValue('panelPosition', config.panelPosition);
        GM_setValue('maxScrollTimes', config.maxScrollTimes);
        GM_setValue('timeoutSeconds', config.timeoutSeconds);
        GM_setValue('language', config.language);

        // Update panel position
        const controlPanel = document.getElementById('scrollControlPanel');
        controlPanel.style.cssText = `
            position: fixed;
            ${getPanelPositionStyles(config.panelPosition)}
            background: #fff;
            border: 1px solid #ccc;
            padding: 10px;
            z-index: 10000;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            border-radius: 5px;
        `;

        // Close config panel
        closeConfigPanel();

        // Recreate panels to update language
        document.getElementById('scrollControlPanel').remove();
        document.getElementById('scrollConfigPanel').remove();
        createControlPanel();
        createConfigPanel();

        // Notify user
        alert(t('saveSuccess'));

        // Restart scrolling if active
        if (isScrolling) {
            clearInterval(scrollTimer);
            scrollCount = 0;
            lastScrollHeight = document.documentElement.scrollHeight;
            atBottomSince = null;
            startScroll();
        }
    }

    // Initialize
    createControlPanel();
    createConfigPanel();
})();