NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name BTN Infinite Scroll // @namespace https://openuserjs.org/users/SB100 // @description Infinitely scroll on paginated BTN pages // @version 1.0.0 // @author SB100 // @copyright 2023, SB100 (https://openuserjs.org/users/SB100) // @license MIT // @updateURL https://openuserjs.org/meta/SB100/BTN_Infinite_Scroll.meta.js // @match https://broadcasthe.net/torrents.php* // @match https://broadcasthe.net/tvnews.php* // @match https://broadcasthe.net/reviews.php* // @match https://broadcasthe.net/collages.php* // @match https://broadcasthe.net/requests.php* // @match https://broadcasthe.net/actorshowcase.php* // @match https://broadcasthe.net/recommend.php* // @match https://broadcasthe.net/forums.php* // @grant GM.xmlHttpRequest // ==/UserScript== // ==OpenUserJS== // @author SB100 // ==/OpenUserJS== /** * ============================= * ADVANCED OPTIONS * ============================= */ // will automatically open the reply box when you get to the bottom of the // infinite scroll list, if a reply box is available const SETTING_SHOW_REPLY_WHEN_AT_BOTTOM_OF_PAGE = false; /** * ============================= * END ADVANCED OPTIONS * DO NOT MODIFY BELOW THIS LINE * ============================= */ /** * Different pages have different table ids/classnames - lets set them up here */ let tableSelector; let rowSelector; let hasFloatingElement; function setTableSelector() { const { href } = window.location; // torrent browse page if (href.includes('torrents.php')) { tableSelector = '#torrent_table'; rowSelector = `${tableSelector} .torrent`; return true; } // tv news if (href.includes('tvnews.php')) { tableSelector = '.main_column'; rowSelector = `${tableSelector} .box`; return true; } // reviews if (href.includes('reviews.php')) { tableSelector = '.thin2'; rowSelector = `${tableSelector} .box`; return true; } // reviews if (href.includes('collages.php')) { tableSelector = '#torrent_table'; rowSelector = `${tableSelector} tr:not(.colhead)`; return true; } // requests if (href.includes('requests.php')) { tableSelector = '.thin > table.border'; rowSelector = `${tableSelector} .rowa, .rowb`; return true; } // actorshowcase if (href.includes('actorshowcase.php')) { tableSelector = '.thin > table'; rowSelector = `${tableSelector} > tbody > tr`; return true; } // recommend if (href.includes('recommend.php')) { tableSelector = '.thin .box > table'; rowSelector = `${tableSelector} tr:not(.colhead)`; return true; } // forums view forum if (href.includes('forums.php') && href.includes('forumid')) { tableSelector = '.thin > table'; rowSelector = `${tableSelector} .rowa, .rowb`; return true; } // forums view thread if (href.includes('forums.php') && href.includes('threadid')) { tableSelector = '.thin'; rowSelector = `${tableSelector} > table`; hasFloatingElement = true; return true; } return false; } /** * Simple throttle function */ const throttle = (func, limit) => { let lastFunc; let lastRan; return function inner() { const context = this; // eslint-disable-next-line prefer-rest-params const args = arguments; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(() => { if (Date.now() - lastRan >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; }; /** * Turn a HTML string into a HTML element so that we can run querySelector calls against it */ function htmlToElement(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); 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; } /** * 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 = Array.from( // misspelled on /recommend.php document.querySelectorAll('.linkbox, .linxbox') ).slice(-1)[0]; 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 - 10 ) { elem.classList.add('floating--slide'); window.onscroll = null; } } } /** * Makes the reply box a floating bar at the bottom of the page */ function setupFloatingElement() { if (hasFloatingElement) { const postReply = document.querySelector('h3'); const replyBox = document.querySelector('h3 + .box.pad'); const elem = document.createElement('div'); elem.appendChild(postReply); elem.appendChild(replyBox); document.querySelector('#content').appendChild(elem); const transformHeight = window.getComputedStyle(elem).height; elem.style.position = 'fixed'; elem.style.bottom = '0'; elem.style.left = '50%'; elem.style.transform = `translateY(${transformHeight}) translateY(-23px) translateX(-50%)`; elem.style.maxWidth = '960px'; 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 = elem.querySelector(`h3`); elemHeader.style.background = 'rgba(0, 0, 0, 0.7)'; elemHeader.style.padding = '10px'; elemHeader.style.margin = '0'; elemHeader.style.cursor = 'pointer'; elemHeader.onclick = () => { elem.classList.toggle('floating--slide'); }; if (SETTING_SHOW_REPLY_WHEN_AT_BOTTOM_OF_PAGE) { window.onscroll = throttle(handleScroll.bind(null, elem), 500); } const style = document.createElement('style'); style.innerHTML = `.floating--slide { transform: translateX(-50%) translateY(0)!important;}`; document.getElementsByTagName('head')[0].appendChild(style); } } /** * 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 newLinkBox = Array.from( // misspelled on /recommend.php html.querySelectorAll('.linkbox, .linxbox') ).slice(-1)[0]; const nextHref = findNextLinkFromPagination(newLinkBox); // 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, timeout: 5000, onloadstart() { 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 main() { // check we have observers available to us if (!IntersectionObserver) return; // check we're on a compatible page if (!setTableSelector()) { // eslint-disable-next-line no-console console.log('[BTN Infinite Scroll] Not on a compatible page'); return; } setupFloatingElement(); // find the last pagination element const linkBox = Array.from( // misspelled on /recommend.php document.querySelectorAll('.linkbox, .linxbox') ).slice(-1)[0]; // what to do when we find the element in the root element const callback = function callback(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); })();