Enhanced Smooth AutoScroll

Smooth automatic scrolling with HUD controls. Toggle with 'S'. Adjust speed with '[' and ']'. Change step with '+/-'. Reset with 'R'. Hide HUD with 'H'. Block sites directly from HUD.

当前为 2025-09-12 提交的版本,查看 最新版本

// ==UserScript==
// @name         Enhanced Smooth AutoScroll
// @namespace    https://gf.qytechs.cn/users/1513610
// @version      2.2
// @description  Smooth automatic scrolling with HUD controls. Toggle with 'S'. Adjust speed with '[' and ']'. Change step with '+/-'. Reset with 'R'. Hide HUD with 'H'. Block sites directly from HUD.
// @author       NAABO
// @license      MIT
// @match        *://*/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

/*
📌 Features:
- Press 'S' to start/pause smooth scrolling.
- '[' / ']' decrease/increase scroll speed.
- '+' / '-' adjust speed step size.
- 'R' resets to default speed.
- 'H' shows/hides the HUD.
- HUD includes buttons for controls + 🚫 blocklist per site.
- Respects "prefers-reduced-motion".
*/

(function () {
  'use strict';

  /************* Configuration *************/
  const CONFIG = {
    STORAGE_KEY: 'enhanced_autoscroll_config',
    BLOCKLIST_KEY: 'enhanced_autoscroll_blocklist',
    DEFAULT_SPEED: 100,
    DEFAULT_SPEED_STEP: 10,
    MIN_SPEED_STEP: 1,
    MAX_SPEED_STEP: 50,
    HUD_POSITIONS: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
    DEBOUNCE_DELAY: 100,
    FLASH_DURATION: 1500,
  };

  /************* State *************/
  let state = {
    enabled: false,
    speed: CONFIG.DEFAULT_SPEED,
    speedStep: CONFIG.DEFAULT_SPEED_STEP,
    lastFrameTime: null,
    rafId: null,
    hud: null,
    hudVisible: true,
    hudPosition: 'bottom-right',
    terminated: false,
    flashTimeout: null,
    keyDebounceTimeout: null,
    respectsReducedMotion: true,
  };

  /************* Config Persistence *************/
  function loadConfig() {
    try {
      const saved = localStorage.getItem(CONFIG.STORAGE_KEY);
      if (saved) {
        const config = JSON.parse(saved);
        state.speed = Number(config.speed) || CONFIG.DEFAULT_SPEED;
        state.speedStep = Number(config.speedStep) || CONFIG.DEFAULT_SPEED_STEP;
        state.hudPosition = config.hudPosition || 'bottom-right';
        state.respectsReducedMotion = Boolean(config.respectsReducedMotion);
      }
    } catch (error) {
      console.warn('Enhanced AutoScroll: Failed to load config', error);
    }
  }

  function saveConfig() {
    try {
      const config = {
        speed: state.speed,
        speedStep: state.speedStep,
        hudPosition: state.hudPosition,
        respectsReducedMotion: state.respectsReducedMotion,
      };
      localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(config));
    } catch (error) {
      console.warn('Enhanced AutoScroll: Failed to save config', error);
    }
  }

  function loadBlocklist() {
    try {
      const saved = localStorage.getItem(CONFIG.BLOCKLIST_KEY);
      return saved ? JSON.parse(saved) : [];
    } catch {
      return [];
    }
  }

  function saveBlocklist(list) {
    try {
      localStorage.setItem(CONFIG.BLOCKLIST_KEY, JSON.stringify(list));
    } catch {}
  }

  /************* Helpers *************/
  function isTyping(event) {
    const tgt = event.target;
    if (!tgt) return false;
    const tag = (tgt.tagName || '').toLowerCase();
    return tag === 'input' || tag === 'textarea' || tgt.isContentEditable;
  }

  function prefersReducedMotion() {
    return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
  }

  function debounce(func, delay) {
    return function (...args) {
      if (state.keyDebounceTimeout) clearTimeout(state.keyDebounceTimeout);
      state.keyDebounceTimeout = setTimeout(() => func.apply(this, args), delay);
    };
  }

  function safeRAF(callback) {
    try { return requestAnimationFrame(callback); }
    catch { return setTimeout(callback, 16); }
  }

  function safeCancelRAF(id) {
    try { cancelAnimationFrame(id); }
    catch { clearTimeout(id); }
  }

  /************* Scrolling Engine *************/
  function step(now) {
    if (state.terminated) return;

    if (state.respectsReducedMotion && prefersReducedMotion()) {
      if (state.enabled) {
        toggleEnabled(false);
        flashHUD('Paused: Reduced motion preferred');
      }
      return;
    }

    if (!state.lastFrameTime) state.lastFrameTime = now;
    const dt = Math.min((now - state.lastFrameTime) / 1000, 0.1);
    state.lastFrameTime = now;

    if (state.enabled) {
      const delta = state.speed * dt;
      const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
      const currentY = window.scrollY || window.pageYOffset || 0;

      if (state.speed > 0 && currentY >= Math.floor(maxScroll)) {
        toggleEnabled(false);
        flashHUD('End of page reached');
      } else if (state.speed < 0 && currentY <= 0) {
        toggleEnabled(false);
        flashHUD('Top of page reached');
      } else {
        const newY = Math.max(0, Math.min(maxScroll, currentY + delta));
        window.scrollTo({ top: newY, behavior: 'instant' });
      }
    }

    if (!state.terminated) state.rafId = safeRAF(step);
  }

  function startLoop() {
    if (!state.rafId) {
      state.lastFrameTime = null;
      state.rafId = safeRAF(step);
    }
  }

  function stopLoop() {
    if (state.rafId) {
      safeCancelRAF(state.rafId);
      state.rafId = null;
    }
    state.lastFrameTime = null;
  }

  /************* Controls *************/
  function toggleEnabled(force) {
    state.enabled = typeof force === 'boolean' ? force : !state.enabled;
    updateHUD();

    if (state.enabled) {
      startLoop();
      flashHUD(`Scrolling ${state.speed >= 0 ? 'down' : 'up'} at ${Math.abs(state.speed)} px/s`);
    } else {
      stopLoop();
      flashHUD('Scrolling paused');
    }
    saveConfig();
  }

  function adjustSpeed(delta) {
    const oldSpeed = state.speed;
    state.speed += delta;
    const direction = state.speed >= 0 ? '↓' : '↑';
    updateHUD();
    flashHUD(`Speed: ${Math.abs(state.speed)} px/s ${direction}`);
    saveConfig();
    if (state.enabled && Math.sign(oldSpeed) !== Math.sign(state.speed)) {
      flashHUD(`Direction changed! Speed: ${Math.abs(state.speed)} px/s ${direction}`);
    }
  }

  function resetSpeed() {
    state.speed = CONFIG.DEFAULT_SPEED;
    updateHUD();
    flashHUD(`Speed reset to ${CONFIG.DEFAULT_SPEED} px/s`);
    saveConfig();
  }

  /************* HUD *************/
  function getHUDPositionStyles() {
    return {
      'bottom-right': 'right:12px; bottom:12px;',
      'bottom-left': 'left:12px; bottom:12px;',
      'top-right': 'right:12px; top:12px;',
      'top-left': 'left:12px; top:12px;',
    }[state.hudPosition] || 'right:12px; bottom:12px;';
  }

  function createHUD() {
    if (state.hud) state.hud.remove();
    state.hud = document.createElement('div');
    state.hud.id = 'enhanced-autoscroll-hud';
    state.hud.style.cssText = `
      position:fixed; ${getHUDPositionStyles()} z-index:999999;
      padding:10px 14px; background:rgba(0,0,0,0.75); color:#fff;
      font-family:system-ui, sans-serif;
      font-size:13px; border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,0.6);
      backdrop-filter:blur(8px); max-width:340px;
      pointer-events:auto; opacity:0.95; border:1px solid rgba(255,255,255,0.1);
    `;

    state.hud.innerHTML = `
      <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
        <div id="hud-status" style="font-weight:600; font-size:14px;">PAUSED</div>
        <button id="hud-close" style="
          background:none; border:none; color:#fff; font-size:16px;
          cursor:pointer; padding:2px 6px; opacity:0.7;
        " title="Close Enhanced AutoScroll">&times;</button>
      </div>
      <div id="hud-speed" style="margin-bottom:8px; font-size:12px; opacity:0.9;"></div>
      <div id="hud-config" style="margin-bottom:8px; font-size:11px; opacity:0.8;"></div>
      <div id="hud-buttons" style="margin-bottom:8px; display:flex; gap:4px; flex-wrap:wrap;">
        <button class="hud-btn" data-action="toggle">S</button>
        <button class="hud-btn" data-action="speed-down">[</button>
        <button class="hud-btn" data-action="speed-up">]</button>
        <button class="hud-btn" data-action="reset">R</button>
        <button class="hud-btn" data-action="hide-hud">H</button>
        <button class="hud-btn" data-action="step-up">+</button>
        <button class="hud-btn" data-action="step-down">-</button>
        <button class="hud-btn" data-action="block-site" title="Block this site">🚫</button>
      </div>
      <div style="font-size:9px; opacity:0.7; line-height:1.3;">
        Use keyboard shortcuts or click buttons
      </div>
    `;
    document.body.appendChild(state.hud);

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

  function setupHUDButtons() {
    state.hud.querySelectorAll('.hud-btn').forEach(button => {
      const action = button.dataset.action;
      button.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        switch (action) {
          case 'toggle': toggleEnabled(); break;
          case 'speed-down': adjustSpeed(-state.speedStep); break;
          case 'speed-up': adjustSpeed(state.speedStep); break;
          case 'reset': resetSpeed(); break;
          case 'hide-hud': state.hudVisible = false; updateHUD(); flashHUD('HUD hidden - Press H to show', 3000); break;
          case 'step-up': if (state.speedStep < CONFIG.MAX_SPEED_STEP) { state.speedStep++; saveConfig(); updateHUD(); flashHUD(`Speed step: ${state.speedStep}`); } break;
          case 'step-down': if (state.speedStep > CONFIG.MIN_SPEED_STEP) { state.speedStep--; saveConfig(); updateHUD(); flashHUD(`Speed step: ${state.speedStep}`); } break;
          case 'block-site':
            const domain = window.location.hostname;
            if (confirm(`Block Enhanced AutoScroll on ${domain}?`)) {
              const list = loadBlocklist();
              if (!list.includes(domain)) {
                list.push(domain);
                saveBlocklist(list);
              }
              flashHUD(`Blocked on ${domain}`);
              shutdownScript();
            }
            break;
        }
      });
    });
  }

  function updateHUD() {
    if (!state.hud) createHUD();
    if (!state.hudVisible) { state.hud.style.display = 'none'; return; }
    state.hud.style.display = 'block';

    const statusEl = state.hud.querySelector('#hud-status');
    const speedEl = state.hud.querySelector('#hud-speed');
    const configEl = state.hud.querySelector('#hud-config');

    if (state.enabled) {
      statusEl.textContent = `SCROLLING ${state.speed >= 0 ? '↓' : '↑'}`;
      statusEl.style.color = '#4ade80';
    } else {
      statusEl.textContent = 'PAUSED';
      statusEl.style.color = '#ef4444';
    }
    speedEl.textContent = `Speed: ${Math.abs(state.speed)} px/s (Step: ${state.speedStep})`;
    configEl.textContent = (state.respectsReducedMotion && prefersReducedMotion()) ? 'Reduced motion' : '';
  }

  function flashHUD(text, duration = CONFIG.FLASH_DURATION) {
    if (!state.hud || !state.hudVisible) return;
    const statusEl = state.hud.querySelector('#hud-status');
    statusEl.textContent = text;
    statusEl.style.color = '#60a5fa';
    if (state.flashTimeout) clearTimeout(state.flashTimeout);
    state.flashTimeout = setTimeout(() => updateHUD(), duration);
  }

  /************* Shutdown *************/
  function shutdownScript() {
    if (state.terminated) return;
    state.terminated = true;
    stopLoop();
    state.enabled = false;
    clearTimeout(state.flashTimeout);
    clearTimeout(state.keyDebounceTimeout);
    document.removeEventListener('keydown', onKeyDown);
    window.removeEventListener('beforeunload', shutdownScript);
    if (state.hud) state.hud.remove();
    state.hud = null;
    console.log('Enhanced AutoScroll: Script terminated');
  }

  /************* Key Handling *************/
  const onKeyDown = debounce(function (e) {
    if (isTyping(e) || state.terminated) return;
    switch (e.key.toLowerCase()) {
      case 's': e.preventDefault(); toggleEnabled(); break;
      case '[': e.preventDefault(); adjustSpeed(-state.speedStep); break;
      case ']': e.preventDefault(); adjustSpeed(state.speedStep); break;
      case 'h': e.preventDefault(); state.hudVisible = !state.hudVisible; updateHUD(); flashHUD(`HUD ${state.hudVisible ? 'shown' : 'hidden'}`); break;
      case 'r': e.preventDefault(); resetSpeed(); break;
      case '+': case '=': e.preventDefault(); if (state.speedStep < CONFIG.MAX_SPEED_STEP) { state.speedStep++; saveConfig(); updateHUD(); flashHUD(`Speed step: ${state.speedStep}`); } break;
      case '-': case '_': e.preventDefault(); if (state.speedStep > CONFIG.MIN_SPEED_STEP) { state.speedStep--; saveConfig(); updateHUD(); flashHUD(`Speed step: ${state.speedStep}`); } break;
    }
  }, CONFIG.DEBOUNCE_DELAY);

  /************* Init *************/
  function init() {
    const blocklist = loadBlocklist();
    if (blocklist.includes(window.location.hostname)) {
      console.log(`Enhanced AutoScroll: Disabled on ${window.location.hostname}`);
      return;
    }
    loadConfig();
    createHUD();
    updateHUD();
    document.addEventListener('keydown', onKeyDown, { passive: false });
    window.addEventListener('beforeunload', shutdownScript, { passive: true });
    flashHUD('Enhanced AutoScroll ready! Press S to start', 2000);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址