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