Gallery Scroll Navigator

Automatically navigate pages for image sites based on user's scroll behavior.

  1. // ==UserScript==
  2. // @name Gallery Scroll Navigator
  3. // @namespace https://github.com/YukiteruDev
  4. // @version 1.35
  5. // @description Automatically navigate pages for image sites based on user's scroll behavior.
  6. // @author Yukiteru
  7. // @match https://hitomi.la/*
  8. // @match https://www.pixiv.net/*
  9. // @match https://nhentai.net/*
  10. // @match https://exhentai.org/*
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_deleteValue
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_unregisterMenuCommand
  16. // @grant GM_addValueChangeListener
  17. // @require https://greasyfork.org/scripts/470224-tampermonkey-config/code/Tampermonkey%20Config.js
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. function printLog(message) {
  25. console.log(`[Scroll Pager]: ${message}`);
  26. }
  27.  
  28. // Initialize the configuration
  29. const config_desc = {
  30. scrolls: {
  31. name: "Number of Scrolls to Next Page",
  32. processor: "int_range-1-10",
  33. value: 3, // Default value
  34. }
  35. };
  36. const config = new GM_config(config_desc);
  37.  
  38. let scrollCounter = 0;
  39. let progressBarBottom;
  40. let progressBarTop;
  41. let disablePaging = false;
  42.  
  43. // Function to create the progress bar element
  44. function createProgressBar(position = 'bottom') {
  45. const bar = document.createElement('div');
  46. bar.style.cssText = `
  47. position: fixed;
  48. left: 0;
  49. right: 0;
  50. height: 3px;
  51. background-color: red;
  52. z-index: 9999;
  53. transform-origin: center;
  54. transform: scaleX(0);
  55. transition: transform 0.3s ease;
  56. ${position === 'top' ? 'top' : 'bottom'}: 0;
  57. `;
  58. document.body.appendChild(bar);
  59. return bar;
  60. }
  61.  
  62. const siteDict = {
  63. hitomi: {
  64. host: 'hitomi.la',
  65. getPageButton(direction) {
  66. try {
  67. const pageContainer = [...document.querySelectorAll('.page-container li')];
  68. const currentPage = pageContainer.filter(i => i.textContent !== '...').find(i => i.children.length === 0);
  69. const targetLi = direction === 'next' ? currentPage.nextElementSibling : currentPage.previousElementSibling;
  70. return targetLi ? targetLi.querySelector('a') : null;
  71. } catch(e) {
  72. return null;
  73. }
  74. }
  75. },
  76. pixiv: {
  77. host: 'www.pixiv.net',
  78. getPageButton(direction) {
  79. const selector = `nav:has(button) > a:${direction === 'next' ? 'last' : 'first'}-child`
  80. const pageButton = document.querySelector(selector);
  81. return pageButton && !pageButton.hasAttribute('hidden') ? pageButton : null;
  82. }
  83. },
  84. nhentai: {
  85. host: 'nhentai.net',
  86. getPageButton(direction) {
  87. const selector = direction === 'next' ? '.next' : '.previous'
  88. const pageButton = document.querySelector(selector);
  89. return pageButton || null;
  90. }
  91. },
  92. exhentai: {
  93. host: 'exhentai.org',
  94. getPageButton(direction) {
  95. const selector = direction === 'next' ? 'a#dnext' : 'a#dprev'
  96. const pageButton = document.querySelector(selector);
  97. return pageButton || null;
  98. }
  99. },
  100. };
  101.  
  102. function getCurrentSite() {
  103. return Object.values(siteDict).find(site => location.host === site.host);
  104. }
  105.  
  106. function getCurrentPosition() {
  107. return window.innerHeight + window.scrollY;
  108. }
  109.  
  110. function loadNextPage() {
  111. printLog('Loading next page...');
  112. const site = getCurrentSite();
  113. const pageButton = site?.getPageButton('next');
  114. if (pageButton) pageButton.click();
  115. }
  116.  
  117. function loadPrevPage() {
  118. printLog('Loading previous page...');
  119. const site = getCurrentSite();
  120. const pageButton = site?.getPageButton('prev');
  121. if (pageButton) pageButton.click();
  122. }
  123.  
  124. function checkIsScrollingDown(event) {
  125. if (event.wheelDelta) {
  126. return event.wheelDelta < 0;
  127. }
  128. return event.deltaY > 0;
  129. }
  130.  
  131. function updateProgressBar(progress, bar) {
  132. const maxScrolls = config.get('scrolls');
  133. const scale = Math.min(progress / maxScrolls, 1);
  134. bar.style.transform = `scaleX(${scale})`;
  135. }
  136.  
  137. progressBarBottom = createProgressBar('bottom');
  138. progressBarTop = createProgressBar('top');
  139.  
  140. function resetScrollProgress() {
  141. scrollCounter = 0;
  142. updateProgressBar(0, progressBarBottom);
  143. updateProgressBar(0, progressBarTop);
  144. }
  145.  
  146. function checkIsBottom() {
  147. return (window.innerHeight + window.scrollY).toFixed() >= document.body.scrollHeight;
  148. }
  149.  
  150. function checkIsTop() {
  151. return window.scrollY === 0;
  152. }
  153.  
  154. function setWheelEvent() {
  155. document.addEventListener("wheel", event => {
  156. if (disablePaging) return;
  157.  
  158. const isBottom = checkIsBottom();
  159. const isTop = checkIsTop();
  160. const isScrollingDown = checkIsScrollingDown(event);
  161.  
  162. const site = getCurrentSite();
  163.  
  164. const isPagingTop = isTop && !isScrollingDown && site.getPageButton('prev');
  165. const isPagingBottom = isBottom && isScrollingDown && site.getPageButton('next');
  166.  
  167. if (isPagingTop || isPagingBottom) {
  168. scrollCounter++;
  169. const progressBar = isPagingTop ? progressBarTop : progressBarBottom;
  170. const loadPage = isPagingTop ? loadPrevPage : loadNextPage;
  171.  
  172. updateProgressBar(scrollCounter, progressBar);
  173. printLog(`Scrolls at ${isPagingTop ? 'top' : 'bottom'}: ${scrollCounter}`);
  174.  
  175. const maxScrolls = config.get('scrolls');
  176. if (scrollCounter >= maxScrolls) {
  177. disablePaging = true;
  178. resetScrollProgress();
  179. loadPage();
  180. }
  181. return;
  182. }
  183.  
  184. resetScrollProgress();
  185. });
  186. }
  187.  
  188. function checkPagination() {
  189. const site = getCurrentSite();
  190. if (!site) return;
  191.  
  192. const pager = site.getPageButton('next') || site.getPageButton('prev');
  193. if (!pager) return;
  194.  
  195. printLog('Pager detected');
  196. observer.disconnect();
  197. setWheelEvent();
  198. }
  199.  
  200. const observer = new MutationObserver(() => checkPagination());
  201. observer.observe(document.body, { childList: true, subtree: true });
  202.  
  203. window.navigation.addEventListener("navigate", () => disablePaging = false); // for ajax-paging sites
  204. })();