scroll-button

scroll-button (Draggable, Auto-Fade, Bright-on-Stop)

Устаревшая версия за 17.12.2025. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         scroll-button
// @namespace    https://github.com/livinginpurple
// @version      20251217.09
// @description  scroll-button (Draggable, Auto-Fade, Bright-on-Stop)
// @license      WTFPL
// @author       livinginpurple
// @include      *
// @run-at       document-end
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    const init = () => {
        const btnId = 'gamma-scroll-btn';
        if (document.getElementById(btnId)) return;

        // SVG 圖示
        const icons = {
            up: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="white" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 12a.5.5 0 0 0 .5-.5V5.707l2.146 2.147a.5.5 0 0 0 .708-.708l-3-3a.5.5 0 0 0-.708 0l-3 3a.5.5 0 1 0 .708.708L7.5 5.707V11.5a.5.5 0 0 0 .5.5z"/></svg>`,
            down: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="white" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 4a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 10.293V4.5A.5.5 0 0 1 8 4z"/></svg>`
        };

        const btn = document.createElement('button');
        btn.id = btnId;
        btn.type = 'button';
        btn.setAttribute('aria-label', 'Scroll navigation');

        // 定義樣式
        Object.assign(btn.style, {
            position: 'fixed',
            right: '20px',
            bottom: '20px',
            width: '40px',
            height: '40px',
            borderRadius: '50%',
            backgroundColor: '#0d6efd',
            border: 'none',
            boxShadow: '0 0.2rem 0.5rem rgba(0,0,0,0.3)',
            zIndex: '10000',
            cursor: 'grab',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            transition: 'opacity 0.5s ease-in-out, transform 0.1s, background-color 0.2s',
            touchAction: 'none',
            padding: '0',
            opacity: '0.3' // 預設 0.3
        });

        // 互動視覺效果 (Hover/Touch)
        btn.onmouseenter = () => { btn.style.transform = 'scale(1.1)'; manualWakeUp(); };
        btn.onmouseleave = () => { btn.style.transform = 'scale(1)'; };
        btn.ontouchstart = () => { manualWakeUp(); };

        document.body.appendChild(btn);

        // --- 狀態管理 ---
        const State = { TOP: 'top', SCROLLED: 'scrolled' };
        
        // Timer 變數
        let scrollStopTimer = null; // 用來偵測是否停止滑動
        let fadeTimer = null;       // 用來計算 3 秒後變暗

        // 更新圖示 (不涉及透明度)
        const updateIconState = () => {
            const scrollTop = window.scrollY || document.documentElement.scrollTop;
            const isAtTop = scrollTop < 50;
            const nextState = isAtTop ? State.TOP : State.SCROLLED;
            
            if (btn.dataset.state === nextState) return;

            if (isAtTop) {
                btn.innerHTML = icons.down;
                btn.dataset.state = State.TOP;
            } else {
                btn.innerHTML = icons.up;
                btn.dataset.state = State.SCROLLED;
            }
        };

        // --- 核心邏輯:滑動停止偵測 ---
        const handleScroll = () => {
            // 1. 滑動中:確保是 0.3
            // 為了避免頻繁操作 DOM,只在需要時設定
            if (btn.style.opacity !== '0.3') {
                btn.style.opacity = '0.3';
            }

            updateIconState();

            // 2. 清除之前的計時器 (因為還在滑)
            if (scrollStopTimer) clearTimeout(scrollStopTimer);
            if (fadeTimer) clearTimeout(fadeTimer);

            // 3. 設定新的偵測計時器
            // 如果 150ms 內沒有新的 scroll 事件,就視為「停止滑動」
            scrollStopTimer = setTimeout(() => {
                // --- 滑動停止時執行 ---
                btn.style.opacity = '1'; // 亮起
                
                // 設定 3 秒後變暗
                fadeTimer = setTimeout(() => {
                    btn.style.opacity = '0.3';
                }, 3000);

            }, 150);
        };

        // 手動操作 (點擊/拖曳) 的喚醒邏輯
        const manualWakeUp = () => {
            btn.style.opacity = '1';
            if (scrollStopTimer) clearTimeout(scrollStopTimer);
            if (fadeTimer) clearTimeout(fadeTimer);
            
            // 操作後也是等 3 秒變暗
            fadeTimer = setTimeout(() => {
                btn.style.opacity = '0.3';
            }, 3000);
        };

        // --- 拖曳邏輯 ---
        let isPressed = false;
        let isDragging = false;
        let startX, startY, dragOffsetX, dragOffsetY;

        const onDragStart = (e) => {
            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            isPressed = true;
            isDragging = false;
            startX = clientX;
            startY = clientY;

            const rect = btn.getBoundingClientRect();
            dragOffsetX = clientX - rect.left;
            dragOffsetY = clientY - rect.top;

            btn.style.backgroundColor = '#0b5ed7';
            manualWakeUp(); // 拖曳開始,喚醒
        };

        const onDragMove = (e) => {
            if (!isPressed) return;

            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            const moveX = clientX - startX;
            const moveY = clientY - startY;
            const distance = Math.sqrt(moveX * moveX + moveY * moveY);

            if (!isDragging && distance < 10) return;

            if (!isDragging) {
                isDragging = true;
                btn.style.cursor = 'grabbing';
                const rect = btn.getBoundingClientRect();
                btn.style.left = rect.left + 'px';
                btn.style.top = rect.top + 'px';
                btn.style.right = 'auto';
                btn.style.bottom = 'auto';
            }

            let newX = clientX - dragOffsetX;
            let newY = clientY - dragOffsetY;
            const maxX = window.innerWidth - 40;
            const maxY = window.innerHeight - 40;

            btn.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
            btn.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
            
            manualWakeUp(); // 拖曳中保持喚醒
        };

        const onDragEnd = () => {
            isPressed = false;
            btn.style.cursor = 'grab';
            btn.style.backgroundColor = '#0d6efd';
        };

        btn.addEventListener('mousedown', onDragStart);
        window.addEventListener('mousemove', onDragMove);
        window.addEventListener('mouseup', onDragEnd);

        btn.addEventListener('touchstart', onDragStart, { passive: false });
        window.addEventListener('touchmove', onDragMove, { passive: false });
        window.addEventListener('touchend', onDragEnd);

        // --- 點擊邏輯 ---
        btn.addEventListener('click', (e) => {
            if (isDragging) {
                e.preventDefault();
                e.stopImmediatePropagation();
                isDragging = false;
                return;
            }
            e.preventDefault();
            manualWakeUp();
            
            if (btn.dataset.state === State.TOP) {
                window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
            } else {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            }
        });

        // 綁定 Scroll 事件
        window.addEventListener('scroll', handleScroll, { passive: true });
        
        // 初始化
        updateIconState();
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();