scroll up and down buttons

Adds a clean set of up/down/settings buttons. Settings panel includes a hide timer and a transparency toggle.

目前為 2025-09-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         scroll up and down buttons
// @namespace    http://tampermonkey.net/
// @version      1.5.1
// @description  Adds a clean set of up/down/settings buttons. Settings panel includes a hide timer and a transparency toggle.
// @author       You
// @match        *://*/*
// @grant        none
// @license      Apache 2.0
// ==/UserScript==

(function () {
    'use strict';

    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;
    }

    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 });
    });

    const style = document.createElement('style');
    style.textContent = `
        /* Main container for the vertical column */
        .tm-scroll-container {
            position: fixed;
            bottom: 10px;
            right: 10px;
            display: flex;
            flex-direction: column;
            gap: 6px;
            z-index: 9999999;
            pointer-events: auto;
            align-items: center;
        }

        /* Unified button styles */
        .tm-scroll-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-settings-btn { background-color: #000; border-radius: 50%; font-size: 20px; }

        /* Transparency state for buttons */
        .tm-scroll-btn.transparent, .tm-settings-btn.transparent { opacity: 0.1; }

        /* Settings Panel */
        .tm-settings-panel {
            display: none;
            position: fixed;
            bottom: 110px;
            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 hr { border: none; border-top: 1px solid #555; margin: 15px 0; }
        .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 .setting-row { display: flex; justify-content: space-between; align-items: center; }
        .tm-settings-panel-content label { font-size: 14px; }
        .tm-settings-panel-content input[type="text"] {
            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;
        }

        /* Toggle Switch CSS */
        .toggle-switch { position: relative; display: inline-block; width: 50px; height: 26px; }
        .toggle-switch input { opacity: 0; width: 0; height: 0; }
        .slider {
            position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
            background-color: #666; transition: .3s; border-radius: 26px;
        }
        .slider:before {
            position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px;
            background-color: white; transition: .3s; border-radius: 50%;
        }
        input:checked + .slider { background-color: #1a73e8; }
        input:checked + .slider:before { transform: translateX(24px); }
    `;
    document.head.appendChild(style);

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

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

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

    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);

    container.appendChild(settingsBtn);
    container.appendChild(upBtn);
    container.appendChild(downBtn);
    document.body.appendChild(container);

    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">
                <div class="setting-row">
                     <label for="tm-transparency-toggle">Make buttons transparent</label>
                     <label class="toggle-switch">
                        <input type="checkbox" id="tm-transparency-toggle">
                        <span class="slider"></span>
                    </label>
                </div>
            </div>
            <hr>
            <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);

    const closeSettingsBtn = settingsPanel.querySelector('.tm-settings-panel-close');
    const hideTimerInput = document.getElementById('tm-hide-timer-input');
    const startHideTimerBtn = document.getElementById('tm-start-hide-timer');
    const transparencyToggle = document.getElementById('tm-transparency-toggle'); 

    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 || parseInt(match[2], 10) >= 60 || parseInt(match[3], 10) >= 60) {
            hideTimerInput.classList.add('error');
            return;
        }

        const [ , hours, minutes, seconds] = match.map(Number);
        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 = 'flex'; 
                }
            }, totalMilliseconds);
        }
    });

    const STORAGE_KEY = 'tm_scroll_transparent_v1';
    const buttonsToMakeTransparent = [upBtn, downBtn, settingsBtn];

    function applyTransparencyState(state) {
        transparencyToggle.checked = state;
        buttonsToMakeTransparent.forEach(btn => {
            state ? btn.classList.add('transparent') : btn.classList.remove('transparent');
        });

        try {
            localStorage.setItem(STORAGE_KEY, state ? '1' : '0');
        } catch (e) { /* ignore storage errors */ }
    }

    transparencyToggle.addEventListener('change', () => {
        applyTransparencyState(transparencyToggle.checked);
    });

    try {
        const savedState = localStorage.getItem(STORAGE_KEY);
        applyTransparencyState(savedState === '1');
    } catch (e) { /* ignore storage errors */ }

    document.addEventListener('fullscreenchange', () => {
        if (container.style.display !== 'none') {
            container.style.display = document.fullscreenElement ? 'none' : 'flex'; 
        }
    });

    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 });

})();