NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @namespace https://openuserjs.org/users/SB100 // @name PTP Infinite Scroll // @description Infinitely scroll on paginated PTP pages // @updateURL https://openuserjs.org/meta/SB100/PTP_Infinite_Scroll.meta.js // @version 1.2.2 // @author SB100 // @copyright 2021, SB100 (https://openuserjs.org/users/SB100) // @license MIT // @match https://passthepopcorn.me/torrents.php*id=* // @match https://passthepopcorn.me/forums.php?*action=view* // @match https://passthepopcorn.me/collages.php* // @exclude https://passthepopcorn.me/collages.php*id=* // @match https://passthepopcorn.me/requests.php* // @exclude https://passthepopcorn.me/requests.php?*action=new* // @grant GM_xmlhttpRequest // ==/UserScript== // ==OpenUserJS== // @author SB100 // ==/OpenUserJS== /* jshint esversion: 6 */ const SETTING_SHOW_REPLY_WHEN_AT_BOTTOM_OF_PAGE = true; /** * Different pages have different table ids/classnames - lets set them up here */ let tableSelector; let rowSelector; let floatingElement; function setTableSelector() { const href = window.location.href; // individual movie page (torrent comments) if (href.includes('torrents.php')) { tableSelector = '#comments-container'; rowSelector = `${tableSelector} .forum-post`; floatingElement = '#reply_box'; return true; } // forum viewthread page if (href.includes('forums.php') && href.includes('threadid=')) { tableSelector = '.thin'; rowSelector = `${tableSelector} .forum_post`; floatingElement = '#reply_box'; return true; } // forum viewforum page if (href.includes('forums.php')) { tableSelector = '.forum_list'; rowSelector = `${tableSelector} tbody tr`; return true; } // main collections page if (href.includes('collages.php')) { tableSelector = '.table'; rowSelector = `${tableSelector} tbody tr`; return true; } // individual request page if (href.includes('requests.php') && href.includes('id=')) { tableSelector = '.main-column'; rowSelector = `${tableSelector} .forum-post`; floatingElement = '#reply_box'; return true; } // main requests page if (href.includes('requests.php')) { tableSelector = '#request_table'; rowSelector = `${tableSelector} tbody tr`; return true; } return false; } /** * Simple throttle function */ const throttle = (func, limit) => { let lastFunc let lastRan return function () { const context = this const args = arguments if (!lastRan) { func.apply(context, args) lastRan = Date.now() } else { clearTimeout(lastFunc) lastFunc = setTimeout(function () { if ((Date.now() - lastRan) >= limit) { func.apply(context, args) lastRan = Date.now() } }, limit - (Date.now() - lastRan)) } } } /** * Create a loading indicator to show the user we're doing something */ let loadingElementReference; function getLoadingElementReference() { if (!loadingElementReference) { loadingElementReference = document.createElement('div'); loadingElementReference.innerHTML = 'Loading ...'; loadingElementReference.style.color = '#fff'; loadingElementReference.style.lineHeight = '30px'; } return loadingElementReference; } /** * Checks if we're at the bottom of the page. If so, let's auto show the reply box */ function handleScroll(elem) { const linkBox = document.querySelector(`.pagination--bottom`); const nextAnchorElem = findNextLinkFromPagination(linkBox); // if there is no next page, no point in checking the scroll position if (!nextAnchorElem || nextAnchorElem.href.endsWith('null')) { if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) { elem.classList.add('floating--slide'); document.querySelector('.footer').classList.toggle('footer--extra-margin'); window.onscroll = null; } } } /** * Makes the reply box a floating bar at the bottom of the page */ function setupFloatingElement() { if (floatingElement) { const transformHeight = window.getComputedStyle(document.querySelector(`${floatingElement} .forum-post__avatar-and-body`)); const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = `.floating--slide { transform: translateY(0)!important; } .footer--extra-margin { margin-bottom: calc(${transformHeight.height} + 35px)}`; document.getElementsByTagName('head')[0].appendChild(style); const elem = document.querySelector(floatingElement); elem.style.position = 'fixed'; elem.style.bottom = 0; elem.style.transform = `translateY(${transformHeight.height})`; elem.style.maxWidth = '975px'; elem.style.margin = 0; elem.style.zIndex = 2; // set this a tick later so that we don't get the initial transition setTimeout(() => { elem.style.transition = 'transform 0.25s ease-out'; }, 1); const elemHeader = document.querySelector(`${floatingElement} .forum-post__heading`) elemHeader.style.cursor = 'pointer'; elemHeader.onclick = () => { elem.classList.toggle('floating--slide'); document.querySelector('.footer').classList.toggle('footer--extra-margin'); } if (SETTING_SHOW_REPLY_WHEN_AT_BOTTOM_OF_PAGE) { window.onscroll = throttle(handleScroll.bind(null, elem), 500); } } } /** * Turn a HTML string into a HTML element so that we can run querySelector calls against it */ function htmlToElement(html) { var template = document.createElement('template'); html = html.trim(); template.innerHTML = html; return template.content; } /** * Append new rows to the current rows */ function appendRows(lastRowSelector, newRows) { newRows.forEach(row => { const currRows = document.querySelectorAll(lastRowSelector); currRows[currRows.length - 1].insertAdjacentElement('afterEnd', row); }); } /** * Find the "next" anchor element from the pagination element. */ function findNextLinkFromPagination(paginationElement) { if (!paginationElement) return null; const nextAnchorArray = Array.from(paginationElement.querySelectorAll('a')).filter(a => a.innerText.startsWith('Next')); return nextAnchorArray.length > 0 ? nextAnchorArray[0] : null; } /** * On successful xmlRequest, process the results */ function xmlOnLoad(linkBox, successCallback, result) { // must be a successful page load if (result.status !== 200) { return; } // remove the loading indicator linkBox.removeChild(getLoadingElementReference()); // turn into html const html = htmlToElement(result.response); // get all the trs in the table body const trs = Array.from(html.querySelectorAll(rowSelector)).filter(tr => tr.classList.contains('forum-post--unread') === false); // get the next href link const nextHref = findNextLinkFromPagination(html.querySelector(`.pagination--bottom`)); // append to the current table appendRows(rowSelector, trs); // update old next href with new next href successCallback(nextHref); } /** * On a failed xmlRequest, show an error message and cancel the observer */ function xmlOnFailure(linkBox, failureCallback) { linkBox.removeChild(getLoadingElementReference()); const errorDiv = document.createElement('div'); errorDiv.innerHTML = 'There was an error loading the next page. Aborting infinite scroll'; errorDiv.style.color = '#ff3232'; errorDiv.style.lineHeight = '30px'; linkBox.insertBefore(errorDiv, linkBox.firstChild); failureCallback(); } /** * Loads the next page by: * - Loading the URL * - Turning the result into a HTML element * - Finding the results table and appending its rows to the current table * - Running a callback to update the "next" link to the "next" link thst was loaded from this call */ function loadNextPage(url, linkBox, successCallback, failureCallback) { GM_xmlhttpRequest({ method: 'get', url: url, timeout: 5000, onloadstart: function () { linkBox.insertBefore(getLoadingElementReference(), linkBox.firstChild); }, onload: xmlOnLoad.bind(null, linkBox, successCallback), onerror: xmlOnFailure.bind(null, linkBox, failureCallback), ontimeout: xmlOnFailure.bind(null, linkBox, failureCallback) }); } /** * Setup the intersection observer, and load the next page when we get to the bottom of the page */ (function () { 'use strict'; // check we have observers available to us if (!IntersectionObserver) return; // check we're on a compatible page if (!setTableSelector()) { console.log('[PTP Infinite Scroll] Not on a compatible page'); return; } setupFloatingElement(); // find the pagination element const linkBox = document.querySelector(`.pagination--bottom`); // what to do when we find the element in the root element const callback = function (entries, observer) { entries.forEach(entry => { // we're intersecting the pagination - find the next page and load it if (entry.isIntersecting !== true) { return } // find the "next" page link from the results const nextAnchorElem = findNextLinkFromPagination(entry.target); // if there is no next page, there is nothing more to do! if (!nextAnchorElem || nextAnchorElem.href.endsWith('null')) { // stop observing observer.unobserve(linkBox); // return early return; } // otherwise: // success: append the results of the next page to the current table and update "next" link // failure: stop observing the page - there are no more pages to load loadNextPage(nextAnchorElem.href, linkBox, (nextHref) => { // update the "next" link with the next "next" link! nextAnchorElem.href = nextHref; }, () => { // stop observing observer.unobserve(linkBox); }); }); }; const options = { root: document, rootMargin: '0px', threshold: 1.0 }; // create the observer const observer = new IntersectionObserver(callback, options); // Start observing the target node for mutations observer.observe(linkBox, options); })();