Auto Scroll/Jump Back Helper (Scroll Position Saver/Tracker)

Detects sudden scroll jumps and restores your previous position automatically or on click. (+ Settings)

// ==UserScript==
// @name         Auto Scroll/Jump Back Helper (Scroll Position Saver/Tracker)
// @namespace    https://nemeth.it/
// @version      0.3
// @description  Detects sudden scroll jumps and restores your previous position automatically or on click. (+ Settings)
// @license      MIT
// @author       nemeth.it
// @match        *://*/*
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(async function() {
    'use strict';

    const defaultSettings = {
        whitelist: ['manga', 'manhua','webtoon'],
        pixelThreshold: 3000,
        trackInterval: 3000,
        maxButtons: 10,
        maxHistory: 400,
        settingsBtnAnchor: 'bottom left',
        containerAnchor: 'right center',
        settingsBtnOffsetX: 10,
        settingsBtnOffsetY: 10,
        offsetX: 20,
        offsetY: 20,
        autoJump: false,
        autoJumpDelay: 1000,
    };

    const anchorOptions = ['top right', 'top left', 'bottom right', 'bottom left', 'right center', 'left center', 'top center', 'bottom center', 'center'];
    const staticWhitelist = ['greasyfork.org'];
    const settingsKey = 'scroll_jump_saver_settings';
    let settings = sanitizeSettings(await loadSettings());
    let pendingAutoJump = null;

    const url = window.location.href.toLowerCase();
    const lowerUrl = url.toLowerCase();
    const isWhitelisted = staticWhitelist.some(entry => lowerUrl.includes(entry.toLowerCase())) || settings.whitelist.some(entry => lowerUrl.includes(entry.toLowerCase()));
    
    if (!isWhitelisted) {
        return;
    } else {
        console.log("This page is on the whitelist, scroll helper started."); 
    }


    const positionHistory = [];
    const buttonContainer = document.createElement('div');
    applyAnchor(buttonContainer, settings.containerAnchor, settings.offsetX, settings.offsetY);
    buttonContainer.style.position = 'fixed';
    buttonContainer.style.zIndex = '9999';
    buttonContainer.style.display = 'flex';
    buttonContainer.style.flexDirection = 'column-reverse';
    buttonContainer.style.gap = '8px';
    document.body.appendChild(buttonContainer);

    let lastPosition = window.scrollY;

    setInterval(() => {
        const currentPosition = window.scrollY;
        positionHistory.push(currentPosition);
        if (positionHistory.length > settings.maxHistory) positionHistory.shift();

        const diff = currentPosition - lastPosition;
        if (diff >= settings.pixelThreshold && getButtonCount() < settings.maxButtons) {
            const wrapper = createJumpButton(lastPosition);
            if (settings.autoJump) {
                if (pendingAutoJump) clearTimeout(pendingAutoJump);
                pendingAutoJump = setTimeout(() => {
                    if (document.body.contains(wrapper)) {
                        wrapper.querySelector('div').click();
                        pendingAutoJump = null;
                    }
                }, settings.autoJumpDelay);
            }
        }
        lastPosition = currentPosition;
        handleCleanupButton();
    }, settings.trackInterval);

    function sanitizeSettings(input) {
        const safe = { ...defaultSettings, ...input };
        safe.pixelThreshold = Math.max(1, safe.pixelThreshold || 5000);
        safe.trackInterval = Math.max(1, safe.trackInterval || 2000);
        safe.autoJumpDelay = Math.max(1, safe.autoJumpDelay || 1000);
        safe.maxButtons = Math.max(1, safe.maxButtons);
        safe.maxHistory = Math.max(10, safe.maxHistory);
        safe.offsetX = Math.max(0, safe.offsetX || 10);
        safe.offsetY = Math.max(0, safe.offsetY || 10);
        safe.settingsBtnOffsetX = Math.max(0, safe.settingsBtnOffsetX || 10);
        safe.settingsBtnOffsetY = Math.max(0, safe.settingsBtnOffsetY || 10);
        safe.containerAnchor = anchorOptions.includes(safe.containerAnchor) ? safe.containerAnchor : 'top right';
        safe.settingsBtnAnchor = anchorOptions.includes(safe.settingsBtnAnchor) ? safe.settingsBtnAnchor : 'top right';
        return safe;
    }

    function createJumpButton(scrollPos) {
        const wrapper = document.createElement('div');

        scrollPos = Math.round(scrollPos) === scrollPos ? scrollPos : scrollPos.toFixed(1);

        wrapper.className = 'jump-btn';
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.gap = '4px';
        wrapper.style.background = 'rgba(0,0,0,0.7)';
        wrapper.style.color = '#fff';
        wrapper.style.padding = '6px 10px';
        wrapper.style.borderRadius = '4px';
        wrapper.style.fontSize = '14px';
        wrapper.style.cursor = 'pointer';
        wrapper.style.opacity = '0';
        wrapper.style.transition = 'opacity 0.5s';

        const btn = document.createElement('div');
        btn.textContent = settings.autoJump ? `AutoJump back to ${scrollPos}px in ${(settings.autoJumpDelay/1000).toFixed(2)}sec` : `Jump back to position ${scrollPos}px`;
        btn.title = `Jump back to position ${scrollPos}px`;
        btn.style.flex = '1';
        btn.style.textAlign = 'right';
        btn.onclick = () => {
            window.scrollTo({ top: scrollPos, behavior: 'smooth' });
            wrapper.remove();
            if (pendingAutoJump && document.body.contains(wrapper)) {
                clearTimeout(pendingAutoJump);
                pendingAutoJump = null;
            }
            handleCleanupButton();
        };

        const close = document.createElement('div');
        close.textContent = '✕';
        close.style.marginLeft = '8px';
        close.style.cursor = 'pointer';
        close.onclick = (e) => {
            e.stopPropagation();
            wrapper.remove();
            if (pendingAutoJump && document.body.contains(wrapper)) {
                clearTimeout(pendingAutoJump);
                pendingAutoJump = null;
            }
            handleCleanupButton();
        };

        wrapper.appendChild(btn);
        wrapper.appendChild(close);
        buttonContainer.appendChild(wrapper);

        requestAnimationFrame(() => {
            wrapper.style.opacity = '1';
        });

        return wrapper;
    }

    function handleCleanupButton() {
        const existing = document.getElementById('mass-remove-btn');
        const btnCount = getButtonCount();
        if (btnCount > 1 && !existing) {
            const massBtn = document.createElement('div');
            massBtn.id = 'mass-remove-btn';
            massBtn.textContent = `✕ Remove all buttons`;
            massBtn.style.background = 'rgba(255,0,0,0.7)';
            massBtn.style.color = '#fff';
            massBtn.style.padding = '6px 10px';
            massBtn.style.borderRadius = '4px';
            massBtn.style.fontSize = '12px';
            massBtn.style.cursor = 'pointer';
            massBtn.onclick = () => {
                const all = buttonContainer.querySelectorAll('.jump-btn');
                all.forEach(btn => btn.remove());
                massBtn.remove();
            };
            buttonContainer.insertBefore(massBtn, buttonContainer.firstChild);
        } else if (btnCount <= 1 && existing) {
            existing.remove();
        }
    }

    function getButtonCount() {
        return buttonContainer.querySelectorAll('.jump-btn').length;
    }

    function applyAnchor(element, anchor, offsetX, offsetY) {
        const positions = {
            'top right': { top: offsetY + 'px', right: offsetX + 'px' },
            'top left': { top: offsetY + 'px', left: offsetX + 'px' },
            'bottom right': { bottom: offsetY + 'px', right: offsetX + 'px' },
            'bottom left': { bottom: offsetY + 'px', left: offsetX + 'px' },
            'right center': { top: '50%', right: offsetX + 'px', transform: 'translateY(-50%)' },
            'left center': { top: '50%', left: offsetX + 'px', transform: 'translateY(-50%)' },
            'top center': { top: offsetY + 'px', left: '50%', transform: 'translateX(-50%)' },
            'bottom center': { bottom: offsetY + 'px', left: '50%', transform: 'translateX(-50%)' },
            'center': { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }
        };
        const chosen = positions[anchor] || positions['top right'];
        for (const [key, value] of Object.entries(chosen)) {
            element.style[key] = value;
        }
    }

    // SETTINGS UI FOLGT IN TEIL 2...


    // Gemeinsamer Wrapper für Settings-Button und AutoJump-Toggle
    const settingsBtnWrapper = document.createElement('div');
    settingsBtnWrapper.style.position = 'fixed';
    settingsBtnWrapper.style.zIndex = '9999';
    settingsBtnWrapper.style.display = 'flex';
    settingsBtnWrapper.style.alignItems = 'center';
    settingsBtnWrapper.style.gap = '6px';
    document.body.appendChild(settingsBtnWrapper);

    // AutoJump Toggle-Button [▶️]/[⏸]
    const autoJumpBtn = document.createElement('div');
    autoJumpBtn.textContent = settings.autoJump ? '[↪️]' : '[⏹️]';
    autoJumpBtn.style.cursor = 'pointer';
    autoJumpBtn.style.fontSize = '20px';
    autoJumpBtn.style.userSelect = 'none';
    autoJumpBtn.onclick = () => {
        settings.autoJump = !settings.autoJump;
        autoJumpBtn.textContent = settings.autoJump ? '[↪️]' : '[⏹️]';
        GM_setValue(settingsKey, JSON.stringify(settings));
    };
    settingsBtnWrapper.appendChild(autoJumpBtn);

    // ⚙️ Settings-Button
    const settingsBtn = document.createElement('div');
    settingsBtn.textContent = '⚙️';
    settingsBtn.style.cursor = 'pointer';
    settingsBtn.style.fontSize = '20px';
    settingsBtn.style.userSelect = 'none';
    settingsBtnWrapper.appendChild(settingsBtn);

    // Ausrichtung beider Buttons entsprechend den gespeicherten Einstellungen
    applyAnchor(settingsBtnWrapper, settings.settingsBtnAnchor, settings.settingsBtnOffsetX, settings.settingsBtnOffsetY);

    const overlay = document.createElement('div');
    overlay.style.position = 'fixed';
    overlay.style.top = 0;
    overlay.style.left = 0;
    overlay.style.width = '100vw';
    overlay.style.height = '100vh';
    overlay.style.background = 'rgba(0,0,0,0.3)';
    overlay.style.display = 'none';
    overlay.style.zIndex = '9998';
    document.body.appendChild(overlay);

    const settingsMenu = document.createElement('div');
    settingsMenu.style.position = 'fixed';
    settingsMenu.style.background = 'rgba(255,255,255,0.95)';
    settingsMenu.style.color = '#000';
    settingsMenu.style.padding = '10px';
    settingsMenu.style.borderRadius = '6px';
    settingsMenu.style.display = 'none';
    settingsMenu.style.zIndex = '9999';
    settingsMenu.style.minWidth = '340px';
    settingsMenu.style.boxShadow = '0 0 20px rgba(0,0,0,0.4)';
    settingsMenu.style.position = 'fixed';
    settingsMenu.style.maxWidth = '400px';
    settingsMenu.style.fontFamily = 'Arial, sans-serif';
    document.body.appendChild(settingsMenu);

    settingsBtn.onclick = () => {
        renderSettingsMenu();
        settingsMenu.style.display = 'block';
        overlay.style.display = 'block';

        // Kurz sichtbar machen, damit wir Größe berechnen können
        settingsMenu.style.visibility = 'hidden';
        settingsMenu.style.left = '0px';
        settingsMenu.style.top = '0px';

        // Nach einem Frame Größe auslesen
        requestAnimationFrame(() => {
            const btnRect = settingsBtn.getBoundingClientRect();
            const menuRect = settingsMenu.getBoundingClientRect();
            const padding = 10;

            let top = btnRect.bottom + padding;
            let left = btnRect.left;

            if (left + menuRect.width > window.innerWidth) {
                left = window.innerWidth - menuRect.width - padding;
            }
            if (top + menuRect.height > window.innerHeight) {
                top = btnRect.top - menuRect.height - padding;
                if (top < 0) top = padding;
            }

            settingsMenu.style.left = `${left}px`;
            settingsMenu.style.top = `${top}px`;
            settingsMenu.style.visibility = 'visible';
        });
    };

    overlay.onclick = () => {
        settingsMenu.style.display = 'none';
        overlay.style.display = 'none';
    };

    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            settingsMenu.style.display = 'none';
            overlay.style.display = 'none';
        }
    });

    function renderSettingsMenu() {
        settingsMenu.innerHTML = `
            <div style="text-align: right; margin-bottom: 5px;">
                <span id="close-settings" style="cursor: pointer; font-weight: bold;">✕</span>
            </div>
            <table style="width: 100%; border-spacing: 6px;">
                <tr><th colspan="2" style="text-align:left;">General Settings</th></tr>
                <tr><td title="Coma seperated strings to match in url to activate this script">Whitelist:</td><td><input type="text" id="st_whitelist" value="${settings.whitelist.join(',')}" /></td></tr>
                <tr><td title="How many pixels define a big jump">Pixel Threshold (px):</td><td><input type="number" id="st_pixel" value="${settings.pixelThreshold}" /></td></tr>
                <tr><td title="Interval in ms to record the scroll position">Track Interval (ms):</td><td><input type="number" id="st_interval" value="${settings.trackInterval}" /></td></tr>
                <tr><td title="Max jump allowed on screen">Max Buttons:</td><td><input type="number" id="st_maxbtns" value="${settings.maxButtons}" /></td></tr>
                <tr><td title="AutoJump back after big jump">AutoJump:</td><td><input type="checkbox" id="st_autojump" ${settings.autoJump ? 'checked' : ''} /></td></tr>
                <tr><td title="AutoJump back delayed by ms">AutoJumpDelay (ms):</td><td><input type="number" id="st_autoumpdelaybtn" value="${settings.autoJumpDelay}" /></td></tr>
                <tr><th colspan="2" style="text-align:left;">Jump Buttons Position</th></tr>
                <tr><td>Anchor:</td><td><select id="st_anchor">${anchorOptions.map(p => `<option ${settings.containerAnchor === p ? 'selected' : ''}>${p}</option>`).join('')}</select></td></tr>
                <tr><td>- X Offset:</td><td><input type="number" id="st_offsetx" value="${settings.offsetX}" /></td></tr>
                <tr><td>- Y Offset:</td><td><input type="number" id="st_offsety" value="${settings.offsetY}" /></td></tr>
                <tr><th colspan="2" style="text-align:left;">⚙️ Button Position</th></tr>
                <tr><td>Anchor:</td><td><select id="st_btnpos">${anchorOptions.map(p => `<option ${settings.settingsBtnAnchor === p ? 'selected' : ''}>${p}</option>`).join('')}</select></td></tr>
                <tr><td>- X Offset:</td><td><input type="number" id="st_btnx" value="${settings.settingsBtnOffsetX}" /></td></tr>
                <tr><td>- Y Offset:</td><td><input type="number" id="st_btny" value="${settings.settingsBtnOffsetY}" /></td></tr>
            </table>
            <p style="font-size: 11px; color: #555; margin-top: 5px;">Tip: Hover on labels for tooltips.</p>
            <div style="display: flex; justify-content: space-between; margin-top: 15px;">
                <button id="st_reset" style="background: #e74c3c; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer;">Reset</button>
                <button id="st_save" style="background: #2ecc71; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer;">Save</button>
            </div>
        `;

        document.getElementById('close-settings').onclick = () => {
            settingsMenu.style.display = 'none';
            overlay.style.display = 'none';
        };

        document.getElementById('st_save').onclick = () => {
            settings.whitelist = document.getElementById('st_whitelist').value.split(',').map(s => s.trim());
            settings.pixelThreshold = parseInt(document.getElementById('st_pixel').value);
            settings.trackInterval = parseInt(document.getElementById('st_interval').value);
            settings.maxButtons = parseInt(document.getElementById('st_maxbtns').value);
            settings.autoJump = document.getElementById('st_autojump').checked;
            settings.autoJumpDelay = parseInt(document.getElementById('st_autoumpdelaybtn').value);
            settings.containerAnchor = document.getElementById('st_anchor').value;
            settings.offsetX = parseInt(document.getElementById('st_offsetx').value);
            settings.offsetY = parseInt(document.getElementById('st_offsety').value);
            settings.settingsBtnAnchor = document.getElementById('st_btnpos').value;
            settings.settingsBtnOffsetX = parseInt(document.getElementById('st_btnx').value);
            settings.settingsBtnOffsetY = parseInt(document.getElementById('st_btny').value);
            saveSettings();
            if (confirm('Settings saved. Do you want to reload the page now?')) {
                location.reload();
            }
        };

        document.getElementById('st_reset').onclick = () => {
            if (confirm('Reset all settings?')) {
                GM_deleteValue(settingsKey);//localStorage.removeItem(settingsKey);
                location.reload();
            }
        };
    }

    function saveSettings() {
        GM_setValue(settingsKey, JSON.stringify(settings));//localStorage.setItem(settingsKey, JSON.stringify(settings));
    }

    async function loadSettings() {
        const data = await GM_getValue(settingsKey); // alte localStorage-Zeile ersetzt
        return data ? JSON.parse(data) : { ...defaultSettings };
    }

})();