Gelbooru Robust Infinite Scroll + Save State

Надёжный автопейджер для классических страниц Gelbooru (pid, теги) + сохранение/восстановление позиции и загруженных постов.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Gelbooru Robust Infinite Scroll + Save State
// @namespace    vm/gelbooru-autopager-robust-save
// @version      2.1
// @description  Надёжный автопейджер для классических страниц Gelbooru (pid, теги) + сохранение/восстановление позиции и загруженных постов.
// @match        https://gelbooru.com/index.php?page=post&s=list*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const PAGE_SIZE = 42;
  const AUTO_LOAD = true; // автоподгрузка при прокрутке
  const SCROLL_THRESHOLD_PX = 800;

  // ---- helpers для обнаружения контейнера и постов ----
  function findPostsContainer(doc = document) {
    const candidates = [
      'ul#post-list-posts',
      '#post-list-posts',
      '#posts',
      '.thumbnail-container',
      '.thumbs',
      '.post-list',
      '#post-list',
      '#content #posts',
      'div#post-list-posts',
      'div.thumb-list'
    ];
    for (const s of candidates) {
      const el = doc.querySelector(s);
      if (el) return el;
    }
    // fallback: найдём множество ссылок на просмотр поста и возьмём их общий контейнер
    const anchors = doc.querySelectorAll('a[href*="index.php?page=post&s=view&id="], a[href*="/post/show/"], a[href*="/post/"]');
    if (anchors.length) {
      let p = anchors[0].closest('ul,div,section') || document.body;
      return p;
    }
    return null;
  }

  function getPostNodes(root) {
    const trySelectors = [
      'ul#post-list-posts > li',
      '#post-list-posts > li',
      '#posts > article',
      '#posts > div',
      '.thumbs > li',
      '.thumbs > div',
      '.thumb',
      'article.post-preview',
      'li[id^="p"]',
      '#post-list-posts li',
      '.post-preview'
    ];
    for (const sel of trySelectors) {
      const nodes = root.querySelectorAll(sel);
      if (nodes && nodes.length) return Array.from(nodes);
    }
    // супер-фолбэк: взять элементы по ссылкам на просмотр поста
    const anchors = Array.from(root.querySelectorAll('a[href*="index.php?page=post&s=view&id="], a[href*="/post/show/"], a[href*="/post/"]'));
    if (anchors.length) {
      return anchors.map(a => a.closest('li') || a.closest('div') || a);
    }
    return [];
  }

  function extractPostId(node) {
    if (!node) return null;
    // data attributes
    const ds = node.dataset?.postId || node.dataset?.id || node.getAttribute('data-id');
    if (ds && /\d/.test(ds)) return String(ds).match(/\d+/)[0];

    // id attribute like "p123"
    if (node.id) {
      const m = node.id.match(/\d+/);
      if (m) return m[0];
    }

    // ссылки внутри
    const a = node.querySelector('a[href*="index.php?page=post&s=view&id="], a[href*="/post/show/"], a[href*="/post/"]');
    if (a && a.href) {
      const m1 = a.href.match(/id=(\d+)/);
      if (m1) return m1[1];
      const m2 = a.href.match(/post\/show\/(\d+)/);
      if (m2) return m2[1];
      const m3 = a.href.match(/\/post\/(\d+)/);
      if (m3) return m3[1];
    }

    // src миниатюры / пути с цифрами
    const img = node.querySelector('img[src], img[data-src]');
    if (img && img.src) {
      const m = img.src.match(/\/(\d+)[^\d]/);
      if (m) return m[1];
    }

    return null;
  }

  function absolutize(node, baseHref) {
    const base = new URL(baseHref, location.origin);
    // обычные ссылки
    node.querySelectorAll('a[href]').forEach(a => {
      try { a.href = new URL(a.getAttribute('href'), base).href; } catch(e) {}
    });
    // img: если lazy (data-src) — перенести
    node.querySelectorAll('img').forEach(img => {
      const ds = img.getAttribute('data-src') || img.getAttribute('data-original') || img.getAttribute('data-lazy-src');
      if (ds) {
        try { img.src = new URL(ds, base).href; } catch(e) {}
      } else if (img.getAttribute('src')) {
        try { img.src = new URL(img.getAttribute('src'), base).href; } catch(e) {}
      }
      // srcset
      const ss = img.getAttribute('srcset');
      if (ss) {
        const parts = ss.split(',').map(s => s.trim()).map(entry => {
          const [url, dpr] = entry.split(/\s+/);
          try { return new URL(url, base).href + (dpr ? ' ' + dpr : ''); } catch(e) { return entry; }
        });
        img.setAttribute('srcset', parts.join(', '));
      }
    });
  }

  // ---- начальная логика ----
  const container = findPostsContainer(document);
  if (!container) {
    console.warn('[Gelbooru Autopager] контейнер не найден — возможно нестандартная тема');
    return;
  }

  // сформируем ключ для localStorage: URL *без* pid (чтобы хранить по тегам/поиску)
  const curUrl = new URL(location.href);
  const paramsNoPid = new URLSearchParams(curUrl.searchParams);
  paramsNoPid.delete('pid');
  const STORAGE_KEY = 'gelbooru_autopager:' + curUrl.origin + curUrl.pathname + '?' + paramsNoPid.toString();

  // прочитаем pid из url (если есть)
  let pid = parseInt(curUrl.searchParams.get('pid') || '0', 10) || 0;
  let loading = false;
  let ended = false;

  // восстановление состояния (если есть)
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (raw) {
      const saved = JSON.parse(raw);
      if (saved && saved.html && !isNaN(Number(saved.pid))) {
        // подменяем содержимое контейнера и восстанавливаем скролл
        try {
          container.innerHTML = saved.html;
          pid = Number(saved.pid);
          // небольшая задержка чтобы страница успела отрисоваться
          setTimeout(() => { window.scrollTo(0, saved.scroll || 0); }, 80);
          console.log('[Gelbooru Autopager] restored state, pid=', pid, ' scroll=', saved.scroll);
        } catch (e) {
          console.warn('[Gelbooru Autopager] restore failed', e);
        }
      }
    }
  } catch (e) {
    console.warn('[Gelbooru Autopager] error reading saved state', e);
  }

  // Собираем уже видимые id, чтобы не дублировать
  const initialPosts = getPostNodes(container);
  const seen = new Set(initialPosts.map(n => extractPostId(n)).filter(Boolean));
  console.log('[Gelbooru Autopager] found container with', initialPosts.length, 'items, seen=', seen.size);

  // UI: статус и кнопка
  const panel = document.createElement('div');
  panel.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:99999;background:rgba(0,0,0,0.6);color:#fff;padding:10px;border-radius:10px;font:13px/1.1 system-ui;box-shadow:0 6px 18px rgba(0,0,0,0.4)';
  const status = document.createElement('div');
  status.textContent = 'Autopager ready';
  status.style.marginBottom = '6px';
  const btn = document.createElement('button');
  btn.textContent = 'Load more';
  btn.style.cssText = 'cursor:pointer;border:none;padding:6px 10px;border-radius:8px;font-weight:600';
  const toggleAuto = document.createElement('button');
  toggleAuto.textContent = AUTO_LOAD ? 'Auto: ON' : 'Auto: OFF';
  toggleAuto.style.cssText = 'margin-left:6px;border-radius:8px;padding:6px 8px;border:none;background:#666;color:#fff';
  const clearBtn = document.createElement('button');
  clearBtn.textContent = 'Clear saved';
  clearBtn.style.cssText = 'margin-left:6px;border-radius:8px;padding:6px 8px;border:none;background:#b33;color:#fff';
  panel.appendChild(status);
  panel.appendChild(btn);
  panel.appendChild(toggleAuto);
  panel.appendChild(clearBtn);
  document.body.appendChild(panel);

  btn.addEventListener('click', () => loadNext());
  let autoEnabled = AUTO_LOAD;
  toggleAuto.addEventListener('click', () => { autoEnabled = !autoEnabled; toggleAuto.textContent = autoEnabled ? 'Auto: ON' : 'Auto: OFF'; });
  clearBtn.addEventListener('click', () => {
    localStorage.removeItem(STORAGE_KEY);
    status.textContent = 'Saved cleared';
    console.log('[Gelbooru Autopager] cleared saved state for', STORAGE_KEY);
  });

  // автоподгрузка
  if (AUTO_LOAD) window.addEventListener('scroll', onScroll, { passive: true });

  function onScroll() {
    if (!autoEnabled || loading || ended) return;
    const remain = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
    if (remain < SCROLL_THRESHOLD_PX) loadNext();
    // регулярно сохраняем позицию/контент
    saveState();
  }

  async function loadNext() {
    if (loading || ended) return;
    loading = true;
    status.textContent = 'Loading...';
    pid += PAGE_SIZE;

    const next = new URL(location.href);
    next.searchParams.set('pid', String(pid));

    try {
      const resp = await fetch(next.href, { credentials: 'include' });
      if (!resp.ok) {
        console.error('[Gelbooru Autopager] HTTP', resp.status);
        status.textContent = 'HTTP ' + resp.status;
        loading = false;
        return;
      }
      const text = await resp.text();
      const doc = new DOMParser().parseFromString(text, 'text/html');

      // на странице может быть метка "no results"
      const newContainer = findPostsContainer(doc);
      const newNodes = newContainer ? getPostNodes(newContainer) : [];

      if (!newNodes || newNodes.length === 0) {
        // попробуем найти rel=next — если нет, значит конец
        const rel = doc.querySelector('a[rel="next"], link[rel="next"]');
        if (!rel) {
          ended = true;
          status.textContent = 'No more posts';
          console.log('[Gelbooru Autopager] no more posts.');
          window.removeEventListener('scroll', onScroll);
        } else {
          status.textContent = 'No items parsed from page';
        }
        loading = false;
        return;
      }

      let appended = 0;
      const frag = document.createDocumentFragment();
      for (const nd of newNodes) {
        const cloned = document.importNode(nd, true);
        const id = extractPostId(cloned);
        if (id && seen.has(id)) continue;
        if (id) seen.add(id);

        absolutize(cloned, next.href);
        frag.appendChild(cloned);
        appended++;
      }

      if (appended > 0) {
        // разделитель
        const sep = document.createElement('div');
        sep.textContent = `— page pid=${pid} —`;
        sep.style.cssText = 'text-align:center;margin:12px 0;opacity:.6;font-size:12px';
        container.appendChild(sep);
        container.appendChild(frag);
        status.textContent = `Loaded pid=${pid} (+${appended})`;

        // обновим URL без создания новой записи в истории (чтобы Back возвращал на нужный pid)
        try {
          history.replaceState(null, '', next.pathname + next.search);
        } catch (e) { /* ignore */ }

        // сохраним состояние после успешной загрузки
        saveState();
      } else {
        status.textContent = 'No new posts (duplicates)';
      }
      // small delay to let images start loading
      setTimeout(() => { loading = false; }, 300);
    } catch (err) {
      console.error('[Gelbooru Autopager] error', err);
      status.textContent = 'Error (see console)';
      loading = false;
    }
  }

  function saveState() {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({
        html: container.innerHTML,
        pid: pid,
        scroll: window.scrollY,
        ts: Date.now()
      }));
    } catch (e) {
      console.warn('[Gelbooru Autopager] save failed', e);
    }
  }

  // если страница короткая — подгрузим сразу пару страниц
  setTimeout(() => {
    if (autoEnabled) onScroll();
  }, 900);
})();