scroll-button

scroll-button (Draggable & Auto-Fade)

À partir de 2025-12-17. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         scroll-button
// @namespace    https://github.com/livinginpurple
// @version      20251217.06
// @description  scroll-button (Draggable & Auto-Fade)
// @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;

        // --- 1. 設定與記憶 ---
        const STORAGE_KEY = 'gamma-scroll-pos';
        let savedPos = null;
        try {
            savedPos = JSON.parse(localStorage.getItem(STORAGE_KEY));
        } catch (e) {}

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

        // --- 2. 樣式定義 ---
        // 判斷初始位置:如果有存檔用存檔的位置,否則預設右下角
        const initialStyles = savedPos 
            ? { left: savedPos.x + 'px', top: savedPos.y + 'px' } 
            : { right: '20px', bottom: '20px' };

        Object.assign(btn.style, {
            position: 'fixed',
            ...initialStyles,
            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',
            // 注意:不對 left/top 設 transition,避免拖曳時遲滯
            transition: 'opacity 0.5s ease-in-out, transform 0.1s, background-color 0.2s',
            touchAction: 'none', // 關鍵:防止拖曳時觸發網頁捲動
            padding: '0',
            opacity: '0.3'
        });

        // 互動視覺效果
        btn.onmouseenter = () => { btn.style.transform = 'scale(1.1)'; };
        btn.onmouseleave = () => { btn.style.transform = 'scale(1)'; };
        
        document.body.appendChild(btn);

        // --- 3. 狀態與透明度控制 ---
        const State = { TOP: 'top', SCROLLED: 'scrolled' };
        let idleTimer = null;

        const activeHandler = () => {
            btn.style.opacity = '1';
            if (idleTimer) clearTimeout(idleTimer);
            idleTimer = setTimeout(() => {
                btn.style.opacity = '0.3';
            }, 2000);
        };

        const updateButtonState = () => {
            activeHandler();
            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;
            }
        };

        // --- 4. 拖曳邏輯核心 (Drag Core) ---
        let isDragging = false;
        let hasMoved = false;
        let startX, startY, initialRect;

        // 處理開始 (Mouse & Touch)
        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;

            isDragging = true;
            hasMoved = false;
            startX = clientX;
            startY = clientY;
            initialRect = btn.getBoundingClientRect(); // 取得當前絕對位置

            // 確保切換為絕對定位模式 (避免從 right/bottom 切換時跳動)
            btn.style.right = 'auto';
            btn.style.bottom = 'auto';
            btn.style.left = initialRect.left + 'px';
            btn.style.top = initialRect.top + 'px';
            
            btn.style.cursor = 'grabbing';
            btn.style.backgroundColor = '#0b5ed7'; // 按下變深色
            activeHandler();
        };

        // 處理移動
        const onDragMove = (e) => {
            if (!isDragging) 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 deltaX = clientX - startX;
            const deltaY = clientY - startY;

            // 判定是否真的在拖曳 (超過 5px)
            if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
                hasMoved = true;
            }

            // 計算新位置並限制在視窗內
            let newX = initialRect.left + deltaX;
            let newY = initialRect.top + deltaY;
            
            const maxX = window.innerWidth - 40; // 40 is btn width
            const maxY = window.innerHeight - 40;

            newX = Math.max(0, Math.min(newX, maxX));
            newY = Math.max(0, Math.min(newY, maxY));

            btn.style.left = newX + 'px';
            btn.style.top = newY + 'px';
        };

        // 處理結束
        const onDragEnd = () => {
            if (!isDragging) return;
            isDragging = false;
            btn.style.cursor = 'grab';
            btn.style.backgroundColor = '#0d6efd';

            if (hasMoved) {
                // 儲存位置
                localStorage.setItem(STORAGE_KEY, JSON.stringify({
                    x: parseFloat(btn.style.left),
                    y: parseFloat(btn.style.top)
                }));
            }
            activeHandler();
        };

        // 綁定事件 (支援 PC 與 Mobile)
        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);

        // --- 5. 點擊捲動邏輯 ---
        btn.addEventListener('click', (e) => {
            // 如果剛才發生了拖曳位移,則不執行捲動
            if (hasMoved) {
                e.preventDefault();
                e.stopImmediatePropagation();
                return;
            }
            
            e.preventDefault();
            activeHandler();
            
            if (btn.dataset.state === State.TOP) {
                window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
            } else {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            }
        });

        window.addEventListener('scroll', updateButtonState, { passive: true });
        
        // 初始化狀態
        updateButtonState();
    };

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