Pull-down-to-refresh with adaptive overlay, spinner, and real-time drag physics
// ==UserScript==
// @name Pull Down to Refresh
// @namespace UHVsbCBEb3duIHRvIFJlZnJlc2g
// @version 1.4
// @description Pull-down-to-refresh with adaptive overlay, spinner, and real-time drag physics
// @author smed79
// @license GPLv3
// @icon https://i25.servimg.com/u/f25/11/94/21/24/pd2r10.png
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
const MIN_DY = 180; // Distance needed to trigger refresh
const RESISTANCE = 0.4; // How much the spinner resists being pulled down
const KEY = encodeURIComponent('Pull down to refresh');
const EXCLUDED_DOMAINS = [];
if (window[KEY]) return;
window[KEY] = true;
let startX = 0;
let startY = 0;
let isAtTop = false;
let isPulling = false;
let centerEl = null;
function isExcludedDomain(hostname) {
if (!hostname) return false;
for (const pat of EXCLUDED_DOMAINS) {
try {
const re = new RegExp('^' + pat.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i');
if (re.test(hostname)) return true;
} catch (err) {}
}
return false;
}
if (isExcludedDomain(location.hostname)) return;
// Build UI
const overlay = document.createElement('div');
overlay.className = 'pdr-overlay';
overlay.setAttribute('aria-hidden', 'true');
overlay.style.display = 'none';
overlay.innerHTML = `
<div class="pdr-center" id="pdr-center">
<div class="pdr-loading-circle" role="status" aria-label="Loading"></div>
</div>
`;
const style = document.createElement('style');
style.textContent = `
.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 200ms ease;
opacity: 0;
}
.pdr-overlay.pdr-visible {
opacity: 1;
}
.pdr-center {
position: absolute;
top: -60px; /* Hide above screen initially */
left: 50%;
margin-left: -21px; /* Half of width */
display: flex;
align-items: center;
pointer-events: none;
will-change: transform;
transition: transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* When actively dragging, remove transition for instant 1:1 finger tracking */
.pdr-dragging .pdr-center {
transition: none;
}
.pdr-loading-circle {
box-sizing: border-box;
border-radius: 50%;
width: 42px;
height: 42px;
border: 5px solid transparent;
animation: pdr-spin 800ms linear infinite;
}
@keyframes pdr-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@media (prefers-color-scheme: light) {
.pdr-overlay { background: rgba(255,255,255,0.4); }
.pdr-loading-circle { border-top-color: rgba(0,0,0,0.8); border-right-color: rgba(0,0,0,0.2); border-bottom-color: rgba(0,0,0,0.2); border-left-color: rgba(0,0,0,0.2); }
}
@media (prefers-color-scheme: dark) {
.pdr-overlay { background: rgba(0,0,0,0.4); }
.pdr-loading-circle { border-top-color: rgba(255,255,255,0.9); border-right-color: rgba(255,255,255,0.2); border-bottom-color: rgba(255,255,255,0.2); border-left-color: rgba(255,255,255,0.2); }
}
`;
function attachUI() {
if (!document.head || !document.body) return;
if (!document.head.contains(style)) document.head.appendChild(style);
if (!document.body.contains(overlay)) {
document.body.appendChild(overlay);
centerEl = document.getElementById('pdr-center');
}
}
attachUI();
document.addEventListener('DOMContentLoaded', attachUI);
// Optimized Scroll Checker (Fixes Layout Thrashing)
function isElementScrollable(el) {
if (!el || el === document.documentElement || el === document.body) return false;
// FAST PATH: If it physically cannot scroll, skip getComputedStyle immediately
if (el.scrollHeight <= el.clientHeight + 1) return false;
try {
const overflowY = window.getComputedStyle(el).overflowY;
return (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay');
} catch (err) {
return false;
}
}
function isInScrollableOrInteractiveArea(target) {
let el = target;
while (el && el !== document.documentElement) {
if (el.matches && el.matches('input, textarea, select, button, [contenteditable="true"]')) return true;
if (isElementScrollable(el)) return true;
el = el.parentElement;
}
return false;
}
function getScrollTop() {
return Math.max(
window.scrollY || 0,
document.documentElement.scrollTop || 0,
document.body.scrollTop || 0
);
}
document.addEventListener('touchstart', (e) => {
if (e.touches.length !== 1) return;
attachUI();
if (getScrollTop() > 5 || isInScrollableOrInteractiveArea(e.target)) {
isAtTop = false;
return;
}
isAtTop = true;
startX = e.touches[0].screenX;
startY = e.touches[0].screenY;
}, { passive: true });
// Add Real-time visual pull effect
document.addEventListener('touchmove', (e) => {
if (!isAtTop) return;
const touch = e.touches[0];
const dY = touch.screenY - startY;
const dX = Math.abs(touch.screenX - startX);
// If pulling down and mostly vertically
if (dY > 0 && dX < dY * 0.6) {
if (!isPulling) {
isPulling = true;
overlay.style.display = 'flex';
overlay.classList.add('pdr-dragging');
}
// Calculate resistance physics
const pullDistance = Math.min(dY * RESISTANCE, MIN_DY + 40);
// Move spinner down with finger
if (centerEl) centerEl.style.transform = `translate3d(0, ${pullDistance}px, 0)`;
overlay.style.opacity = Math.min(pullDistance / MIN_DY, 1).toString();
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!isAtTop || !isPulling) {
isAtTop = false;
isPulling = false;
return;
}
isAtTop = false;
isPulling = false;
overlay.classList.remove('pdr-dragging');
const touch = e.changedTouches[0];
const dY = touch.screenY - startY;
if (dY > MIN_DY) {
// Trigger Reload
overlay.classList.add('pdr-visible');
if (centerEl) centerEl.style.transform = `translate3d(0, 120px, 0)`; // lock spinner in view
setTimeout(() => {
try { location.reload(); }
catch (err) { overlay.style.display = 'none'; }
}, 300);
} else {
// Abort and snap back
if (centerEl) centerEl.style.transform = 'translate3d(0, 0, 0)';
overlay.classList.remove('pdr-visible');
setTimeout(() => { overlay.style.display = 'none'; }, 200); // Wait for transition
}
}, { passive: true });
})();