Progress Bar and Quick Up and Down Buttons

[en] A modern scroll progress bar at the bottom of the screen and smart scroll-to-top/bottom buttons with improved dark mode support (SVG, discreet, SPA & mobile friendly).

// ==UserScript==
// @name         Progress Bar and Quick Up and Down Buttons
// @name:pt-BR    BF - Barra de progressão e Botões de Subida e Decida Rápido
// @namespace    https://github.com/BrunoFortunatto
// @version      1.1
// @description [en] A modern scroll progress bar at the bottom of the screen and smart scroll-to-top/bottom buttons with improved dark mode support (SVG, discreet, SPA & mobile friendly).
// @description:pt-BR Adiciona uma barra de progresso de rolagem moderna na parte inferior da tela e botões de subir/descer inteligentes com suporte a modo escuro aprimorado (SVG, discretos, compatíveis com SPA e mobile).
// @author       Bruno Fortunato
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // VERIFICAÇÃO PARA EVITAR IFRAMES
    if (window.self !== window.top) {
        // Se este script estiver rodando dentro de um iframe, ele para aqui.
        return;
    }

    const INACTIVITY_TIMEOUT = 2000; // Tempo em milissegundos (2 segundos) para esconder os botões
    const RIGHT_EDGE_THRESHOLD_PX = 100; // Distância da borda direita para ativar os botões no PC
    let inactivityTimer;
    let buttonContainer;
    let progressBar;

    // --- Funções Auxiliares para Controle de Tema ---

    function applyTheme(isDarkMode) {
        // Cores para os botões
        const lightButtonBg = 'rgba(0, 123, 255, 0.5)'; // Azul claro padrão
        const darkButtonBg = 'rgba(50, 50, 70, 0.7)';  // Cinza escuro para o modo escuro
        const lightButtonHoverBg = 'rgba(0, 123, 255, 0.9)'; // Azul mais forte no hover
        const darkButtonHoverBg = 'rgba(80, 80, 100, 0.9)'; // Cinza mais forte no hover
        const buttonShadow = '0 3px 6px rgba(0,0,0,0.4)'; // Sombra padrão para ambos (ajusta a opacidade para ser visível)
        const darkButtonShadow = '0 3px 10px rgba(0,0,0,0.6)'; // Sombra mais intensa no modo escuro para contraste

        // Cores para a barra de progresso
        const lightProgressBarBg = 'linear-gradient(to right, #007bff, #00c7ff, #007bff)'; // Gradiente azul padrão
        // Gradiente para modo escuro: Mantenho tons escuros no preenchimento mas adiciono um brilho mais evidente
        const darkProgressBarBg = 'linear-gradient(to right, #3498db, #4a69bd, #3498db)'; // Azul mais perceptível no escuro
        const lightProgressBarShadow = '0 -2px 10px rgba(0, 123, 255, 0.7)'; // Sombra azul brilhante padrão
        // Nova sombra para modo escuro: mais intensa e com brilho para "luz"
        const darkProgressBarShadow = '0 -2px 12px rgba(173, 216, 230, 0.8), 0 -0.5px 5px rgba(255, 255, 255, 0.3)'; // Brilho azul claro/branco

        const textColor = 'white'; // Cor do texto/ícones permanece branco

        if (buttonContainer) {
            buttonContainer.querySelectorAll('button').forEach(button => {
                button.style.backgroundColor = isDarkMode ? darkButtonBg : lightButtonBg;
                button.style.boxShadow = isDarkMode ? darkButtonShadow : buttonShadow; // Aplica a sombra específica do tema
                button.onmouseover = () => Object.assign(button.style, { backgroundColor: isDarkMode ? darkButtonHoverBg : lightButtonHoverBg, transform: 'scale(1.05)' });
                button.onmouseout = () => Object.assign(button.style, { backgroundColor: isDarkMode ? darkButtonBg : lightButtonBg, transform: 'scale(1)' });
                button.style.color = textColor;
            });
        }

        if (progressBar) {
            progressBar.style.background = isDarkMode ? darkProgressBarBg : lightProgressBarBg;
            progressBar.style.boxShadow = isDarkMode ? darkProgressBarShadow : lightProgressBarShadow;
        }
    }

    function detectAndApplyTheme() {
        const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
        applyTheme(prefersDarkMode);
    }

    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectAndApplyTheme);

    // --- Funções Auxiliares para Controle dos Botões ---

    function hideButtons() {
        if (buttonContainer) {
            buttonContainer.style.opacity = '0';
            buttonContainer.style.pointerEvents = 'none';
        }
    }

    function showButtonsAndResetTimer() {
        const scrolledEnough = document.body.scrollTop > 20 || document.documentElement.scrollTop > 20;
        const pageIsScrollable = document.body.scrollHeight > window.innerHeight;

        if (scrolledEnough && pageIsScrollable) {
            if (buttonContainer) {
                buttonContainer.style.opacity = '1';
                buttonContainer.style.pointerEvents = 'auto';
                clearTimeout(inactivityTimer);
                inactivityTimer = setTimeout(hideButtons, INACTIVITY_TIMEOUT);
            }
        } else {
            hideButtons();
            clearTimeout(inactivityTimer);
        }
    }

    // --- Funções para a Barra de Progresso e Rolagem ---

    function getScrollableElement() {
        return document.documentElement.scrollTop > 0 || document.documentElement.scrollHeight > document.documentElement.clientHeight ? document.documentElement : document.body;
    }

    function updateProgressBar() {
        const scrollElem = getScrollableElement();
        const scrollTop = scrollElem.scrollTop;
        const scrollHeight = scrollElem.scrollHeight;
        const clientHeight = scrollElem.clientHeight;

        const totalScrollableHeight = scrollHeight - clientHeight;
        let scrollProgress = 0;

        if (totalScrollableHeight > 0) {
            scrollProgress = (scrollTop / totalScrollableHeight) * 100;
            progressBar.style.width = scrollProgress + '%';
            progressBar.style.display = 'block';
        } else {
            progressBar.style.width = '0%';
            progressBar.style.display = 'none';
        }
    }

    // --- Inicialização dos Elementos (Botões e Barra de Progresso) ---

    function initializeScrollElements() {
        // --- Inicialização dos Botões ---
        if (buttonContainer && buttonContainer.parentNode) {
            buttonContainer.parentNode.removeChild(buttonContainer);
        }

        buttonContainer = document.createElement('div');
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.right = '20px';
        buttonContainer.style.top = '50%';
        buttonContainer.style.transform = 'translateY(-50%)';
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'column';
        buttonContainer.style.gap = '10px';
        buttonContainer.style.opacity = '0';
        buttonContainer.style.transition = 'opacity 0.3s ease-in-out';
        buttonContainer.style.pointerEvents = 'none';

        document.body.appendChild(buttonContainer);

        const baseButtonStyle = {
            color: 'white',
            border: 'none',
            borderRadius: '50%',
            width: '50px',
            height: '50px',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            cursor: 'pointer',
            // box-shadow será definido por applyTheme
            transition: 'background-color 0.2s ease, transform 0.2s ease',
        };

        const applyBaseStyle = (button) => Object.assign(button.style, baseButtonStyle);

        const topArrowSVG = `
            <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="12 19 12 5"></polyline>
                <polyline points="5 12 12 5 19 12"></polyline>
            </svg>
        `;

        const bottomArrowSVG = `
            <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="12 5 12 19"></polyline>
                <polyline points="5 12 12 19 19 12"></polyline>
            </svg>
        `;

        const topButton = document.createElement('button');
        applyBaseStyle(topButton);
        topButton.innerHTML = topArrowSVG;
        topButton.onclick = () => {
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
            showButtonsAndResetTimer();
        };
        buttonContainer.appendChild(topButton);

        const bottomButton = document.createElement('button');
        applyBaseStyle(bottomButton);
        bottomButton.innerHTML = bottomArrowSVG;
        bottomButton.onclick = () => {
            const scrollElem = getScrollableElement();
            const totalHeight = scrollElem.scrollHeight - scrollElem.clientHeight;
            window.scrollTo({
                top: totalHeight,
                behavior: 'smooth'
            });
            showButtonsAndResetTimer();
        };
        buttonContainer.appendChild(bottomButton);

        // --- Inicialização da Barra de Progresso ---
        if (progressBar && progressBar.parentNode) {
            progressBar.parentNode.removeChild(progressBar);
        }

        progressBar = document.createElement('div');
        progressBar.style.position = 'fixed';
        progressBar.style.bottom = '0';
        progressBar.style.left = '0';
        progressBar.style.width = '0%';
        progressBar.style.height = '5px';
        progressBar.style.zIndex = '10000';
        progressBar.style.transition = 'width 0.2s ease-out';
        progressBar.style.display = 'none';
        document.body.appendChild(progressBar);

        // --- Aplica o tema inicial ---
        detectAndApplyTheme();

        // --- Eventos para mostrar/esconder os botões e atualizar a barra de progresso ---

        window.onscroll = () => {
            showButtonsAndResetTimer();
            updateProgressBar();
        };

        document.onmousemove = (event) => {
            if (event.clientX > (window.innerWidth - RIGHT_EDGE_THRESHOLD_PX)) {
                showButtonsAndResetTimer();
            }
        };

        document.addEventListener('touchstart', showButtonsAndResetTimer, { passive: true });
        document.addEventListener('touchmove', showButtonsAndResetTimer, { passive: true });

        // --- Observador de Mutação para SPAs (detecta mudanças no DOM) ---
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList' || mutation.type === 'subtree') {
                    showButtonsAndResetTimer();
                    updateProgressBar();
                    detectAndApplyTheme(); // Reaplicar tema em SPAs que mudam muito o DOM
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: false
        });

        // --- Intercepta a API de Histórico para SPAs (detecta mudanças de URL sem reload) ---
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function() {
            originalPushState.apply(this, arguments);
            showButtonsAndResetTimer();
            updateProgressBar();
            detectAndApplyTheme(); // Reaplicar tema em SPAs
        };

        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            showButtonsAndResetTimer();
            updateProgressBar();
            detectAndApplyTheme(); // Reaplicar tema em SPAs
        };

        // Garante que os elementos apareçam/desapareçam/atualizem corretamente na carga inicial
        window.addEventListener('load', () => {
            showButtonsAndResetTimer();
            updateProgressBar();
            detectAndApplyTheme();
        });
        window.addEventListener('DOMContentLoaded', () => {
            showButtonsAndResetTimer();
            updateProgressBar();
            detectAndApplyTheme();
        });
    }

    // Inicializa todos os elementos quando o script é carregado
    initializeScrollElements();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址