Unlimited Paginator Works

Makes any(?) page with a paginator on various Danbooru clones "bottomless"--blend pages together or separate each with a paginator.

  1. // ==UserScript==
  2. // @name Unlimited Paginator Works
  3. // @namespace https://greasyfork.org/scripts/5250
  4. // @description Makes any(?) page with a paginator on various Danbooru clones "bottomless"--blend pages together or separate each with a paginator.
  5. // @include *://behoimi.org/*
  6. // @include *://www.behoimi.org/*
  7. // @include *://*.donmai.us/*
  8. // @include *://konachan.tld/*
  9. // @include *://yande.re/*
  10. // @version 2022.06.25
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. //If true, each added page retains its paginator. If false, elements are smoothly joined together.
  15. var pageBreak = false;
  16.  
  17. //Minimum amount of window left to scroll, maintained by loading more pages.
  18. var scrollBuffer = 600;
  19.  
  20. //Time (in ms) the script will wait for a response from the next page before attempting to fetch the page again. If the script gets trapped in a loop trying to load the next page, increase this value.
  21. var timeToFailure = 15000;
  22.  
  23. //============================================================================
  24. //=========================Script initialization==============================
  25. //============================================================================
  26.  
  27. var nextPage, mainTable, mainParent, pending, timeout, iframe;
  28.  
  29. if( typeof(customF) != "undefined" )
  30. customF();
  31.  
  32. initialize();
  33. function initialize()
  34. {
  35. //Stop if inside an iframe
  36. if( window != window.top || scrollBuffer == 0 )
  37. return;
  38. //Stop if no "table"
  39. mainTable = getMainTable(document);
  40. if( !mainTable )
  41. {
  42. //console.log("UPW: No main table");
  43. return;
  44. }
  45. //Stop if no paginator
  46. var paginator = getPaginator(document);
  47. if( !paginator )
  48. {
  49. //console.log("UPW: No paginator found");
  50. return;
  51. }
  52. //Stop if no more pages
  53. nextPage = getNextPage(paginator);
  54. if( !nextPage )
  55. return;
  56. //Hide the blacklist sidebar, since this script breaks the tag totals and post unhiding.
  57. var sidebar = document.getElementById("blacklisted-sidebar");
  58. if( sidebar )
  59. sidebar.style.display = "none";
  60.  
  61. //Other important variables:
  62. scrollBuffer += window.innerHeight;
  63. mainParent = mainTable.parentNode;
  64. pending = false;
  65. iframe = document.createElement("iframe");
  66. iframe.width = iframe.height = 0;
  67. iframe.style.visibility = "hidden";
  68. document.body.appendChild(iframe);
  69.  
  70. //Slight delay so that Danbooru's initialize_edit_links() has time to hide all the edit boxes on the Comment index
  71. iframe.addEventListener("load", function(e){ setTimeout( appendNewContent, 100 ); }, false);
  72. //Stop if empty page
  73. if( /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(mainTable.innerHTML) )
  74. return;
  75.  
  76. //Add copy of paginator to the top
  77. mainParent.insertBefore( paginator.cloneNode(true), mainParent.firstChild );
  78.  
  79. if( !pageBreak )
  80. paginator.style.display = "none";//Hide bottom paginator
  81. else
  82. {
  83. //Reposition bottom paginator and add horizontal break
  84. mainTable.parentNode.insertBefore( document.createElement("hr"), mainTable.nextSibling );
  85. mainTable.parentNode.insertBefore( paginator, mainTable.nextSibling );
  86. }
  87. //Listen for scroll events
  88. window.addEventListener("scroll", testScrollPosition, false);
  89. testScrollPosition();
  90. }
  91.  
  92. //============================================================================
  93. //============================Script functions================================
  94. //============================================================================
  95.  
  96. //Some pages match multiple "tables", so order is important.
  97. function getMainTable(source)
  98. {
  99. var xpath =
  100. [
  101. ".//div[contains(@class,'posts-container') or contains(@class,'media-assets-container')]" // Danbooru (posts, ai_tags, uploads)
  102. ,".//div[@id='a-index']/table[not(contains(@class,'search'))]" // Danbooru (/forum_topics, ...), take care that this doesn't catch comments containing tables
  103. ,".//div[@id='a-index']" // Danbooru (/comments, ...)
  104. ,".//table[contains(@class,'highlight')]" // large number of pages
  105. ,".//div[contains(@id,'comment-list')]/div/.." // comment index
  106. ,".//*[not(contains(@id,'popular'))]/span[contains(@class,'thumb')]/a/../.." // post/index, pool/show, note/index
  107. ,".//li/div/a[contains(@class,'thumb')]/../../.." // post/index, note/index
  108. ,".//div[@id='content']//table/tbody/tr[contains(@class,'even')]/../.." // user/index, wiki/history
  109. ,".//div[@id='content']/div/table" // 3dbooru user records
  110. ,".//div[@id='forum']" // forum/show
  111. ];
  112. for( var i = 0; i < xpath.length; i++ )
  113. {
  114. getMainTable = (function(query){ return function(source)
  115. {
  116. return new XPathEvaluator().evaluate(query, source, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  117. }; })( xpath[i] );
  118. var result = getMainTable(source);
  119. if( result )
  120. {
  121. //console.log("UPW main table: "+xpath[i]+"\n\n"+location.pathname);
  122. return result;
  123. }
  124. }
  125. return null;
  126. }
  127.  
  128. function getPaginator( source )
  129. {
  130. var pager = new XPathEvaluator().evaluate("descendant-or-self::div[@id='paginator' or contains(@class,'paginator') or @id='paginater']", source, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  131. // Need clear:none to prevent the 2nd page from being pushed to below the sidebar on the Post index... but we don't want this when viewing a specific pool,
  132. // because then the paginator is shoved to the right of the last images on a page. Other sites have issues with clear:none as well, like //yande.re/post.
  133. if( pager && location.host.indexOf("donmai.") >= 0 && document.getElementById("sidebar") )
  134. pager.style.clear = "none";
  135. return pager;
  136. }
  137.  
  138. function getNextPage( source )
  139. {
  140. let page = getPaginator(source);
  141. if( page )
  142. page = new XPathEvaluator().evaluate(".//a[@alt='next' or @rel='next' or contains(text(),'>') or contains(text(),'Next')]", page, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  143. return( page && page.href );
  144. }
  145.  
  146. function testScrollPosition()
  147. {
  148. if( !nextPage )
  149. testScrollPosition = function(){};
  150. //Take the max of the two heights for browser compatibility
  151. else if( !pending && window.pageYOffset + scrollBuffer > Math.max( document.documentElement.scrollHeight, document.documentElement.offsetHeight ) )
  152. {
  153. pending = true;
  154. timeout = setTimeout( function(){pending=false;testScrollPosition();}, timeToFailure );
  155. iframe.contentDocument.location.replace(nextPage);
  156. }
  157. }
  158.  
  159. function appendNewContent()
  160. {
  161. //Make sure page is correct. Using 'indexOf' instead of '!=' because links like "https://danbooru.donmai.us/pools?page=2&search%5Border%5D=" become "https://danbooru.donmai.us/pools?page=2" in the iframe href.
  162. clearTimeout(timeout);
  163. if( nextPage.indexOf(iframe.contentDocument.location.href) < 0 )
  164. {
  165. setTimeout( function(){ pending = false; }, 1000 );
  166. return;
  167. }
  168. //Copy content from retrived page to current page, but leave off certain headers, labels, etc...
  169. var sourcePaginator = document.adoptNode( getPaginator(iframe.contentDocument) );
  170. var nextElem, deleteMe, source = document.adoptNode( getMainTable(iframe.contentDocument) );
  171. if( /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(source.innerHTML) )
  172. nextPage = null;
  173. else
  174. {
  175. nextPage = getNextPage(sourcePaginator);
  176.  
  177. if( pageBreak )
  178. mainParent.appendChild(source);
  179. else
  180. {
  181. //Hide elements separating one table from the next (h1 is used for user names on comment index)
  182. var rems = source.querySelectorAll("h2, h3, h4, thead, tfood");
  183. for( var i = 0; i < rems.length; i++ )
  184. rems[i].style.display = "none";
  185. //Move contents of next table into current one
  186. var fragment = document.createDocumentFragment();
  187. while( (nextElem = source.firstChild) )
  188. fragment.appendChild(nextElem);
  189. mainTable.appendChild(fragment);
  190. }
  191. }
  192.  
  193. //Add the paginator at the bottom if needed.
  194. if( !nextPage || pageBreak )
  195. mainParent.appendChild( sourcePaginator );
  196. if( pageBreak && nextPage )
  197. mainParent.appendChild( document.createElement("hr") );
  198. //Clear the pending request marker and check position again
  199. pending = false;
  200. testScrollPosition();
  201. }
  202.  
  203. // I am the code of my script.
  204. // HTML is my body, and JavaScript is my blood.
  205. // I have incorporated over a thousand paginators.
  206. // Unaware of loss.
  207. // Nor aware of gain.
  208. // Withstood boredom to include many pages,
  209. // Striving for the script's completion.
  210. // I have no regrets, this is the only path.
  211. // My whole life was "Unlimited Paginator Works."