Enhanced AutoScroll v4.0 (Advanced Lazy Loading Support)

Advanced smooth auto-scroll with HUD, lazy loading detection, performance optimization, and accessibility features. Press S to toggle, [ ] speed, + - step, R reset, H hide, P position. Negative speed scrolls up.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Enhanced AutoScroll v4.0 (Advanced Lazy Loading Support)
// @namespace    https://greasyfork.org/users/1513610
// @version      4.0
// @description  Advanced smooth auto-scroll with HUD, lazy loading detection, performance optimization, and accessibility features. Press S to toggle, [ ] speed, + - step, R reset, H hide, P position. Negative speed scrolls up.
// @author       NAABO (Enhanced)
// @match        *://*/*
// @grant        none
// ==/UserScript==

/*
📌 Enhanced Features:
- Smart lazy loading detection with MutationObserver
- Performance-optimized frame rate limiting
- User activity tracking with smart pause
- Enhanced scroll progress indicator
- Accessibility improvements (reduced motion support)
- Error handling and recovery
- Debounced DOM change detection
- Memory leak prevention
- Advanced HUD with detailed status
*/

(function () {
  'use strict';

  /************* Configuration *************/
  const CONFIG = {
    STORAGE_KEY: 'enhanced_autoscroll_config_v4',
    DEFAULT_SPEED: 250,
    DEFAULT_SPEED_STEP: 50,
    MIN_SPEED_STEP: 10,
    MAX_SPEED_STEP: 100,
    HUD_POSITIONS: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
    FLASH_DURATION: 1500,
    TARGET_FPS: 60,
    MUTATION_DEBOUNCE: 150,
    USER_ACTIVITY_THRESHOLD: 2000, // 2 seconds
    IDLE_CHECK_INTERVAL: 5000, // 5 seconds
    HEIGHT_CHANGE_THRESHOLD: 100, // Minimum height change to resume
  };

  /************* State *************/
  const state = {
    // Core scrolling
    scrolling: false,
    speed: CONFIG.DEFAULT_SPEED,
    speedStep: CONFIG.DEFAULT_SPEED_STEP,

    // UI
    hud: null,
    hudPositionIndex: 0,
    hudVisible: true,

    // Animation and performance
    animationFrame: null,
    lastFrameTime: 0,
    frameInterval: 1000 / CONFIG.TARGET_FPS,

    // Lazy loading detection
    mutationObserver: null,
    lastScrollHeight: 0,
    pausedAtBottom: false,
    pausedAtTop: false,
    mutationDebounceTimeout: null,

    // User activity tracking
    lastUserActivity: Date.now(),
    userActivityListeners: [],
    idleCheckInterval: null,

    // Accessibility
    prefersReducedMotion: false,

    // Error handling
    errorCount: 0,
    maxErrors: 5,
  };

  /************* Utils *************/
  function saveConfig() {
    try {
      const data = {
        speed: state.speed,
        speedStep: state.speedStep,
        hudPositionIndex: state.hudPositionIndex,
        hudVisible: state.hudVisible,
      };
      localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(data));
    } catch (error) {
      console.warn('AutoScroll: Failed to save config', error);
    }
  }

  function loadConfig() {
    try {
      const data = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY) || '{}');
      if (data.speed != null) state.speed = data.speed;
      if (data.speedStep != null) state.speedStep = data.speedStep;
      if (data.hudPositionIndex != null) state.hudPositionIndex = data.hudPositionIndex;
      if (data.hudVisible != null) state.hudVisible = data.hudVisible;
    } catch (error) {
      console.warn('AutoScroll: Failed to load config, using defaults', error);
    }
  }

  function flashHUD(msg, type = 'info') {
    if (!state.hud) return;

    const div = document.createElement('div');
    div.textContent = msg;

    const colors = {
      info: '#3b82f6',
      success: '#22c55e',
      warning: '#f59e0b',
      error: '#ef4444',
    };

    div.style.cssText = `
      position:absolute; top:-28px; left:50%; transform:translateX(-50%);
      background:${colors[type] || colors.info}; color:#fff; padding:4px 8px;
      border-radius:4px; font-size:11px; pointer-events:none;
      white-space:nowrap; z-index:10; box-shadow:0 2px 8px rgba(0,0,0,0.3);
    `;

    state.hud.appendChild(div);
    setTimeout(() => {
      if (div.parentNode) div.remove();
    }, CONFIG.FLASH_DURATION);
  }

  function getHUDPositionClass() {
    return `hud-pos-${CONFIG.HUD_POSITIONS[state.hudPositionIndex]}`;
  }

  function calculateScrollProgress() {
    const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
    return maxScroll > 0 ? Math.round((window.scrollY / maxScroll) * 100) : 100;
  }

  function getScrollDirection() {
    return state.speed >= 0 ? '↓' : '↑';
  }

  /************* User Activity Tracking *************/
  function setupUserActivityTracking() {
    const events = ['mousedown', 'keydown', 'wheel', 'touchstart', 'mousemove'];

    const updateActivity = () => {
      state.lastUserActivity = Date.now();
    };

    events.forEach(event => {
      const listener = updateActivity;
      document.addEventListener(event, listener, { passive: true });
      state.userActivityListeners.push({ event, listener });
    });

    // Periodic idle check
    state.idleCheckInterval = setInterval(() => {
      const idleTime = Date.now() - state.lastUserActivity;
      if (idleTime > CONFIG.IDLE_CHECK_INTERVAL && state.scrolling) {
        // User has been idle, can continue normal scrolling
        updateHUD();
      }
    }, CONFIG.IDLE_CHECK_INTERVAL);
  }

  function cleanupUserActivityTracking() {
    state.userActivityListeners.forEach(({ event, listener }) => {
      document.removeEventListener(event, listener);
    });
    state.userActivityListeners = [];

    if (state.idleCheckInterval) {
      clearInterval(state.idleCheckInterval);
      state.idleCheckInterval = null;
    }
  }

  function isUserActive() {
    return Date.now() - state.lastUserActivity < CONFIG.USER_ACTIVITY_THRESHOLD;
  }

  /************* Lazy Loading Detection *************/
  function setupContentObserver() {
    if (state.mutationObserver) {
      state.mutationObserver.disconnect();
    }

    state.lastScrollHeight = document.documentElement.scrollHeight;

    state.mutationObserver = new MutationObserver((mutations) => {
      // Clear existing timeout
      if (state.mutationDebounceTimeout) {
        clearTimeout(state.mutationDebounceTimeout);
      }

      // Debounce rapid changes
      state.mutationDebounceTimeout = setTimeout(() => {
        handleContentChanges(mutations);
      }, CONFIG.MUTATION_DEBOUNCE);
    });

    // Observe with optimized settings for performance
    state.mutationObserver.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: false, // Don't observe attribute changes for better performance
      characterData: false, // Don't observe text changes
    });
  }

  function handleContentChanges(mutations) {
    try {
      const currentScrollHeight = document.documentElement.scrollHeight;
      const heightIncrease = currentScrollHeight - state.lastScrollHeight;

      // Only process significant height changes
      if (heightIncrease < CONFIG.HEIGHT_CHANGE_THRESHOLD) {
        return;
      }

      // Resume scrolling if we were paused at bottom and content was added
      if (state.pausedAtBottom && heightIncrease > 0 && state.speed > 0) {
        state.pausedAtBottom = false;
        state.scrolling = true;
        requestScroll();
        flashHUD(`📄 New content detected, resuming scroll`, 'success');
        updateHUD();
      }

      // Resume scrolling if we were paused at top and content was added above
      if (state.pausedAtTop && heightIncrease > 0 && state.speed < 0) {
        state.pausedAtTop = false;
        state.scrolling = true;
        requestScroll();
        flashHUD(`📄 Content added above, resuming scroll`, 'success');
        updateHUD();
      }

      state.lastScrollHeight = currentScrollHeight;

    } catch (error) {
      handleError('Content change detection failed', error);
    }
  }

  function cleanupContentObserver() {
    if (state.mutationObserver) {
      state.mutationObserver.disconnect();
      state.mutationObserver = null;
    }

    if (state.mutationDebounceTimeout) {
      clearTimeout(state.mutationDebounceTimeout);
      state.mutationDebounceTimeout = null;
    }
  }

  /************* Error Handling *************/
  function handleError(message, error) {
    console.error(`AutoScroll: ${message}`, error);
    state.errorCount++;

    if (state.errorCount >= state.maxErrors) {
      flashHUD(`⚠️ Too many errors, stopping`, 'error');
      state.scrolling = false;
      updateHUD();
      return;
    }

    flashHUD(`⚠️ ${message}`, 'warning');
  }

  /************* HUD *************/
  function createHUD() {
    if (state.hud) {
      state.hud.remove();
    }

    const style = document.createElement('style');
    style.id = 'enhanced-autoscroll-styles';
    style.textContent = `
      #enhanced-autoscroll-hud {
        position:fixed; z-index:999999;
        padding:10px 12px; background:#111; color:#fff;
        font-family:'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace, system-ui;
        font-size:12px; border-radius:8px;
        box-shadow:0 4px 16px rgba(0,0,0,0.7);
        border:1px solid #b91c1c; opacity:0.96;
        max-width:320px; min-width:280px;
        backdrop-filter:blur(4px);
        transition:opacity 0.2s ease;
      }
      #enhanced-autoscroll-hud.hidden { opacity:0; pointer-events:none; }
      .hud-pos-bottom-right { bottom:16px; right:16px; }
      .hud-pos-bottom-left { bottom:16px; left:16px; }
      .hud-pos-top-right { top:16px; right:16px; }
      .hud-pos-top-left { top:16px; left:16px; }
      .hud-btn {
        background:#1a1a1a; border:1px solid #b91c1c; color:#f87171;
        font-size:11px; padding:3px 7px; border-radius:4px;
        cursor:pointer; transition:all 0.15s ease;
        font-family:inherit;
      }
      .hud-btn:hover {
        background:#b91c1c; color:#fff; transform:translateY(-1px);
        box-shadow:0 2px 4px rgba(185,28,28,0.3);
      }
      .hud-btn:active { transform:translateY(0); }
      .hud-status-bar {
        display:flex; justify-content:space-between; align-items:center;
        margin-bottom:8px; padding-bottom:6px;
        border-bottom:1px solid #333;
      }
      .hud-progress-bar {
        height:3px; background:#333; border-radius:2px;
        margin:6px 0; overflow:hidden;
      }
      .hud-progress-fill {
        height:100%; background:linear-gradient(90deg, #22c55e, #16a34a);
        transition:width 0.3s ease; border-radius:2px;
      }
      .hud-info-grid {
        display:grid; grid-template-columns:1fr 1fr;
        gap:4px 8px; margin-bottom:8px; font-size:10px;
        color:#aaa;
      }
      .hud-buttons {
        display:flex; flex-wrap:wrap; gap:4px;
      }
    `;

    // Remove existing styles
    const existingStyle = document.getElementById('enhanced-autoscroll-styles');
    if (existingStyle) {
      existingStyle.remove();
    }

    document.head.appendChild(style);

    state.hud = document.createElement('div');
    state.hud.id = 'enhanced-autoscroll-hud';
    state.hud.classList.add(getHUDPositionClass());

    if (!state.hudVisible) {
      state.hud.classList.add('hidden');
    }

    state.hud.innerHTML = `
      <div class="hud-status-bar">
        <div id="hud-status" style="font-weight:bold; font-size:13px; color:#ef4444;">PAUSED</div>
        <button id="hud-close" style="background:none; border:none; color:#ef4444; font-size:16px; cursor:pointer; padding:0; line-height:1;" title="Terminate Script">&times;</button>
      </div>

      <div class="hud-progress-bar">
        <div id="hud-progress-fill" class="hud-progress-fill" style="width:0%"></div>
      </div>

      <div class="hud-info-grid">
        <div>Speed: <span id="hud-speed-value">0</span>px/s</div>
        <div>Step: <span id="hud-step-value">0</span></div>
        <div>Direction: <span id="hud-direction">-</span></div>
        <div>Progress: <span id="hud-progress-text">0%</span></div>
      </div>

      <div class="hud-buttons">
        ${makeButton("S", "toggle", "Toggle scroll")}
        ${makeButton("[", "speed-down", "Decrease speed")}
        ${makeButton("]", "speed-up", "Increase speed")}
        ${makeButton("+", "step-up", "Increase step")}
        ${makeButton("-", "step-down", "Decrease step")}
        ${makeButton("R", "reset", "Reset speed")}
        ${makeButton("P", "pos", "Change position")}
        ${makeButton("H", "hide", "Hide/Show HUD")}
      </div>
    `;

    document.body.appendChild(state.hud);

    state.hud.querySelector('#hud-close').addEventListener('click', shutdownScript);
    setupHUDButtons();
    updateHUD();
  }

  function makeButton(label, action, title) {
    return `<button class="hud-btn" data-action="${action}" title="${title}">${label}</button>`;
  }

  function setupHUDButtons() {
    state.hud.querySelectorAll('.hud-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        handleAction(btn.dataset.action);
      });
    });
  }

  function updateHUD() {
    if (!state.hud) return;

    try {
      // Status
      const status = state.hud.querySelector('#hud-status');
      const progress = calculateScrollProgress();

      let statusText = state.scrolling ? 'SCROLLING' : 'PAUSED';
      if (state.pausedAtBottom) statusText = 'AT BOTTOM';
      if (state.pausedAtTop) statusText = 'AT TOP';

      status.textContent = statusText;
      status.style.color = state.scrolling ? '#22c55e' : '#ef4444';

      // Progress bar and text
      const progressFill = state.hud.querySelector('#hud-progress-fill');
      const progressText = state.hud.querySelector('#hud-progress-text');
      if (progressFill) progressFill.style.width = `${progress}%`;
      if (progressText) progressText.textContent = `${progress}%`;

      // Info grid
      const speedValue = state.hud.querySelector('#hud-speed-value');
      const stepValue = state.hud.querySelector('#hud-step-value');
      const direction = state.hud.querySelector('#hud-direction');

      if (speedValue) speedValue.textContent = Math.abs(state.speed);
      if (stepValue) stepValue.textContent = state.speedStep;
      if (direction) direction.textContent = getScrollDirection();

    } catch (error) {
      handleError('HUD update failed', error);
    }
  }

  /************* Core Actions *************/
  function handleAction(action) {
    try {
      switch (action) {
        case 'toggle': toggleScroll(); break;
        case 'speed-down': changeSpeed(-state.speedStep); break;
        case 'speed-up': changeSpeed(state.speedStep); break;
        case 'reset': resetSpeed(); break;
        case 'hide': toggleHUD(); break;
        case 'step-up': changeStep(10); break;
        case 'step-down': changeStep(-10); break;
        case 'pos': cycleHudPosition(); break;
      }
      saveConfig();
      updateHUD();
    } catch (error) {
      handleError('Action failed', error);
    }
  }

  function toggleScroll() {
    state.scrolling = !state.scrolling;
    state.pausedAtBottom = false;
    state.pausedAtTop = false;

    if (state.scrolling) {
      requestScroll();
      flashHUD(`${getScrollDirection()} Scrolling started`, 'success');
    } else {
      if (state.animationFrame) {
        cancelAnimationFrame(state.animationFrame);
        state.animationFrame = null;
      }
      flashHUD('⏸️ Scrolling paused', 'info');
    }
  }

  function changeSpeed(delta) {
    const oldDirection = state.speed >= 0;
    state.speed += delta;
    const newDirection = state.speed >= 0;

    // Reset position flags when direction changes
    if (oldDirection !== newDirection) {
      state.pausedAtBottom = false;
      state.pausedAtTop = false;
    }

    const speedAbs = Math.abs(state.speed);
    const directionText = newDirection ? 'down' : 'up';
    flashHUD(`${getScrollDirection()} ${speedAbs}px/s (${directionText})`, 'info');
  }

  function resetSpeed() {
    state.speed = state.prefersReducedMotion
      ? Math.min(CONFIG.DEFAULT_SPEED, 150)
      : CONFIG.DEFAULT_SPEED;
    state.speedStep = CONFIG.DEFAULT_SPEED_STEP;
    state.pausedAtBottom = false;
    state.pausedAtTop = false;
    flashHUD("🔄 Speed & Step reset", 'success');
  }

  function changeStep(delta) {
    state.speedStep = Math.min(
      CONFIG.MAX_SPEED_STEP,
      Math.max(CONFIG.MIN_SPEED_STEP, state.speedStep + delta)
    );
    flashHUD(`Step: ${state.speedStep}`, 'info');
  }

  function toggleHUD() {
    if (!state.hud) return;

    state.hudVisible = !state.hudVisible;

    if (state.hudVisible) {
      state.hud.classList.remove('hidden');
      flashHUD('👁️ HUD visible', 'info');
    } else {
      state.hud.classList.add('hidden');
    }
  }

  function cycleHudPosition() {
    if (!state.hud) return;

    state.hud.classList.remove(getHUDPositionClass());
    state.hudPositionIndex = (state.hudPositionIndex + 1) % CONFIG.HUD_POSITIONS.length;
    state.hud.classList.add(getHUDPositionClass());

    const position = CONFIG.HUD_POSITIONS[state.hudPositionIndex].replace('-', ' ');
    flashHUD(`📍 ${position}`, 'info');
  }

  /************* Core Scrolling *************/
  function requestScroll() {
    if (!state.scrolling) return;

    try {
      // Frame rate limiting for better performance
      const currentTime = performance.now();
      if (currentTime - state.lastFrameTime < state.frameInterval) {
        state.animationFrame = requestAnimationFrame(requestScroll);
        return;
      }
      state.lastFrameTime = currentTime;

      // Pause during active user interaction
      if (isUserActive()) {
        state.animationFrame = requestAnimationFrame(requestScroll);
        return;
      }

      const currentScrollY = window.scrollY;
      const maxScrollY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);

      // Check boundaries and pause if reached
      if (state.speed > 0 && currentScrollY >= maxScrollY - 2) {
        // Scrolling down and reached bottom
        state.scrolling = false;
        state.pausedAtBottom = true;
        updateHUD();
        flashHUD('📍 Reached bottom, waiting for new content...', 'warning');
        return;
      }

      if (state.speed < 0 && currentScrollY <= 0) {
        // Scrolling up and reached top
        state.scrolling = false;
        state.pausedAtTop = true;
        updateHUD();
        flashHUD('📍 Reached top', 'warning');
        return;
      }

      // Calculate smooth step based on frame rate
      const step = state.speed / CONFIG.TARGET_FPS;
      window.scrollBy(0, step);

      // Continue animation
      state.animationFrame = requestAnimationFrame(requestScroll);

      // Update HUD periodically (not every frame for performance)
      if (Math.floor(currentTime / 500) !== Math.floor((currentTime - state.frameInterval) / 500)) {
        updateHUD();
      }

    } catch (error) {
      handleError('Scroll animation failed', error);
      state.scrolling = false;
      updateHUD();
    }
  }

  /************* Keyboard Handling *************/
  function keyHandler(e) {
    // Don't interfere with form inputs
    if (e.target.isContentEditable ||
        ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) {
      return;
    }

    // Don't interfere with modifier key combinations
    if (e.ctrlKey || e.altKey || e.metaKey) {
      return;
    }

    const key = e.key.toLowerCase();
    const actions = {
      's': 'toggle',
      '[': 'speed-down',
      ']': 'speed-up',
      'r': 'reset',
      'h': 'hide',
      'p': 'pos',
      '+': 'step-up',
      '=': 'step-up', // Same key as +
      '-': 'step-down',
      '_': 'step-down', // Same key as -
    };

    if (actions[key]) {
      e.preventDefault();
      handleAction(actions[key]);
    }
  }

  /************* Accessibility *************/
  function checkAccessibility() {
    // Check for reduced motion preference
    state.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    if (state.prefersReducedMotion) {
      // Reduce default speeds for accessibility
      if (Math.abs(state.speed) > 150) {
        state.speed = state.speed > 0 ? 150 : -150;
      }
      flashHUD('♿ Reduced motion mode active', 'info');
    }

    // Listen for preference changes
    window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
      state.prefersReducedMotion = e.matches;
      if (e.matches) {
        state.speed = Math.sign(state.speed) * Math.min(Math.abs(state.speed), 150);
        flashHUD('♿ Reduced motion enabled', 'info');
        updateHUD();
      }
    });
  }

  /************* Cleanup *************/
  function shutdownScript() {
    try {
      console.log(`❌ Enhanced AutoScroll v4.0 terminated on ${location.hostname}`);

      // Stop scrolling
      state.scrolling = false;
      if (state.animationFrame) {
        cancelAnimationFrame(state.animationFrame);
      }

      // Cleanup observers and listeners
      cleanupContentObserver();
      cleanupUserActivityTracking();
      document.removeEventListener('keydown', keyHandler);

      // Remove UI
      if (state.hud) {
        state.hud.remove();
        state.hud = null;
      }

      // Remove styles
      const style = document.getElementById('enhanced-autoscroll-styles');
      if (style) {
        style.remove();
      }

      flashHUD('👋 AutoScroll terminated', 'info');

    } catch (error) {
      console.error('AutoScroll: Cleanup failed', error);
    }
  }

  /************* Initialization *************/
  function init() {
    try {
      console.log(`✅ Enhanced AutoScroll v4.0 active on ${location.hostname}`);

      // Load configuration
      loadConfig();

      // Setup accessibility
      checkAccessibility();

      // Create UI
      createHUD();

      // Setup observers and trackers
      setupContentObserver();
      setupUserActivityTracking();

      // Setup keyboard handling
      document.addEventListener('keydown', keyHandler);

      // Initialize display
      updateHUD();

      // Welcome message
      setTimeout(() => {
        flashHUD('🚀 Enhanced AutoScroll v4.0 ready!', 'success');
      }, 500);

    } catch (error) {
      console.error('AutoScroll: Initialization failed', error);

      // Fallback: try basic initialization
      try {
        createHUD();
        document.addEventListener('keydown', keyHandler);
      } catch (fallbackError) {
        console.error('AutoScroll: Fallback initialization also failed', fallbackError);
      }
    }
  }

  // Handle page visibility changes
  document.addEventListener('visibilitychange', () => {
    if (document.hidden && state.scrolling) {
      // Pause when page is hidden
      state.scrolling = false;
      if (state.animationFrame) {
        cancelAnimationFrame(state.animationFrame);
        state.animationFrame = null;
      }
      updateHUD();
    }
  });

  // Initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();