Mobile Refresh Button / Botão de Atualização Mobile

[EN] Shows a refresh button after pulling down at the top of the page. No auto-refresh, optimized performance.

// ==UserScript==
// @name         Mobile Refresh Button / Botão de Atualização Mobile
// @name:pt-BR   Botão de Atualização Mobile
// @namespace    https://github.com/BrunoFortunatto
// @version      1.5
// @description  [EN] Shows a refresh button after pulling down at the top of the page. No auto-refresh, optimized performance.
// @description:pt-BR Exibe um botão de atualização após gesto de puxar no topo da página. Sem recarregamento automático e otimizado.
// @author       Bruno Fortunato
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // Evitar execução dentro de iframes
  if (window.self !== window.top) return;

  // Evitar múltiplas instâncias
  if (document.getElementById('refreshBtn')) return;

  document.body.style.overscrollBehaviorY = 'contain';

  let touchStartY = 0;
  let pulling = false;
  let cooldown = false;
  let showTimeout = null;

  const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

  // Detectar idioma do navegador
  const userLang = navigator.language.startsWith('pt') ? 'pt' : 'en';

  // Texto do botão em diferentes idiomas
  const buttonText = userLang === 'pt' ? 'Atualizar página' : 'Refresh page';

  // Criar estilos no <head> para evitar bloqueio de CSP
  const style = document.createElement('style');
  style.textContent = `
    #refreshBtn {
      position: fixed;
      top: -50px;
      left: 50%;
      transform: translateX(-50%) scale(0.9);
      padding: 12px;
      font-size: 16px;
      border-radius: 25px;
      border: none;
      z-index: 9999;
      background-color: ${isDark ? '#444' : '#2196f3'};
      color: #fff;
      box-shadow: 0 2px 6px rgba(0,0,0,0.3);
      display: flex;
      align-items: center;
      gap: 8px;
      opacity: 0;
      transition: top 0.3s ease-out, opacity 0.3s, transform 0.3s;
    }
    .refresh-icon {
      width: 22px;
      height: 22px;
    }
    @media (max-width: 480px) {
      #refreshBtn {
        font-size: 14px;
        padding: 10px;
      }
      .refresh-icon {
        width: 18px;
        height: 18px;
      }
    }
    @media (min-width: 1024px) {
      #refreshBtn {
        font-size: 18px;
        padding: 14px;
      }
      .refresh-icon {
        width: 24px;
        height: 24px;
      }
    }
  `;
  document.head.appendChild(style);

  // Criar botão de atualização
  const refreshBtn = document.createElement('button');
  refreshBtn.id = 'refreshBtn';

  // Ícone SVG embutido
  refreshBtn.innerHTML = `
    <svg class="refresh-icon" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
      <path d="M12 2V4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20C7.58 20 4 16.42 4 12H2C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM7 11V13H13V11H7Z"/>
    </svg>
    ${buttonText}
  `;

  refreshBtn.addEventListener('click', () => {
    location.reload();
  });

  document.body.appendChild(refreshBtn);

  function showRefreshButton() {
    if (cooldown) return;
    cooldown = true;
    setTimeout(() => (cooldown = false), 1000);

    refreshBtn.style.display = 'flex';
    requestAnimationFrame(() => {
      refreshBtn.style.top = '10px';
      refreshBtn.style.opacity = '1';
      refreshBtn.style.transform = 'translateX(-50%) scale(1)';
    });

    if (showTimeout) clearTimeout(showTimeout);
    showTimeout = setTimeout(hideRefreshButton, 4000);
  }

  function hideRefreshButton() {
    refreshBtn.style.opacity = '0';
    refreshBtn.style.transform = 'translateX(-50%) scale(0.9)';
    refreshBtn.style.top = '-50px';
    setTimeout(() => {
      refreshBtn.style.display = 'none';
    }, 300);
  }

  window.addEventListener('touchstart', (e) => {
    if (window.scrollY <= 0) {
      touchStartY = e.touches[0].clientY;
      pulling = true;
    }
  }, { passive: true });

  window.addEventListener('touchmove', (e) => {
    if (!pulling) return;
    const deltaY = e.touches[0].clientY - touchStartY;
    if (deltaY > 25 && window.scrollY === 0) {
      showRefreshButton();
      pulling = false;
    }
  }, { passive: true });

  window.addEventListener('scroll', () => {
    if (window.scrollY > 50) {
      pulling = false;
      hideRefreshButton();
    }
  }, { passive: true });

})();