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.

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

})();