// ==UserScript==
// @name Enhanced Smooth AutoScroll
// @version 2.0
// @description Press 's' to toggle smooth auto-scroll. '[', ']' adjust speed (negative = scroll up). 'h' hides/shows HUD. Enhanced with error handling, accessibility, and configuration options.
// @author NAABO
// @match *://*/*
// @grant none
// @run-at document-idle
// @namespace https://gf.qytechs.cn/users/1513610
// ==/UserScript==
/**
* Enhanced Auto-Scroll UserScript
* Hotkeys:
* S → toggle auto-scroll
* [/] → adjust speed (unlimited, can be negative)
* H → hide/show HUD
* R → reset to default speed
* +/- → adjust speed step
*/
(function () {
'use strict';
/************* Configuration & Constants *************/
const CONFIG = {
STORAGE_KEY: 'enhanced_autoscroll_config',
DEFAULT_SPEED: 100, // px per second (downward)
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, // ms
FLASH_DURATION: 1500, // ms
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,
};
/************* Configuration Management *************/
/**
* Load configuration from localStorage with error handling
*/
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, using defaults', error);
}
}
/**
* Save configuration to localStorage with error handling
*/
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);
}
}
/************* Utility Functions *************/
/**
* Check if user is currently typing in an input field
*/
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 (error) {
console.warn('Enhanced AutoScroll: Error checking typing state', error);
return false;
}
}
/**
* Check if user prefers reduced motion
*/
function prefersReducedMotion() {
try {
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
} catch (error) {
return false;
}
}
/**
* Debounced function execution
*/
function debounce(func, delay) {
return function (...args) {
if (state.keyDebounceTimeout) {
clearTimeout(state.keyDebounceTimeout);
}
state.keyDebounceTimeout = setTimeout(() => func.apply(this, args), delay);
};
}
/**
* Safe requestAnimationFrame with fallback
*/
function safeRequestAnimationFrame(callback) {
try {
return requestAnimationFrame(callback);
} catch (error) {
console.warn('Enhanced AutoScroll: requestAnimationFrame not available, using setTimeout');
return setTimeout(callback, 16); // ~60fps fallback
}
}
/**
* Safe cancelAnimationFrame with fallback
*/
function safeCancelAnimationFrame(id) {
try {
cancelAnimationFrame(id);
} catch (error) {
clearTimeout(id);
}
}
/************* Scrolling Engine *************/
/**
* Main animation loop with error handling and accessibility checks
*/
function step(now) {
try {
if (state.terminated) return;
// Check for reduced motion preference
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); // Cap delta time
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;
// Check boundaries
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 {
// Smooth scrolling with bounds checking
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();
}
}
/**
* Start the animation loop with error handling
*/
function startLoop() {
try {
if (!state.rafId) {
state.lastFrameTime = null;
state.rafId = safeRequestAnimationFrame(step);
}
} catch (error) {
console.error('Enhanced AutoScroll: Failed to start animation loop', error);
}
}
/**
* Stop the animation loop and clean up
*/
function stopLoop() {
try {
if (state.rafId) {
safeCancelAnimationFrame(state.rafId);
state.rafId = null;
}
state.lastFrameTime = null;
} catch (error) {
console.error('Enhanced AutoScroll: Failed to stop animation loop', error);
}
}
/************* Controls *************/
/**
* Toggle scroll state with optional force parameter
*/
function toggleEnabled(forceState) {
try {
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();
} catch (error) {
console.error('Enhanced AutoScroll: Failed to toggle enabled state', error);
}
}
/**
* Adjust scroll speed with bounds checking
*/
function adjustSpeed(delta) {
try {
const oldSpeed = state.speed;
state.speed += delta;
// Visual feedback with speed change
const direction = state.speed >= 0 ? '↓' : '↑';
const speedText = `Speed: ${Math.abs(state.speed)} px/s ${direction}`;
updateHUD();
flashHUD(speedText);
saveConfig();
// If we were scrolling, show immediate feedback
if (state.enabled && Math.sign(oldSpeed) !== Math.sign(state.speed)) {
flashHUD(`Direction changed! ${speedText}`);
}
} catch (error) {
console.error('Enhanced AutoScroll: Failed to adjust speed', error);
}
}
/**
* Reset speed to default
*/
function resetSpeed() {
try {
state.speed = CONFIG.DEFAULT_SPEED;
updateHUD();
flashHUD(`Speed reset to ${CONFIG.DEFAULT_SPEED} px/s`);
saveConfig();
} catch (error) {
console.error('Enhanced AutoScroll: Failed to reset speed', error);
}
}
/************* HUD Management *************/
/**
* Get HUD position styles based on current position setting
*/
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'];
}
/**
* Create the HUD with enhanced styling and error handling
*/
function createHUD() {
try {
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" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Toggle scrolling">S</button>
<button class="hud-btn" data-action="speed-down" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Decrease speed">[</button>
<button class="hud-btn" data-action="speed-up" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Increase speed">]</button>
<button class="hud-btn" data-action="reset" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Reset speed">R</button>
<button class="hud-btn" data-action="hide-hud" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Hide HUD">H</button>
<button class="hud-btn" data-action="step-up" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Increase speed step">+</button>
<button class="hud-btn" data-action="step-down" style="
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
color:#fff; padding:4px 8px; border-radius:4px; font-size:11px;
cursor:pointer; transition:all 0.2s ease;
" title="Decrease speed step">-</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);
// Enhanced close button interaction
const closeBtn = state.hud.querySelector('#hud-close');
closeBtn.addEventListener('click', shutdownScript);
closeBtn.addEventListener('mouseenter', () => closeBtn.style.opacity = '1');
closeBtn.addEventListener('mouseleave', () => closeBtn.style.opacity = '0.7');
// Set up HUD button interactions
setupHUDButtons();
} catch (error) {
console.error('Enhanced AutoScroll: Failed to create HUD', error);
}
}
/**
* Set up HUD button event listeners with direct function calls
*/
function setupHUDButtons() {
try {
const buttons = state.hud.querySelectorAll('.hud-btn');
buttons.forEach(button => {
const action = button.getAttribute('data-action');
// Add hover effects
button.addEventListener('mouseenter', () => {
button.style.background = 'rgba(255,255,255,0.2)';
button.style.borderColor = 'rgba(255,255,255,0.4)';
});
button.addEventListener('mouseleave', () => {
button.style.background = 'rgba(255,255,255,0.1)';
button.style.borderColor = 'rgba(255,255,255,0.2)';
});
// Add click handlers - direct function calls, no key simulation
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 += 1;
saveConfig();
updateHUD();
flashHUD(`Speed step: ${state.speedStep}`);
}
break;
case 'step-down':
if (state.speedStep > CONFIG.MIN_SPEED_STEP) {
state.speedStep -= 1;
saveConfig();
updateHUD();
flashHUD(`Speed step: ${state.speedStep}`);
}
break;
}
});
});
} catch (error) {
console.error('Enhanced AutoScroll: Failed to setup HUD buttons', error);
}
}
/**
* Update HUD content with current state
*/
function updateHUD() {
try {
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');
// Update status
if (state.enabled) {
const direction = state.speed >= 0 ? '↓' : '↑';
statusEl.textContent = `SCROLLING ${direction}`;
statusEl.style.color = '#4ade80';
} else {
statusEl.textContent = 'PAUSED';
statusEl.style.color = '#ef4444';
}
// Update speed info
speedEl.textContent = `Speed: ${Math.abs(state.speed)} px/s (Step: ${state.speedStep})`;
// Update config info
const configParts = [];
if (state.respectsReducedMotion && prefersReducedMotion()) configParts.push('Reduced motion');
configEl.textContent = configParts.join(' • ');
// Update button states
updateButtonStates();
} catch (error) {
console.error('Enhanced AutoScroll: Failed to update HUD', error);
}
}
/**
* Update button visual states based on current settings
*/
function updateButtonStates() {
try {
if (!state.hud) return;
const buttons = state.hud.querySelectorAll('.hud-btn');
buttons.forEach(button => {
const action = button.getAttribute('data-action');
// Highlight active states
if (action === 'toggle' && state.enabled) {
button.style.background = 'rgba(76, 175, 80, 0.3)';
button.style.borderColor = 'rgba(76, 175, 80, 0.6)';
} else if (action === 'toggle' && !state.enabled) {
button.style.background = 'rgba(244, 67, 54, 0.3)';
button.style.borderColor = 'rgba(244, 67, 54, 0.6)';
}
});
} catch (error) {
console.error('Enhanced AutoScroll: Failed to update button states', error);
}
}
/**
* Flash HUD with temporary message
*/
function flashHUD(text, duration = CONFIG.FLASH_DURATION) {
try {
if (!state.hud) createHUD();
if (!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(() => {
try {
updateHUD();
state.flashTimeout = null;
} catch (error) {
console.error('Enhanced AutoScroll: Error in flash timeout', error);
}
}, duration);
} catch (error) {
console.error('Enhanced AutoScroll: Failed to flash HUD', error);
}
}
/************* Enhanced Shutdown *************/
/**
* Comprehensive script shutdown with cleanup
*/
function shutdownScript() {
try {
if (state.terminated) return;
state.terminated = true;
// Stop all animations and timers
stopLoop();
state.enabled = false;
// Clear all timeouts
if (state.flashTimeout) {
clearTimeout(state.flashTimeout);
state.flashTimeout = null;
}
if (state.keyDebounceTimeout) {
clearTimeout(state.keyDebounceTimeout);
state.keyDebounceTimeout = null;
}
// Remove all event listeners
document.removeEventListener('keydown', onKeyDown);
window.removeEventListener('beforeunload', shutdownScript);
// Remove HUD with fade effect
if (state.hud) {
state.hud.style.transition = 'opacity 0.3s ease';
state.hud.style.opacity = '0';
setTimeout(() => {
if (state.hud && state.hud.parentNode) {
state.hud.remove();
}
state.hud = null;
}, 300);
}
console.log('Enhanced AutoScroll: Script terminated successfully');
} catch (error) {
console.error('Enhanced AutoScroll: Error during shutdown', error);
}
}
/************* Enhanced Key Handling *************/
/**
* Debounced key handler with comprehensive hotkey support
*/
const onKeyDown = debounce(function (e) {
try {
if (isTyping(e) || state.terminated) return;
const key = e.key.toLowerCase();
let handled = false;
switch (key) {
case 's':
e.preventDefault();
toggleEnabled();
handled = true;
break;
case '[':
e.preventDefault();
adjustSpeed(-state.speedStep);
handled = true;
break;
case ']':
e.preventDefault();
adjustSpeed(state.speedStep);
handled = true;
break;
case 'h':
e.preventDefault();
state.hudVisible = !state.hudVisible;
updateHUD();
flashHUD(`HUD ${state.hudVisible ? 'shown' : 'hidden'}`);
handled = true;
break;
case 'r':
e.preventDefault();
resetSpeed();
handled = true;
break;
case '+':
case '=':
e.preventDefault();
if (state.speedStep < CONFIG.MAX_SPEED_STEP) {
state.speedStep += 1;
saveConfig();
updateHUD();
flashHUD(`Speed step: ${state.speedStep}`);
}
handled = true;
break;
case '-':
case '_':
e.preventDefault();
if (state.speedStep > CONFIG.MIN_SPEED_STEP) {
state.speedStep -= 1;
saveConfig();
updateHUD();
flashHUD(`Speed step: ${state.speedStep}`);
}
handled = true;
break;
}
if (handled) {
e.stopPropagation();
}
} catch (error) {
console.error('Enhanced AutoScroll: Error in key handler', error);
}
}, CONFIG.DEBOUNCE_DELAY);
/************* Enhanced Initialization *************/
/**
* Initialize the enhanced autoscroll with comprehensive setup
*/
function init() {
try {
console.log('Enhanced AutoScroll: Initializing v2.0');
// Load saved configuration
loadConfig();
// Create and update HUD
createHUD();
updateHUD();
// Set up event listeners with error handling
document.addEventListener('keydown', onKeyDown, { passive: false });
window.addEventListener('beforeunload', shutdownScript, { passive: true });
// Show initialization message
flashHUD('Enhanced AutoScroll ready! Press S to start', 2000);
console.log('Enhanced AutoScroll: Initialization complete');
} catch (error) {
console.error('Enhanced AutoScroll: Failed to initialize', error);
// Try to show error in a basic way
try {
alert('Enhanced AutoScroll failed to initialize. Check console for details.');
} catch (alertError) {
// If even alert fails, just log
console.error('Enhanced AutoScroll: Critical initialization failure');
}
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();