Greasy Fork镜像 is available in English.

scroll up and down buttons with settings

Adds up/down buttons, transparency toggle, and a settings panel with a hide timer.

Verze ze dne 22. 09. 2025. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         scroll up and down buttons with settings
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @description  Adds up/down buttons, transparency toggle, and a settings panel with a hide timer.
// @author       You
// @match        *://*/*
// @grant        none
// @license      Apache 2.0
// ==/UserScript==

(function () {
    'use strict';

    // --- Initial checks ---
    if (window.top !== window.self) return;
    if (document.body && document.body.children.length === 1) {
        const onlyChild = document.body.children[0];
        const tag = onlyChild.tagName.toLowerCase();
        if (['img', 'video', 'audio', 'embed', 'object'].includes(tag)) return;
    }

    // --- Smooth Scroll Logic (Unchanged) ---
    let scrollAnimationId = null;

    function smoothScrollTo(targetY) {
        if (scrollAnimationId) cancelAnimationFrame(scrollAnimationId);
        const startY = window.scrollY;
        const distance = targetY - startY;
        if (distance === 0) return;
        const duration = 1000;
        const startTime = performance.now();

        function step(currentTime) {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            const ease = progress < 0.5 ? 2 * progress * progress : -1 + (4 - 2 * progress) * progress;
            window.scrollTo(0, startY + distance * ease);
            if (progress < 1) {
                scrollAnimationId = requestAnimationFrame(step);
            } else {
                scrollAnimationId = null;
            }
        }
        scrollAnimationId = requestAnimationFrame(step);
    }

    function stopScroll() {
        if (scrollAnimationId) {
            cancelAnimationFrame(scrollAnimationId);
            scrollAnimationId = null;
        }
    }

    ['mousedown', 'wheel', 'touchstart', 'keydown'].forEach(evt => {
        window.addEventListener(evt, stopScroll, { passive: true });
    });

    // --- Styles (Updated for new layout and settings panel) ---
    const style = document.createElement('style');
    style.textContent = `
        /* Main container for the 2x2 grid */
        .tm-scroll-container {
            position: fixed;
            bottom: 10px;
            right: 10px;
            display: grid;
            grid-template-columns: auto auto;
            gap: 6px;
            z-index: 9999999;
            pointer-events: auto;
            align-items: center;
            justify-items: center;
        }

        /* Unified button styles for a clean grid */
        .tm-scroll-btn, .tm-toggle-btn, .tm-settings-btn {
            width: 40px;
            height: 40px;
            padding: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            border: none;
            cursor: pointer;
            box-shadow: 0 2px 6px rgba(0,0,0,0.3);
            transition: background-color 0.2s, opacity 0.2s;
            color: white;
            font-size: 16px;
        }

        /* Specific styles for each button type */
        .tm-scroll-btn { background-color: #333; border-radius: 6px; }
        .tm-toggle-btn { background-color: #555; border-radius: 50%; }
        .tm-settings-btn { background-color: #000; border-radius: 50%; font-size: 20px; }

        /* Transparency and active states */
        .tm-scroll-btn.transparent { opacity: 0.1; }
        .tm-toggle-btn.active { background-color: #1a73e8; }

        /* NEW Settings Panel */
        .tm-settings-panel {
            display: none; /* Hidden by default */
            position: fixed;
            bottom: 110px; /* Positioned above the buttons */
            right: 10px;
            width: 250px;
            background-color: #3a3a3a;
            color: #f0f0f0;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.4);
            z-index: 10000000;
            border: 1px solid #555;
        }
        .tm-settings-panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            font-size: 16px;
            font-weight: bold;
        }
        .tm-settings-panel-close {
            background: none; border: none; color: #f0f0f0; font-size: 24px;
            cursor: pointer; line-height: 1; padding: 0 5px;
        }
        .tm-settings-panel-content .setting { display: flex; flex-direction: column; gap: 8px; }
        .tm-settings-panel-content label { font-size: 14px; }
        .tm-settings-panel-content input {
            padding: 8px; border-radius: 4px; border: 1px solid #777;
            background-color: #2c2c2c; color: white; font-family: monospace;
        }
        .tm-settings-panel-content input.error { border-color: #e57373; }
        .tm-settings-panel-content button {
            margin-top: 5px; padding: 8px 12px; border-radius: 4px; border: none;
            cursor: pointer; background-color: #1a73e8; color: white; font-weight: bold;
        }
    `;
    document.head.appendChild(style);

    // --- Main Container ---
    const container = document.createElement('div');
    container.className = 'tm-scroll-container';

    // --- Create All Buttons ---
    const toggleBtn = document.createElement('button');
    toggleBtn.className = 'tm-toggle-btn';
    toggleBtn.title = 'Toggle button transparency';
    toggleBtn.setAttribute('aria-pressed', 'false');
    toggleBtn.textContent = '◐';

    const upBtn = document.createElement('button');
    upBtn.className = 'tm-scroll-btn';
    upBtn.textContent = '▲';
    upBtn.title = 'Scroll to top';
    upBtn.onclick = () => smoothScrollTo(0);

    const settingsBtn = document.createElement('button');
    settingsBtn.className = 'tm-settings-btn';
    settingsBtn.title = 'Open Settings';
    settingsBtn.innerHTML = '⚙️'; // Gear icon for settings

    const downBtn = document.createElement('button');
    downBtn.className = 'tm-scroll-btn';
    downBtn.textContent = '▼';
    downBtn.title = 'Scroll to bottom';
    downBtn.onclick = () => smoothScrollTo(document.documentElement.scrollHeight || document.body.scrollHeight);

    // Append buttons to container in grid order (top-left, top-right, bottom-left, bottom-right)
    container.appendChild(toggleBtn);
    container.appendChild(upBtn);
    container.appendChild(settingsBtn);
    container.appendChild(downBtn);
    document.body.appendChild(container);

    // --- NEW Settings Panel HTML ---
    const settingsPanel = document.createElement('div');
    settingsPanel.className = 'tm-settings-panel';
    settingsPanel.innerHTML = `
        <div class="tm-settings-panel-header">
            <span>Settings</span>
            <button class="tm-settings-panel-close" title="Close">&times;</button>
        </div>
        <div class="tm-settings-panel-content">
            <div class="setting">
                <label for="tm-hide-timer-input">Hide buttons temporarily</label>
                <input type="text" id="tm-hide-timer-input" placeholder="HH-MM-SS">
                <button id="tm-start-hide-timer">Start Timer</button>
            </div>
        </div>
    `;
    document.body.appendChild(settingsPanel);

    // --- NEW Settings Panel Logic ---
    const hideTimerInput = document.getElementById('tm-hide-timer-input');
    const startHideTimerBtn = document.getElementById('tm-start-hide-timer');
    const closeSettingsBtn = settingsPanel.querySelector('.tm-settings-panel-close');

    settingsBtn.addEventListener('click', () => {
        settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block';
    });

    closeSettingsBtn.addEventListener('click', () => {
        settingsPanel.style.display = 'none';
    });

    startHideTimerBtn.addEventListener('click', () => {
        const timeValue = hideTimerInput.value;
        const timePattern = /^(\d+)-(\d{1,2})-(\d{1,2})$/;
        const match = timeValue.match(timePattern);

        hideTimerInput.classList.remove('error');

        if (!match) {
            hideTimerInput.classList.add('error');
            return;
        }

        const hours = parseInt(match[1], 10);
        const minutes = parseInt(match[2], 10);
        const seconds = parseInt(match[3], 10);

        if (minutes >= 60 || seconds >= 60) {
            hideTimerInput.classList.add('error');
            return;
        }

        const totalMilliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000;

        if (totalMilliseconds > 0) {
            container.style.display = 'none';
            settingsPanel.style.display = 'none';
            hideTimerInput.value = '';

            setTimeout(() => {
                if (!document.fullscreenElement) {
                   container.style.display = 'grid';
                }
            }, totalMilliseconds);
        }
    });

    // --- Transparency Logic (Original behavior on scroll buttons only) ---
    const STORAGE_KEY = 'tm_scroll_transparent_v1';
    const buttonsToMakeTransparent = [upBtn, downBtn];

    function applyTransparencyState(state) {
        if (state) {
            buttonsToMakeTransparent.forEach(btn => btn.classList.add('transparent'));
            toggleBtn.classList.add('active');
            toggleBtn.setAttribute('aria-pressed', 'true');
            toggleBtn.textContent = '◑';
        } else {
            buttonsToMakeTransparent.forEach(btn => btn.classList.remove('transparent'));
            toggleBtn.classList.remove('active');
            toggleBtn.setAttribute('aria-pressed', 'false');
            toggleBtn.textContent = '◐';
        }
        try {
            localStorage.setItem(STORAGE_KEY, state ? '1' : '0');
        } catch (e) { /* ignore storage errors */ }
    }

    let saved = null;
    try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) { saved = null; }
    applyTransparencyState(saved === '1');

    toggleBtn.addEventListener('click', (e) => {
        const isCurrentlyTransparent = upBtn.classList.contains('transparent');
        applyTransparencyState(!isCurrentlyTransparent);
        e.stopPropagation();
    });

    // --- Fullscreen and Observer Logic (Updated to use 'grid') ---
    document.addEventListener('fullscreenchange', () => {
        if (container.style.display !== 'none') {
            container.style.display = document.fullscreenElement ? 'none' : 'grid';
        }
    });

    const observer = new MutationObserver(() => {
        if (!document.body || !document.body.contains(container)) {
            if (document.body) {
                document.body.appendChild(container);
                document.body.appendChild(settingsPanel);
            }
        }
    });
    observer.observe(document.documentElement || document, { childList: true, subtree: true });

})();
长期地址
遇到问题?请前往 GitHub 提 Issues。