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).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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.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();

})();