Next Image/Previous Image with Alignment and Large Image Support

Scroll down/up by aligning images

  1. // ==UserScript==
  2. // @name Next Image/Previous Image with Alignment and Large Image Support
  3. // @author rekt
  4. // @license MIT
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.0
  7. // @description Scroll down/up by aligning images
  8. // @include *
  9. // @grant unsafeWindow
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. const KEY_W = "w";
  14. const KEY_S = "s";
  15.  
  16. let positions = [];
  17.  
  18. function findNonTruncatedPosition(initialPosition, viewportHeight) {
  19. let currentPosition = initialPosition;
  20. let found = false;
  21. while (!found) {
  22. found = true;
  23. for (let i = 0; i < positions.length; i++) {
  24. const [, imgTop, imgBottom] = positions[i];
  25. if (imgTop < currentPosition && imgBottom > currentPosition) {
  26. currentPosition = imgTop;
  27. found = false;
  28. break;
  29. }
  30. }
  31. }
  32. return currentPosition;
  33. }
  34.  
  35. function scrollToNextImage(currentScroll, viewportHeight) {
  36. const nextScrollPosition = currentScroll + viewportHeight;
  37. const currentBottomEdge = currentScroll + viewportHeight;
  38.  
  39. // First check for large images that extend beyond viewport
  40. for (let i = 0; i < positions.length; i++) {
  41. if (positions[i][1] <= currentScroll && positions[i][2] > currentBottomEdge) {
  42. // Large image case: find next image after this one
  43. for (let j = i + 1; j < positions.length; j++) {
  44. if (positions[j][1] > positions[i][1]) {
  45. return findNonTruncatedPosition(positions[j][1], viewportHeight);
  46. }
  47. }
  48. // If no next image found, scroll by viewport height
  49. return findNonTruncatedPosition(nextScrollPosition, viewportHeight);
  50. }
  51. }
  52.  
  53. // Normal case: handle truncated images
  54. for (let i = 0; i < positions.length; i++) {
  55. if (positions[i][2] > currentBottomEdge && positions[i][1] < nextScrollPosition) {
  56. return findNonTruncatedPosition(positions[i][1], viewportHeight);
  57. }
  58. }
  59.  
  60. return findNonTruncatedPosition(nextScrollPosition, viewportHeight);
  61. }
  62.  
  63. function scrollToPreviousImage(currentScroll, viewportHeight) {
  64. const previousScrollPosition = currentScroll - viewportHeight;
  65. const newTopCandidate = Math.max(0, previousScrollPosition);
  66.  
  67. for (let i = positions.length - 1; i >= 0; i--) {
  68. if (positions[i][1] < currentScroll && positions[i][2] > newTopCandidate) {
  69. return findNonTruncatedPosition(positions[i][1], viewportHeight);
  70. }
  71. }
  72. return findNonTruncatedPosition(newTopCandidate, viewportHeight);
  73. }
  74.  
  75. function scrollShiftUp(currentScroll, viewportHeight) {
  76. const prevViewportTop = Math.max(currentScroll - viewportHeight, 0);
  77. let newScroll = prevViewportTop;
  78.  
  79. for (let i = positions.length - 1; i >= 0; i--) {
  80. const imgTop = positions[i][1];
  81.  
  82. if (imgTop >= prevViewportTop && imgTop <= prevViewportTop + 20) {
  83. newScroll = imgTop;
  84. break;
  85. }
  86.  
  87. if (imgTop < prevViewportTop && imgTop >= prevViewportTop - 100) {
  88. newScroll = imgTop;
  89. break;
  90. }
  91. }
  92.  
  93. return findNonTruncatedPosition(newScroll, viewportHeight);
  94. }
  95.  
  96. function getYOffset(node) {
  97. let offset = 0;
  98. while (node) {
  99. offset += node.offsetTop;
  100. node = node.offsetParent;
  101. }
  102. return offset;
  103. }
  104.  
  105. document.addEventListener("keydown", function(event) {
  106. if (event.ctrlKey || event.altKey) return;
  107.  
  108. const tagName = event.target.tagName;
  109. const contentEditable = event.target.getAttribute("contenteditable");
  110. if ((tagName && tagName.match(/input|select|textarea/i)) || contentEditable === "true") {
  111. return;
  112. }
  113.  
  114. positions = [];
  115. const allImages = document.images;
  116. for (let idx = 0; idx < allImages.length; idx++) {
  117. const img = allImages[idx];
  118. if (img.width * img.height < 80 * 80) continue;
  119. const ytop = getYOffset(img);
  120. const ybottom = ytop + img.height;
  121. positions.push([idx, ytop, ybottom]);
  122. }
  123. positions.sort((a, b) => a[1] - b[1]);
  124.  
  125. const currentScroll = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
  126. const viewportHeight = window.innerHeight;
  127.  
  128. let newScrollPosition = currentScroll;
  129.  
  130. const key = event.key;
  131. const lowerKey = key.toLowerCase();
  132.  
  133. if (lowerKey === KEY_S && !event.shiftKey) {
  134. newScrollPosition = scrollToNextImage(currentScroll, viewportHeight);
  135. } else if (lowerKey === KEY_W && !event.shiftKey) {
  136. newScrollPosition = scrollToPreviousImage(currentScroll, viewportHeight);
  137. } else if (lowerKey === KEY_W && event.shiftKey) {
  138. newScrollPosition = scrollShiftUp(currentScroll, viewportHeight);
  139. } else {
  140. return;
  141. }
  142.  
  143. if (newScrollPosition !== currentScroll) {
  144. event.preventDefault();
  145. document.body.scrollTop = newScrollPosition;
  146. document.documentElement.scrollTop = newScrollPosition;
  147. }
  148. });
  149. })();