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 GGn Infinite Scroll // @description Infinitely scroll on paginated GGn pages // @updateURL https://openuserjs.org/meta/SB100/GGn_Infinite_Scroll.meta.js // @version 1.4.0 // @author SB100 // @copyright 2021, SB100 (https://openuserjs.org/users/SB100) // @license MIT // @match https://gazellegames.net/torrents.php* // @match https://gazellegames.net/collections.php* // @match https://gazellegames.net/requests.php* // @exclude https://gazellegames.net/requests.php?*action=view* // @match https://gazellegames.net/forums.php*action=viewforum* // @match https://gazellegames.net/user.php*action=inventory* // @match https://gazellegames.net/user.php?*action=userlog* // @match https://gazellegames.net/user.php?*action=crafteditems* // @grant GM_xmlhttpRequest // ==/UserScript== // ==OpenUserJS== // @author SB100 // ==/OpenUserJS== /* jshint esversion: 6 */ /** * Different pages have different table ids/classnames - lets set them up here */ let tableSelector; function setTableSelector() { const href = window.location.href; // HnR page / Seeding page if (href.includes('torrents.php') && (href.includes('type=viewseed') || href.includes('type=hitnrun'))) { tableSelector = '.torrent_history > table'; return true; } // potential HnR if (href.includes('torrents.php') && href.includes('type=potential_hnr')) { tableSelector = '#torrent_history_table'; return true; } // ignore individual torrent pages if (href.includes('torrents.php') && href.includes('id=')) { return false; } // torrent list page if (href.includes('torrents.php')) { tableSelector = '#torrent_table'; return true; } // collection individual view page if (href.includes('collections.php') && href.includes('id=')) { tableSelector = '#torrent_table'; return true; } // collection list page if (href.includes('collections.php')) { tableSelector = '.collections_art_display'; return true; } // request list page if (href.includes('requests.php')) { tableSelector = '#requests_list'; return true; } // forum thread view page if (href.includes('forums.php')) { tableSelector = '.forum_index'; return true; } // inventory page if (href.includes('user.php') && href.includes('action=inventory')) { tableSelector = '#items_list'; return true; } // userlog page if (href.includes('user.php') && href.includes('action=userlog')) { tableSelector = '.thin > table'; return true; } if (href.includes('user.php') && href.includes('action=crafteditems')) { tableSelector = '#content > table'; return true; } return false; } /** * 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; } /** * 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 all "rows" to "tableElement" */ function appendRowsToTable(tableElement, rows) { rows.forEach(row => { tableElement.appendChild(row); }); } /** * Find the "next" anchor element from the pagination element. */ function findNextLinkFromPagination(paginationElement) { 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, minus the column header const trs = Array.from(html.querySelectorAll(`${tableSelector} tr, ${tableSelector} li`)).filter(tr => tr.classList.contains('colhead') === false && tr.classList.contains('colhead_dark') === false); // get the next href link const nextHref = findNextLinkFromPagination(html.querySelector(`${tableSelector} + .linkbox`)); // append to the current table appendRowsToTable(document.querySelector(tableSelector), trs); // in the case of the inventory page, we also need to find the hidden dialog boxes, and append those const dialogs = Array.from(html.querySelectorAll(`.inventory + .hidden > div`)).filter(div => div.id !== 'titlepreview_dialog' && div.id !== 'avatarpreview_dialog'); appendRowsToTable(document.querySelector('.inventory + .hidden'), dialogs); // 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) }); } /** * Overwrite the current inventory dialog display box with one that works with infnite scroll as well */ function newItemInfo(id, title) { const itemId = document.querySelector(`form[data-itemname="${title}"]`).dataset.itemid; const dialogOptions = { "title": unsafeWindow.$('<div/>').html(title).text(), "width": 800, "height": 620, "show": { effect: 'fold', duration: 'fast' }, "hide": { effect: 'fold', duration: 'fast' }, "resizable": true, "draggable": true, "position": { my: 'center', at: 'center' }, "close": function () { unsafeWindow.$(this).dialog("close"); } }; const dialogExtendOptions = { "closable": true, "maximizable": true, "minimizable": true, "collapsable": false, "dblclick": "collapse", "icons": { "maximize": "ui-icon-arrow-4-diag" }, "load": function () { const $itemlink = document.createElement('a'); $itemlink.classList.add("ui-corner-all", "ui-state-default"); $itemlink.style.width = "19px"; $itemlink.style.height = "18px"; $itemlink.role = "button"; $itemlink.innerHTML = '<span class="ui-icon ui-icon-link">item link</span>'; $itemlink.href = `/shop.php?ItemID=${itemId}`; $itemlink.target = '_blank'; this.previousElementSibling.querySelector('.ui-dialog-titlebar-buttonpane').appendChild($itemlink); }, }; if (document.querySelector(`.hidden div[data-itemid="${itemId}"]`)) { unsafeWindow.$(`.hidden div[data-itemid="${itemId}"]`).dialog(dialogOptions).dialogExtend(dialogExtendOptions); } else { unsafeWindow.$('#' + id).dialog(dialogOptions).dialogExtend(dialogExtendOptions); } } /** * 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('[GGn Infinite Scroll] Not on a compatible page'); return; } // find the pagination element const linkBox = document.querySelector(`${tableSelector} + .linkbox`); // 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); // overwrite the item info function in the inventory to make it infinite scroll compatible unsafeWindow.ItemInfo = newItemInfo; })();