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

  1. // ==UserScript==
  2. // @name Progress Bar and Quick Up and Down Buttons
  3. // @name:pt-BR BF - Barra de progressão e Botões de Subida e Decida Rápido
  4. // @namespace https://github.com/BrunoFortunatto
  5. // @version 1.1
  6. // @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).
  7. // @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).
  8. // @author Bruno Fortunato
  9. // @match *://*/*
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // VERIFICAÇÃO PARA EVITAR IFRAMES
  18. if (window.self !== window.top) {
  19. // Se este script estiver rodando dentro de um iframe, ele para aqui.
  20. return;
  21. }
  22.  
  23. const INACTIVITY_TIMEOUT = 2000; // Tempo em milissegundos (2 segundos) para esconder os botões
  24. const RIGHT_EDGE_THRESHOLD_PX = 100; // Distância da borda direita para ativar os botões no PC
  25. let inactivityTimer;
  26. let buttonContainer;
  27. let progressBar;
  28.  
  29. // --- Funções Auxiliares para Controle de Tema ---
  30.  
  31. function applyTheme(isDarkMode) {
  32. // Cores para os botões
  33. const lightButtonBg = 'rgba(0, 123, 255, 0.5)'; // Azul claro padrão
  34. const darkButtonBg = 'rgba(50, 50, 70, 0.7)'; // Cinza escuro para o modo escuro
  35. const lightButtonHoverBg = 'rgba(0, 123, 255, 0.9)'; // Azul mais forte no hover
  36. const darkButtonHoverBg = 'rgba(80, 80, 100, 0.9)'; // Cinza mais forte no hover
  37. const buttonShadow = '0 3px 6px rgba(0,0,0,0.4)'; // Sombra padrão para ambos (ajusta a opacidade para ser visível)
  38. const darkButtonShadow = '0 3px 10px rgba(0,0,0,0.6)'; // Sombra mais intensa no modo escuro para contraste
  39.  
  40. // Cores para a barra de progresso
  41. const lightProgressBarBg = 'linear-gradient(to right, #007bff, #00c7ff, #007bff)'; // Gradiente azul padrão
  42. // Gradiente para modo escuro: Mantenho tons escuros no preenchimento mas adiciono um brilho mais evidente
  43. const darkProgressBarBg = 'linear-gradient(to right, #3498db, #4a69bd, #3498db)'; // Azul mais perceptível no escuro
  44. const lightProgressBarShadow = '0 -2px 10px rgba(0, 123, 255, 0.7)'; // Sombra azul brilhante padrão
  45. // Nova sombra para modo escuro: mais intensa e com brilho para "luz"
  46. 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
  47.  
  48. const textColor = 'white'; // Cor do texto/ícones permanece branco
  49.  
  50. if (buttonContainer) {
  51. buttonContainer.querySelectorAll('button').forEach(button => {
  52. button.style.backgroundColor = isDarkMode ? darkButtonBg : lightButtonBg;
  53. button.style.boxShadow = isDarkMode ? darkButtonShadow : buttonShadow; // Aplica a sombra específica do tema
  54. button.onmouseover = () => Object.assign(button.style, { backgroundColor: isDarkMode ? darkButtonHoverBg : lightButtonHoverBg, transform: 'scale(1.05)' });
  55. button.onmouseout = () => Object.assign(button.style, { backgroundColor: isDarkMode ? darkButtonBg : lightButtonBg, transform: 'scale(1)' });
  56. button.style.color = textColor;
  57. });
  58. }
  59.  
  60. if (progressBar) {
  61. progressBar.style.background = isDarkMode ? darkProgressBarBg : lightProgressBarBg;
  62. progressBar.style.boxShadow = isDarkMode ? darkProgressBarShadow : lightProgressBarShadow;
  63. }
  64. }
  65.  
  66. function detectAndApplyTheme() {
  67. const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
  68. applyTheme(prefersDarkMode);
  69. }
  70.  
  71. window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectAndApplyTheme);
  72.  
  73. // --- Funções Auxiliares para Controle dos Botões ---
  74.  
  75. function hideButtons() {
  76. if (buttonContainer) {
  77. buttonContainer.style.opacity = '0';
  78. buttonContainer.style.pointerEvents = 'none';
  79. }
  80. }
  81.  
  82. function showButtonsAndResetTimer() {
  83. const scrolledEnough = document.body.scrollTop > 20 || document.documentElement.scrollTop > 20;
  84. const pageIsScrollable = document.body.scrollHeight > window.innerHeight;
  85.  
  86. if (scrolledEnough && pageIsScrollable) {
  87. if (buttonContainer) {
  88. buttonContainer.style.opacity = '1';
  89. buttonContainer.style.pointerEvents = 'auto';
  90. clearTimeout(inactivityTimer);
  91. inactivityTimer = setTimeout(hideButtons, INACTIVITY_TIMEOUT);
  92. }
  93. } else {
  94. hideButtons();
  95. clearTimeout(inactivityTimer);
  96. }
  97. }
  98.  
  99. // --- Funções para a Barra de Progresso e Rolagem ---
  100.  
  101. function getScrollableElement() {
  102. return document.documentElement.scrollTop > 0 || document.documentElement.scrollHeight > document.documentElement.clientHeight ? document.documentElement : document.body;
  103. }
  104.  
  105. function updateProgressBar() {
  106. const scrollElem = getScrollableElement();
  107. const scrollTop = scrollElem.scrollTop;
  108. const scrollHeight = scrollElem.scrollHeight;
  109. const clientHeight = scrollElem.clientHeight;
  110.  
  111. const totalScrollableHeight = scrollHeight - clientHeight;
  112. let scrollProgress = 0;
  113.  
  114. if (totalScrollableHeight > 0) {
  115. scrollProgress = (scrollTop / totalScrollableHeight) * 100;
  116. progressBar.style.width = scrollProgress + '%';
  117. progressBar.style.display = 'block';
  118. } else {
  119. progressBar.style.width = '0%';
  120. progressBar.style.display = 'none';
  121. }
  122. }
  123.  
  124. // --- Inicialização dos Elementos (Botões e Barra de Progresso) ---
  125.  
  126. function initializeScrollElements() {
  127. // --- Inicialização dos Botões ---
  128. if (buttonContainer && buttonContainer.parentNode) {
  129. buttonContainer.parentNode.removeChild(buttonContainer);
  130. }
  131.  
  132. buttonContainer = document.createElement('div');
  133. buttonContainer.style.position = 'fixed';
  134. buttonContainer.style.right = '20px';
  135. buttonContainer.style.top = '50%';
  136. buttonContainer.style.transform = 'translateY(-50%)';
  137. buttonContainer.style.zIndex = '9999';
  138. buttonContainer.style.display = 'flex';
  139. buttonContainer.style.flexDirection = 'column';
  140. buttonContainer.style.gap = '10px';
  141. buttonContainer.style.opacity = '0';
  142. buttonContainer.style.transition = 'opacity 0.3s ease-in-out';
  143. buttonContainer.style.pointerEvents = 'none';
  144.  
  145. document.body.appendChild(buttonContainer);
  146.  
  147. const baseButtonStyle = {
  148. color: 'white',
  149. border: 'none',
  150. borderRadius: '50%',
  151. width: '50px',
  152. height: '50px',
  153. display: 'flex',
  154. justifyContent: 'center',
  155. alignItems: 'center',
  156. cursor: 'pointer',
  157. // box-shadow será definido por applyTheme
  158. transition: 'background-color 0.2s ease, transform 0.2s ease',
  159. };
  160.  
  161. const applyBaseStyle = (button) => Object.assign(button.style, baseButtonStyle);
  162.  
  163. const topArrowSVG = `
  164. <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">
  165. <polyline points="12 19 12 5"></polyline>
  166. <polyline points="5 12 12 5 19 12"></polyline>
  167. </svg>
  168. `;
  169.  
  170. const bottomArrowSVG = `
  171. <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">
  172. <polyline points="12 5 12 19"></polyline>
  173. <polyline points="5 12 12 19 19 12"></polyline>
  174. </svg>
  175. `;
  176.  
  177. const topButton = document.createElement('button');
  178. applyBaseStyle(topButton);
  179. topButton.innerHTML = topArrowSVG;
  180. topButton.onclick = () => {
  181. window.scrollTo({
  182. top: 0,
  183. behavior: 'smooth'
  184. });
  185. showButtonsAndResetTimer();
  186. };
  187. buttonContainer.appendChild(topButton);
  188.  
  189. const bottomButton = document.createElement('button');
  190. applyBaseStyle(bottomButton);
  191. bottomButton.innerHTML = bottomArrowSVG;
  192. bottomButton.onclick = () => {
  193. const scrollElem = getScrollableElement();
  194. const totalHeight = scrollElem.scrollHeight - scrollElem.clientHeight;
  195. window.scrollTo({
  196. top: totalHeight,
  197. behavior: 'smooth'
  198. });
  199. showButtonsAndResetTimer();
  200. };
  201. buttonContainer.appendChild(bottomButton);
  202.  
  203. // --- Inicialização da Barra de Progresso ---
  204. if (progressBar && progressBar.parentNode) {
  205. progressBar.parentNode.removeChild(progressBar);
  206. }
  207.  
  208. progressBar = document.createElement('div');
  209. progressBar.style.position = 'fixed';
  210. progressBar.style.bottom = '0';
  211. progressBar.style.left = '0';
  212. progressBar.style.width = '0%';
  213. progressBar.style.height = '5px';
  214. progressBar.style.zIndex = '10000';
  215. progressBar.style.transition = 'width 0.2s ease-out';
  216. progressBar.style.display = 'none';
  217. document.body.appendChild(progressBar);
  218.  
  219. // --- Aplica o tema inicial ---
  220. detectAndApplyTheme();
  221.  
  222. // --- Eventos para mostrar/esconder os botões e atualizar a barra de progresso ---
  223.  
  224. window.onscroll = () => {
  225. showButtonsAndResetTimer();
  226. updateProgressBar();
  227. };
  228.  
  229. document.onmousemove = (event) => {
  230. if (event.clientX > (window.innerWidth - RIGHT_EDGE_THRESHOLD_PX)) {
  231. showButtonsAndResetTimer();
  232. }
  233. };
  234.  
  235. document.addEventListener('touchstart', showButtonsAndResetTimer, { passive: true });
  236. document.addEventListener('touchmove', showButtonsAndResetTimer, { passive: true });
  237.  
  238. // --- Observador de Mutação para SPAs (detecta mudanças no DOM) ---
  239. const observer = new MutationObserver(mutations => {
  240. mutations.forEach(mutation => {
  241. if (mutation.type === 'childList' || mutation.type === 'subtree') {
  242. showButtonsAndResetTimer();
  243. updateProgressBar();
  244. detectAndApplyTheme(); // Reaplicar tema em SPAs que mudam muito o DOM
  245. }
  246. });
  247. });
  248.  
  249. observer.observe(document.body, {
  250. childList: true,
  251. subtree: true,
  252. attributes: false,
  253. characterData: false
  254. });
  255.  
  256. // --- Intercepta a API de Histórico para SPAs (detecta mudanças de URL sem reload) ---
  257. const originalPushState = history.pushState;
  258. const originalReplaceState = history.replaceState;
  259.  
  260. history.pushState = function() {
  261. originalPushState.apply(this, arguments);
  262. showButtonsAndResetTimer();
  263. updateProgressBar();
  264. detectAndApplyTheme(); // Reaplicar tema em SPAs
  265. };
  266.  
  267. history.replaceState = function() {
  268. originalReplaceState.apply(this, arguments);
  269. showButtonsAndResetTimer();
  270. updateProgressBar();
  271. detectAndApplyTheme(); // Reaplicar tema em SPAs
  272. };
  273.  
  274. // Garante que os elementos apareçam/desapareçam/atualizem corretamente na carga inicial
  275. window.addEventListener('load', () => {
  276. showButtonsAndResetTimer();
  277. updateProgressBar();
  278. detectAndApplyTheme();
  279. });
  280. window.addEventListener('DOMContentLoaded', () => {
  281. showButtonsAndResetTimer();
  282. updateProgressBar();
  283. detectAndApplyTheme();
  284. });
  285. }
  286.  
  287. // Inicializa todos os elementos quando o script é carregado
  288. initializeScrollElements();
  289.  
  290. })();