// ==UserScript==
// @name Enhanced Smooth AutoScroll
// @version 2.1
// @description Press 's' to toggle smooth auto-scroll. '[', ']' adjust speed. 'h' hides HUD. 'r' reset speed. '+/-' adjust step. 🚫 Block sites via HUD.
// @author NAABO
// @match *://*/*
// @grant none
// @run-at document-idle
// @namespace https://gf.qytechs.cn/users/1513610
// ==/UserScript==
(function () {
'use strict';
/************* Configuration & Constants *************/
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,
EASING_FACTOR: 0.2,
};
/************* State Management *************/
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 Management *************/
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 (error) {
console.warn('Enhanced AutoScroll: Failed to load blocklist', error);
return [];
}
}
function saveBlocklist(list) {
try {
localStorage.setItem(CONFIG.BLOCKLIST_KEY, JSON.stringify(list));
} catch (error) {
console.warn('Enhanced AutoScroll: Failed to save blocklist', error);
}
}
/************* Utility Functions *************/
function isTyping(event) {
try {
const tgt = event.target;
if (!tgt) return false;
const tag = (tgt.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea') return true;
if (tgt.isContentEditable) return true;
return false;
} catch {
return false;
}
}
function prefersReducedMotion() {
try {
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
} catch {
return false;
}
}
function debounce(func, delay) {
return function (...args) {
if (state.keyDebounceTimeout) clearTimeout(state.keyDebounceTimeout);
state.keyDebounceTimeout = setTimeout(() => func.apply(this, args), delay);
};
}
function safeRequestAnimationFrame(callback) {
try {
return requestAnimationFrame(callback);
} catch {
return setTimeout(callback, 16);
}
}
function safeCancelAnimationFrame(id) {
try {
cancelAnimationFrame(id);
} catch {
clearTimeout(id);
}
}
/************* Scrolling Engine *************/
function step(now) {
try {
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 = safeRequestAnimationFrame(step);
} catch (error) {
console.error('Enhanced AutoScroll: Error in animation step', error);
stopLoop();
}
}
function startLoop() {
if (!state.rafId) {
state.lastFrameTime = null;
state.rafId = safeRequestAnimationFrame(step);
}
}
function stopLoop() {
if (state.rafId) {
safeCancelAnimationFrame(state.rafId);
state.rafId = null;
}
state.lastFrameTime = null;
}
/************* Controls *************/
function toggleEnabled(forceState) {
if (typeof forceState === 'boolean') state.enabled = forceState;
else state.enabled = !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 ? '↓' : '↑';
const speedText = `Speed: ${Math.abs(state.speed)} px/s ${direction}`;
updateHUD();
flashHUD(speedText);
saveConfig();
if (state.enabled && Math.sign(oldSpeed) !== Math.sign(state.speed)) {
flashHUD(`Direction changed! ${speedText}`);
}
}
function resetSpeed() {
state.speed = CONFIG.DEFAULT_SPEED;
updateHUD();
flashHUD(`Speed reset to ${CONFIG.DEFAULT_SPEED} px/s`);
saveConfig();
}
/************* HUD *************/
function getHUDPositionStyles() {
const positions = {
'bottom-right': 'right:12px; bottom:12px;',
'bottom-left': 'left:12px; bottom:12px;',
'top-right': 'right:12px; top:12px;',
'top-left': 'left:12px; top:12px;',
};
return positions[state.hudPosition] || positions['bottom-right'];
}
function createHUD() {
if (state.hud) state.hud.remove();
state.hud = document.createElement('div');
state.hud.setAttribute('id', 'enhanced-autoscroll-hud');
const positionStyles = getHUDPositionStyles();
state.hud.style.cssText = `
position:fixed; ${positionStyles} z-index:999999;
padding:10px 14px 12px 14px; background:rgba(0,0,0,0.75); color:#fff;
font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial;
font-size:13px; border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,0.6);
backdrop-filter:blur(8px); max-width:340px; line-height:1.3;
pointer-events:auto; opacity:0.95; transition:all 0.2s ease;
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; line-height:1; opacity:0.7;
border-radius:4px; transition:opacity 0.2s ease;
" 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;">
Click buttons above or use keyboard shortcuts
</div>
`;
document.body.appendChild(state.hud);
state.hud.querySelector('#hud-close').addEventListener('click', shutdownScript);
setupHUDButtons();
}
function setupHUDButtons() {
const buttons = state.hud.querySelectorAll('.hud-btn');
buttons.forEach(button => {
const action = button.getAttribute('data-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) {
const direction = state.speed >= 0 ? '↓' : '↑';
statusEl.textContent = `SCROLLING ${direction}`;
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');
const originalText = statusEl.textContent;
const originalColor = statusEl.style.color;
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;
if (state.flashTimeout) clearTimeout(state.flashTimeout);
if (state.keyDebounceTimeout) 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;
const key = e.key.toLowerCase();
switch (key) {
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);
/************* Initialization *************/
function init() {
const blocklist = loadBlocklist();
const domain = window.location.hostname;
if (blocklist.includes(domain)) {
console.log(`Enhanced AutoScroll: Disabled on blocked site ${domain}`);
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();
}
})();