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