Greasy Fork is available in English.

Mobile Pull Down to Refresh

Pull-down-to-refresh with adaptive overlay and spinner

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
  });
})();