您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Надёжный автопейджер для классических страниц Gelbooru (pid, теги) + сохранение/восстановление позиции и загруженных постов.
// ==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); })();