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 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();

})();