Pull-down-to-refresh with adaptive overlay and spinner
// ==UserScript==
// @name Mobile Pull Down to Refresh
// @namespace TW9iaWxlIFB1bGwgRG93biB0byBSZWZyZXNo
// @version 1.3
// @description Pull-down-to-refresh with adaptive overlay and spinner
// @author smed79
// @license GPLv3
// @icon https://i25.servimg.com/u/f25/11/94/21/24/pd2r10.png
// @homepage https://greasyfork.org/en/scripts/545016-mobile-pull-down-to-refresh
// @include http://*
// @include https://*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
// Config
const MIN_DY = 200; // Trigger distance in pixels
const KEY = encodeURIComponent('Pull down to refresh');
const COOLDOWN_MS = 3000; // Cooldown between reloads (ms)
// Exclude domains
const EXCLUDED_DOMAINS = [
// Add patterns here, e.g.:
// 'example.com',
// 'example.*',
// '*.example.com'
];
if (window[KEY]) return;
window[KEY] = true;
let startX = 0;
let startY = 0;
let reachedTop = false;
let onePoint = false;
let lastReloadAt = 0;
function patternToRegExp(pat) {
const esc = pat.replace(/\./g, '\\.').replace(/\*/g, '.*');
return new RegExp('^' + esc + '$', 'i');
}
function isExcludedDomain(hostname) {
if (!hostname) return false;
for (const pat of EXCLUDED_DOMAINS) {
try {
const re = patternToRegExp(pat);
if (re.test(hostname)) return true;
} catch (err) {
// ignore invalid patterns
}
}
return false;
}
try {
const host = location.hostname || '';
if (isExcludedDomain(host)) return;
} catch (err) {
// ignore and continue
}
// Create overlay and styles early but do not attach until needed
const overlay = document.createElement('div');
overlay.className = 'pdr-overlay';
overlay.setAttribute('aria-hidden', 'true');
overlay.style.display = 'none';
overlay.innerHTML = `
<div class="pdr-center">
<div class="pdr-loading-circle" role="status" aria-label="Loading"></div>
</div>
`;
const style = document.createElement('style');
style.textContent = `
/* Base overlay: full-screen, spinner positioned at 10% from top */
.pdr-overlay {
position: fixed;
inset: 0;
z-index: 2147483646;
display: flex;
align-items: flex-start;
justify-content: center;
pointer-events: none;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
transition: opacity 160ms ease;
opacity: 1;
}
.pdr-center {
position: absolute;
top: 10%;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
align-items: center;
pointer-events: auto;
user-select: none;
}
/* Simple spinner, no shadow */
.pdr-loading-circle {
box-sizing: border-box;
border-radius: 50%;
width: 42px;
height: 42px;
border: 6px solid transparent;
animation: pdr-spin 800ms linear infinite;
background: transparent;
}
@keyframes pdr-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Light scheme: semi-transparent light overlay, dark spinner */
@media (prefers-color-scheme: light) {
.pdr-overlay {
background: rgba(255,255,255,0.30);
}
.pdr-loading-circle {
border-top-color: rgba(0,0,0,0.75);
border-right-color: rgba(0,0,0,0.35);
border-bottom-color: rgba(0,0,0,0.12);
border-left-color: rgba(0,0,0,0.12);
}
}
/* Dark scheme: semi-transparent dark overlay, light spinner */
@media (prefers-color-scheme: dark) {
.pdr-overlay {
background: rgba(0,0,0,0.30);
}
.pdr-loading-circle {
border-top-color: rgba(255,255,255,0.95);
border-right-color: rgba(255,255,255,0.35);
border-bottom-color: rgba(255,255,255,0.12);
border-left-color: rgba(255,255,255,0.12);
}
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.pdr-loading-circle { animation: none; }
}
`;
function attachUI() {
if (!document.head) return;
if (!document.head.contains(style)) document.head.appendChild(style);
if (!document.body) return;
if (!document.body.contains(overlay)) document.body.appendChild(overlay);
}
attachUI();
document.addEventListener('DOMContentLoaded', attachUI, { once: true });
function showOverlay() {
attachUI();
overlay.style.display = 'flex';
overlay.setAttribute('aria-hidden', 'false');
const center = overlay.querySelector('.pdr-center');
if (center) center.style.top = '10%';
}
function hideOverlay() {
overlay.style.display = 'none';
overlay.setAttribute('aria-hidden', 'true');
}
function isElementScrollable(el) {
if (!el || el === document.documentElement || el === document.body) return false;
try {
const style = window.getComputedStyle(el);
const overflowY = style.overflowY;
const canScroll = (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay');
if (canScroll && el.scrollHeight > el.clientHeight + 1) return true;
} catch (err) {
// ignore
}
return false;
}
function isInScrollableOrInteractiveArea(target) {
let el = target;
while (el && el !== document.documentElement) {
if (el.matches && (el.matches('input, textarea, select, [contenteditable="true"], [data-pdr-ignore]'))) return true;
if (isElementScrollable(el)) return true;
el = el.parentElement;
}
return false;
}
document.addEventListener('touchstart', function (e) {
if (!e.touches || e.touches.length !== 1) {
onePoint = false;
reachedTop = false;
return;
}
try {
if (isExcludedDomain(location.hostname)) {
onePoint = false;
reachedTop = false;
return;
}
} catch (err) {
// continue
}
const target = e.target;
if (isInScrollableOrInteractiveArea(target)) {
onePoint = false;
reachedTop = false;
return;
}
const scrollTop = (document.scrollingElement && document.scrollingElement.scrollTop) ||
document.documentElement.scrollTop ||
document.body.scrollTop || 0;
if (scrollTop > 5) {
onePoint = false;
reachedTop = false;
return;
}
onePoint = true;
reachedTop = true;
startX = e.touches[0].screenX;
startY = e.touches[0].screenY;
}, { passive: true });
document.addEventListener('touchend', function (e) {
if (!onePoint || !reachedTop) {
onePoint = false;
reachedTop = false;
return;
}
const touch = e.changedTouches && e.changedTouches[0];
if (!touch) {
onePoint = false;
reachedTop = false;
return;
}
const dY = Math.floor(touch.screenY - startY);
const dX = Math.abs(touch.screenX - startX);
const now = Date.now();
if (now - lastReloadAt < COOLDOWN_MS) {
onePoint = false;
reachedTop = false;
return;
}
if (dY > MIN_DY && dX < 0.4 * dY) {
showOverlay();
setTimeout(function () {
try {
lastReloadAt = Date.now();
location.reload();
} catch (err) {
hideOverlay();
console.error('Pull down to refresh reload failed', err);
}
}, 300);
}
onePoint = false;
reachedTop = false;
}, { passive: true, capture: true });
window.addEventListener('pagehide', function () {
hideOverlay();
});
})();