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;
})();