IG Reels Auto-Advance (+ Toggle)

Auto-advance to the next Instagram Reel when the current one ends (handles looping) with a toggle button

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         IG Reels Auto-Advance (+ Toggle)
// @namespace     https://github.com/wintrick/reels-auto-scroll 
// @version      1.0
// @description  Auto-advance to the next Instagram Reel when the current one ends (handles looping) with a toggle button
// @match        https://www.instagram.com/*
// @run-at       document-idle
// @grant        none
// @license Apache 2.0
// ==/UserScript==

(function () {
  'use strict';

  var STORAGE_KEY = 'igReelsAutoScroll';
  var enabled = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'true');

  function createToggle() {
    if (document.getElementById('ig-auto-advance-toggle')) return;
    var btn = document.createElement('button');
    btn.id = 'ig-auto-advance-toggle';
    btn.textContent = 'Auto-Scroll: ' + (enabled ? 'ON' : 'OFF');
    var s = btn.style;
    s.position = 'fixed';
    s.top = '80px';
    s.right = '20px';
    s.zIndex = '99999';
    s.background = '#111';
    s.color = '#fff';
    s.border = 'none';
    s.padding = '8px 12px';
    s.borderRadius = '10px';
    s.fontSize = '13px';
    s.cursor = 'pointer';
    s.opacity = '0.85';
    btn.onclick = function () {
      enabled = !enabled;
      localStorage.setItem(STORAGE_KEY, JSON.stringify(enabled));
      btn.textContent = 'Auto-Scroll: ' + (enabled ? 'ON' : 'OFF');
    };
    document.body.appendChild(btn);
  }

  function getScrollContainer() {
    // Prefer a large, snap-scrolling container if present
    var snap = document.querySelector('div[style*="scroll-snap-type"]');
    if (snap && snap.scrollHeight > snap.clientHeight) return snap;

    // Fallback: largest scrollable div roughly viewport-sized
    var nodes = document.querySelectorAll('div');
    var best = null;
    var bestH = 0;
    for (var i = 0; i < nodes.length; i++) {
      var el = nodes[i];
      var cs = window.getComputedStyle(el);
      if ((cs.overflowY === 'auto' || cs.overflowY === 'scroll') &&
          el.scrollHeight > el.clientHeight + 5 &&
          el.clientHeight >= window.innerHeight * 0.6) {
        if (el.clientHeight > bestH) { best = el; bestH = el.clientHeight; }
      }
    }
    return best;
  }

  function smoothScrollNext() {
    var sc = getScrollContainer();
    var amount = (sc && sc.clientHeight) ? sc.clientHeight : window.innerHeight;

    // Debounce global: avoid double-firing on the same near-end window
    if (smoothScrollNext._lock) return;
    smoothScrollNext._lock = true;
    setTimeout(function(){ smoothScrollNext._lock = false; }, 800);

    if (sc && typeof sc.scrollBy === 'function') {
      sc.scrollBy({ top: amount, behavior: 'smooth' });
    } else if (sc) {
      sc.scrollTop = sc.scrollTop + amount;
    } else {
      window.scrollBy({ top: amount, behavior: 'smooth' });
    }
  }

  function isInViewport(el) {
    var r = el.getBoundingClientRect();
    var vh = window.innerHeight || document.documentElement.clientHeight;
    var vw = window.innerWidth || document.documentElement.clientWidth;
    var visH = Math.min(r.bottom, vh) - Math.max(r.top, 0);
    var visW = Math.min(r.right, vw) - Math.max(r.left, 0);
    return visH > vh * 0.5 && visW > vw * 0.5;
  }

  function bindVideo(video) {
    if (!video || video.dataset.igAutoBound === '1') return;
    video.dataset.igAutoBound = '1';

    // If reels ever stop looping, this still works:
    video.addEventListener('ended', function () {
      if (enabled) smoothScrollNext();
    });

    // Loop-safe near-end watcher
    var tickTimer = null;
    function checkNearEnd() {
      if (!enabled) return;
      if (!isInViewport(video)) return;
      var d = video.duration, t = video.currentTime;
      if (!isFinite(d) || d <= 0) return;
      // Trigger within last ~200ms
      if (d - t <= 0.2) {
        if (video.dataset.igJustAdvanced !== '1') {
          video.dataset.igJustAdvanced = '1';
          smoothScrollNext();
          setTimeout(function(){ video.dataset.igJustAdvanced = '0'; }, 1500);
        }
      }
    }
    function start() {
      if (tickTimer) clearInterval(tickTimer);
      tickTimer = setInterval(checkNearEnd, 200);
    }
    function stop() {
      if (tickTimer) { clearInterval(tickTimer); tickTimer = null; }
    }

    video.addEventListener('play', start);
    video.addEventListener('loadedmetadata', checkNearEnd);
    video.addEventListener('timeupdate', checkNearEnd);
    video.addEventListener('pause', stop);

    // If it’s already playing when we attach:
    if (!video.paused) start();
  }

  function scanAndBind() {
    var vids = document.querySelectorAll('video');
    for (var i = 0; i < vids.length; i++) bindVideo(vids[i]);
  }

  function init() {
    createToggle();
    scanAndBind();

    // Watch for route changes / virtual DOM updates
    var mo = new MutationObserver(function () {
      createToggle();
      scanAndBind();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();