Gelbooru Robust Infinite Scroll + Save State

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
})();