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 (SVG, discreet, SPA & mobile friendly).

当前为 2025-06-06 提交的版本,查看 最新版本

// ==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.0
// @description [en] A modern scroll progress bar at the bottom of the screen and smart scroll-to-top/bottom buttons (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 (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; // Nova variável para a barra de progresso

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

    // Função para esconder os botões
    function hideButtons() {
        if (buttonContainer) {
            buttonContainer.style.opacity = '0';
            buttonContainer.style.pointerEvents = 'none'; // Desabilita cliques quando invisível
        }
    }

    // Função para mostrar os botões e resetar o timer
    function showButtonsAndResetTimer() {
        // Verifica se a página é rolavel o suficiente antes de mostrar
        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'; // Habilita cliques
                clearTimeout(inactivityTimer);
                inactivityTimer = setTimeout(hideButtons, INACTIVITY_TIMEOUT);
            }
        } else {
            // Se não for rolavel ou estiver no topo, garante que estejam escondidos
            hideButtons();
            clearTimeout(inactivityTimer);
        }
    }

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

    function updateProgressBar() {
        const docElem = document.documentElement;
        const body = document.body;
        const scrollTop = docElem.scrollTop || body.scrollTop; // Posição de rolagem atual
        const scrollHeight = docElem.scrollHeight || body.scrollHeight; // Altura total do conteúdo
        const clientHeight = docElem.clientHeight || window.innerHeight; // Altura visível da janela

        const totalScrollableHeight = scrollHeight - clientHeight; // Altura total que pode ser rolada
        let scrollProgress = 0;

        if (totalScrollableHeight > 0) { // Apenas se a página for rolavel
            scrollProgress = (scrollTop / totalScrollableHeight) * 100;
            progressBar.style.width = scrollProgress + '%';
            progressBar.style.display = 'block'; // Mostra a barra
        } else {
            progressBar.style.width = '0%'; // Reseta a largura para 0
            progressBar.style.display = 'none'; // Esconde se a página não for rolavel
        }
    }


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

    function initializeScrollElements() {
        // --- Inicialização dos Botões ---
        // Se o container de botões já existe, remove para recriar (útil para SPAs)
        if (buttonContainer && buttonContainer.parentNode) {
            buttonContainer.parentNode.removeChild(buttonContainer);
        }

        // Cria o container para os botões para centralizá-los
        buttonContainer = document.createElement('div');
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.right = '20px'; // Distância da margem direita
        buttonContainer.style.top = '50%'; // Começa no meio vertical
        buttonContainer.style.transform = 'translateY(-50%)'; // Ajusta para centralizar exatamente
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'column'; // Organiza os botões em coluna
        buttonContainer.style.gap = '10px'; // Espaço entre os botões
        buttonContainer.style.opacity = '0'; // Começa invisível
        buttonContainer.style.transition = 'opacity 0.3s ease-in-out'; // Transição suave para aparecer/desaparecer
        buttonContainer.style.pointerEvents = 'none'; // Desabilita cliques quando invisível

        document.body.appendChild(buttonContainer);

        // Estilo base para os botões
        const baseButtonStyle = {
            backgroundColor: 'rgba(0, 123, 255, 0.5)', // Azul com 50% de opacidade
            color: 'white', // Cor da seta (herda para o SVG)
            border: 'none',
            borderRadius: '50%', // Torna o botão circular
            width: '50px',   // Largura para formar o círculo
            height: '50px',  // Altura para formar o círculo
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            cursor: 'pointer',
            boxShadow: '0 3px 6px rgba(0,0,0,0.3)', // Sombra mais proeminente
            transition: 'background-color 0.2s ease, transform 0.2s ease', // Transição suave para hover e click
        };

        // Estilo para hover
        const hoverStyle = {
            backgroundColor: 'rgba(0, 123, 255, 0.9)', // Mais opaco ao passar o mouse
            transform: 'scale(1.05)', // Aumenta levemente ao passar o mouse
        };

        // --- SVGs das setas ---
        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>
        `;

        // Cria o botão "Subir"
        const topButton = document.createElement('button');
        Object.assign(topButton.style, baseButtonStyle);
        topButton.innerHTML = topArrowSVG; // Adiciona o SVG ao botão

        topButton.onmouseover = () => Object.assign(topButton.style, hoverStyle);
        topButton.onmouseout = () => Object.assign(topButton.style, baseButtonStyle);
        topButton.onclick = () => {
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
            showButtonsAndResetTimer(); // Resetar o timer após o clique
        };
        buttonContainer.appendChild(topButton);

        // Cria o botão "Descer"
        const bottomButton = document.createElement('button');
        Object.assign(bottomButton.style, baseButtonStyle);
        bottomButton.innerHTML = bottomArrowSVG; // Adiciona o SVG ao botão

        bottomButton.onmouseover = () => Object.assign(bottomButton.style, hoverStyle);
        bottomButton.onmouseout = () => Object.assign(bottomButton.style, baseButtonStyle);
        bottomButton.onclick = () => {
            window.scrollTo({
                top: document.body.scrollHeight,
                behavior: 'smooth'
            });
            showButtonsAndResetTimer(); // Resetar o timer após o clique
        };
        buttonContainer.appendChild(bottomButton);


        // --- Inicialização da Barra de Progresso ---
        // Se a barra de progresso já existe, remove para recriar
        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%'; // Começa com 0% de largura
        progressBar.style.height = '5px'; // Altura da barra
        progressBar.style.zIndex = '10000'; // Garante que fique acima de outros elementos
        progressBar.style.background = 'linear-gradient(to right, #007bff, #00c7ff, #007bff)'; // Gradiente azul com "luzes"
        progressBar.style.boxShadow = '0 -2px 10px rgba(0, 123, 255, 0.7)'; // Sombra com efeito de luz
        progressBar.style.transition = 'width 0.2s ease-out'; // Transição suave para o progresso
        progressBar.style.display = 'none'; // Inicialmente oculta
        document.body.appendChild(progressBar);


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

        // Eventos de rolagem (funciona para desktop e mobile)
        window.onscroll = () => {
            showButtonsAndResetTimer(); // Lógica dos botões
            updateProgressBar(); // Lógica da barra de progresso
        };

        // Eventos de mouse para desktop: Só ativa os botões se o mouse estiver perto da borda direita
        document.onmousemove = (event) => {
            if (event.clientX > (window.innerWidth - RIGHT_EDGE_THRESHOLD_PX)) {
                showButtonsAndResetTimer();
            }
        };

        // Eventos de toque para mobile (usando addEventListener para 'passive')
        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(); // Para botões
                    updateProgressBar(); // Para barra de progresso
                }
            });
        });

        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(); // Para botões
            updateProgressBar(); // Para barra de progresso
        };

        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            showButtonsAndResetTimer(); // Para botões
            updateProgressBar(); // Para barra de progresso
        };

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

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

})();

QingJ © 2025

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