// ==UserScript==
// @name DODI Repacks - Endless Scroll + Page Label (Fixed)
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Robust endless scroll for dodi-repacks.site with floating page number indicator (fixed init + debounce).
// @author Doc00n
// @match https://dodi-repacks.site/*
// @match http://dodi-repacks.site/*
// @icon https://dodi-repacks.site/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let loading = false;
let reachedEnd = false;
let nextUrlCache = null;
let currentPageNum = 1;
let scrollTimeout = null;
// ---------- UI ----------
function createLoader() {
let loader = document.getElementById('tm-endless-loader');
if (!loader) {
loader = document.createElement('div');
loader.id = 'tm-endless-loader';
loader.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);bottom:12px;padding:6px 10px;border-radius:8px;background:rgba(0,0,0,0.75);color:#fff;font-size:13px;z-index:999999;display:none';
loader.textContent = 'Loading...';
document.body.appendChild(loader);
}
return loader;
}
function showLoader(show, text) {
const loader = createLoader();
loader.style.display = show ? 'block' : 'none';
if (text) loader.textContent = text;
}
function createPageLabel() {
let label = document.getElementById('tm-page-label');
if (!label) {
label = document.createElement('div');
label.id = 'tm-page-label';
label.style.cssText = [
'position:fixed',
'top:40%',
'left:50%',
'transform:translate(-50%,-50%)',
'background:rgba(0,0,0,0.8)',
'color:#fff',
'font-size:56px',
'font-weight:700',
'padding:18px 36px',
'border-radius:18px',
'z-index:999999',
'opacity:0',
'pointer-events:none',
'transition:opacity 0.6s ease'
].join(';');
document.body.appendChild(label);
}
return label;
}
function showPageLabel(pageNum, timeout = 1600) {
const label = createPageLabel();
label.textContent = `Page ${pageNum}`;
label.style.opacity = '1';
clearTimeout(label._hideTimeout);
label._hideTimeout = setTimeout(() => {
label.style.opacity = '0';
}, timeout);
}
// ---------- Helpers ----------
function findPostContainer(doc = document) {
const selectors = ['main', '#content', '.site-main', '.content', '.posts', '.post-list', '.container'];
for (const sel of selectors) {
const el = doc.querySelector(sel);
if (el && el.children.length > 0) return el;
}
return doc.body;
}
function extractPosts(doc) {
const postSelectors = ['article', '.post', '.entry', '.post-item', '.blog-entry', '.post-wrap'];
for (const sel of postSelectors) {
const nodes = doc.querySelectorAll(sel);
if (nodes && nodes.length > 0) return Array.from(nodes);
}
// fallback: children of main
const main = findPostContainer(doc);
return main ? Array.from(main.children) : [];
}
function deriveNextFromUrl(urlStr) {
try {
const u = new URL(urlStr, location.origin);
const path = u.pathname.replace(/\/$/, '');
const m = path.match(/\/page\/(\d+)$/);
if (m) {
const nextNum = Number(m[1]) + 1;
return u.origin + path.replace(/\/page\/\d+$/, `/page/${nextNum}/`);
}
// If at root or other, try appending /page/2/
return u.origin + (path === '' ? '/' : path) + (path.endsWith('/') ? 'page/2/' : '/page/2/');
} catch (e) {
return null;
}
}
function detectNextUrl(doc = document) {
// 1) <link rel="next">
const relNext = doc.querySelector('link[rel="next"]');
if (relNext && relNext.href) return relNext.href;
// 2) anchors that suggest next
const anchors = Array.from(doc.querySelectorAll('a'));
const nextTexts = ['next', 'next ›', 'next »', '›', '»', '>>'];
for (const a of anchors) {
const txt = (a.textContent || '').trim().toLowerCase();
const aria = (a.getAttribute('aria-label') || '').toLowerCase();
const rel = (a.getAttribute('rel') || '').toLowerCase();
const cl = (a.className || '').toLowerCase();
if (rel.includes('next') || aria.includes('next') || cl.includes('next') || nextTexts.includes(txt)) {
if (a.href) return a.href;
}
}
// 3) pagination container - try to find currently active and return next sibling anchor
const pagination = doc.querySelector('.pagination, .nav-links, .page-numbers, .paging-navigation, .pagination-wrap');
if (pagination) {
const anchorsIn = Array.from(pagination.querySelectorAll('a'));
for (let i = 0; i < anchorsIn.length; i++) {
const a = anchorsIn[i];
if (a.classList.contains('current') || a.getAttribute('aria-current') === 'page') {
if (anchorsIn[i + 1] && anchorsIn[i + 1].href) return anchorsIn[i + 1].href;
}
}
// fallback: look for anchor with class or text next
for (const a of anchorsIn) {
const txt = (a.textContent || '').trim().toLowerCase();
if (txt.includes('next') || a.className.toLowerCase().includes('next')) return a.href;
}
}
// 4) derive from current location
const derived = deriveNextFromUrl(location.href);
if (derived) return derived;
return null;
}
// ---------- Fetch & Append ----------
async function fetchAndAppend(url) {
if (!url || loading || reachedEnd) return;
loading = true;
showLoader(true, 'Loading...');
try {
const res = await fetch(url, { credentials: 'same-origin' });
if (!res.ok) throw new Error('Fetch failed: ' + res.status);
const text = await res.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
const newPosts = extractPosts(doc);
if (!newPosts || newPosts.length === 0) {
reachedEnd = true;
showLoader(true, 'End of content');
setTimeout(() => showLoader(false), 2000);
return;
}
const dest = findPostContainer(document);
const frag = document.createDocumentFragment();
newPosts.forEach(node => frag.appendChild(document.importNode(node, true)));
dest.appendChild(frag);
// update page number
const match = url.match(/\/page\/(\d+)\/?/);
if (match) {
currentPageNum = Number(match[1]);
} else {
// if no number in fetched URL, try increment
currentPageNum = (Number(currentPageNum) || 1) + 1;
}
showPageLabel(currentPageNum);
// update cached next url from fetched doc (prefer authoritative)
nextUrlCache = detectNextUrl(doc);
// fallback: derive from fetched url if still null
if (!nextUrlCache) nextUrlCache = deriveNextFromUrl(url);
console.log(`Appended ${newPosts.length} items from ${url}`);
} catch (err) {
console.warn('Error loading next page:', err);
reachedEnd = true;
showLoader(true, 'End of content');
setTimeout(() => showLoader(false), 2000);
} finally {
loading = false;
if (!reachedEnd) showLoader(false);
}
}
// ---------- Scroll handler with debounce ----------
function onScrollDebounced() {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(async () => {
if (loading || reachedEnd) return;
const nearBottom = (window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 700);
if (!nearBottom) return;
if (!nextUrlCache) nextUrlCache = detectNextUrl(document);
if (!nextUrlCache) {
// last resort, try deriving from current page
nextUrlCache = deriveNextFromUrl(location.href);
}
if (nextUrlCache) {
await fetchAndAppend(nextUrlCache);
} else {
console.warn('Could not determine next page URL.');
reachedEnd = true;
}
}, 120); // small debounce
}
// ---------- Init ----------
function init() {
// set current page from location if present
const m = location.pathname.match(/\/page\/(\d+)\/?/);
if (m) currentPageNum = Number(m[1]);
else currentPageNum = 1;
// show initial label briefly so you know where you started
showPageLabel(currentPageNum, 900);
nextUrlCache = detectNextUrl(document) || deriveNextFromUrl(location.href);
window.addEventListener('scroll', onScrollDebounced, { passive: true });
// keep nextUrlCache updated if pagination area changes
try {
const pagArea = document.querySelector('.pagination, .nav-links, .page-numbers, .paging-navigation, .pagination-wrap');
if (pagArea) {
const mo = new MutationObserver(() => {
nextUrlCache = detectNextUrl(document);
});
mo.observe(pagArea, { childList: true, subtree: true });
}
} catch (e) { /* ignore */ }
console.log('%cTM Endless Scroll + Page Label initialized', 'color:lime');
}
init();
})();