Sleazy Fork is available in English.

Rule34.xxx Viewer

Image viewer with keyboard navigation for rule34.xxx content.

  1. // ==UserScript==
  2. // @name Rule34.xxx Viewer
  3. // @description Image viewer with keyboard navigation for rule34.xxx content.
  4. // @namespace https://sleazyfork.org/en/scripts/463046-rule34-xxx-viewer
  5. // @author bliblux
  6. // @version 1.0.1
  7. // @license MIT
  8. // @match *://rule34.xxx/index.php?page=post&s=list*
  9. // ==/UserScript==
  10.  
  11. (async function () {
  12. console.log(`start script <${GM.info.script.name}> version <${GM.info.script.version}>`);
  13. let unloadedImageLinks = [];
  14. let imageCount = 0;
  15. let imageData = [];
  16. let imageTagLists = [];
  17. let viewerImageIndex = 0;
  18. let imageDataReady = false;
  19. initUnloadedImageLinks();
  20. initButtons();
  21. initViewer();
  22. initKeyboardEventHandler();
  23. initButtonCss();
  24. initViewerCss();
  25.  
  26. //----------------------------------------------------------------------------------------------------------------//
  27. //BUTTONS---------------------------------------------------------------------------------------------------------//
  28. //----------------------------------------------------------------------------------------------------------------//
  29. function initButtons() {
  30. // create a div above with buttons above the image board
  31. const parentNode = document.querySelector('div.content');
  32. const ButtonContainerDiv = document.createElement('div');
  33. ButtonContainerDiv.id = 'button-container';
  34. const buttons = Array.of(
  35. createLoadImagesButton(),
  36. createViewerStatusDisplay(),
  37. createViewerButton()
  38. );
  39. if (parentNode) {
  40. buttons.forEach(button => {
  41. parentNode
  42. .insertBefore(ButtonContainerDiv, parentNode.firstChild)
  43. .appendChild(button);
  44. })
  45. }
  46. createShortcutDisplay()
  47. }
  48.  
  49. function createViewerButton() {
  50. const viewerButton = document.createElement('button');
  51. viewerButton.id = 'viewer-button';
  52. viewerButton.innerText = 'Open Viewer';
  53. viewerButton.classList.add('button-disabled');
  54. viewerButton.addEventListener('click', function () {
  55. if (viewerButton.classList.contains('button-active')) {
  56. toggleViewer();
  57. }
  58. });
  59. return viewerButton;
  60. }
  61.  
  62. function createLoadImagesButton() {
  63. const loaderButton = document.createElement('button');
  64. loaderButton.id = 'loader-button';
  65. loaderButton.innerText = 'Load Images';
  66. loaderButton.title = 'Hold down to load images!';
  67. loaderButton.addEventListener('click', loadAllImages);
  68. return loaderButton;
  69. }
  70.  
  71. function createViewerStatusDisplay() {
  72. const statusDisplaySpan = document.createElement('span');
  73. statusDisplaySpan.id = 'viewer-status-display';
  74. statusDisplaySpan.innerText = 'Not Ready';
  75. return statusDisplaySpan;
  76. }
  77.  
  78. function createShortcutDisplay() {
  79. const parentNode = document.getElementById('button-container');
  80. const shortCutsHtml = `
  81. <div id="shortcut-display">
  82. <p><span>shift+Q: </span><span>load images</span></p>
  83. <p><span>shift+space: </span><span>toggle viewer</span></p>
  84. <p><span>arrow keys: </span><span>viewer navigation</span></p>
  85. </div>`
  86. parentNode.lastElementChild.insertAdjacentHTML('afterend', shortCutsHtml);
  87. }
  88.  
  89. function setViewerButtonToReady() {
  90. const viewerButton = document.getElementById('viewer-button');
  91. viewerButton.classList.replace('button-disabled', 'button-active');
  92. }
  93.  
  94. //----------------------------------------------------------------------------------------------------------------//
  95. //IMAGE-DATA------------------------------------------------------------------------------------------------------//
  96. //----------------------------------------------------------------------------------------------------------------//
  97. function initUnloadedImageLinks() {
  98. // get image source link from the image preview parent anchor element
  99. unloadedImageLinks = Array.from(document.querySelectorAll('img.preview'), (link, index) => ({
  100. url: link.parentNode.href,
  101. index: index
  102. }));
  103. imageCount = unloadedImageLinks.length;
  104. }
  105.  
  106. function fetchAndCreateImageData(index) {
  107. if (index === unloadedImageLinks.length) {
  108. setViewerStatusToReady();
  109. setViewerButtonToReady();
  110. return;
  111. }
  112.  
  113. if (index === 0) {
  114. imageData = [];
  115. imageTagLists = [];
  116. }
  117.  
  118. const indexedUrl = unloadedImageLinks[index];
  119. fetch(indexedUrl.url)
  120. .then(response => response.text())
  121. .then((imageHtmlString) => {
  122. createImageDataAndTagList(imageHtmlString, index);
  123. setTimeout(() => fetchAndCreateImageData(++index), 100)
  124. })
  125. .catch(error => {
  126. console.error('Failed to fetch an image page: ', error)
  127. })
  128. }
  129.  
  130. function createImageDataAndTagList(imageHtmlString, index) {
  131. const domParser = new DOMParser();
  132.  
  133. const document = domParser.parseFromString(imageHtmlString, 'text/html');
  134. const imageElement = document.getElementById('image');
  135. const videoElement = document.getElementById('gelcomVideoPlayer');
  136.  
  137. // all relevant tag list items for the viewer have a class except the .current-page list item
  138. const tagListItemNodes = document.querySelectorAll('li[class]:not(.current-page)');
  139. const tagListElements = Array.from(tagListItemNodes).map(node => {
  140. const element = document.createElement(node.tagName.toLowerCase())
  141. element.className = node.className;
  142. for (let child of node.children) {
  143. const childElement = document.createElement(child.tagName.toLowerCase());
  144. if (child.tagName.toLowerCase() === 'a') {
  145. childElement.setAttribute('href', child.getAttribute('href'));
  146. }
  147. childElement.innerHTML = child.innerHTML;
  148. childElement.className = child.className;
  149. element.appendChild(childElement);
  150. }
  151. return element;
  152. });
  153.  
  154. if (!imageData.some(object => object.index === index)) {
  155. if (videoElement) {
  156. imageData.push({
  157. image: videoElement,
  158. index: index
  159. });
  160. } else {
  161. imageData.push({
  162. image: imageElement,
  163. index: index
  164. });
  165. }
  166. imageTagLists.push({
  167. tagList: tagListElements,
  168. index: index
  169. })
  170. }
  171. }
  172.  
  173. function setViewerStatusToReady() {
  174. const statusDisplay = document.getElementById('viewer-status-display');
  175. statusDisplay.innerText = 'Ready';
  176. statusDisplay.className = 'viewer-status-ready';
  177. imageDataReady = true;
  178. }
  179.  
  180. function setViewerStatusToLoading() {
  181. const statusDisplay = document.getElementById('viewer-status-display');
  182. statusDisplay.innerText = 'Loading';
  183. statusDisplay.className = 'viewer-status-loading';
  184. }
  185.  
  186. //----------------------------------------------------------------------------------------------------------------//
  187. //KEYBOARD-EVENTS-------------------------------------------------------------------------------------------------//
  188. //----------------------------------------------------------------------------------------------------------------//
  189. function initKeyboardEventHandler() {
  190. window.addEventListener('keydown', function (event) {
  191. if (event.shiftKey && event.code === 'KeyQ') {
  192. loadAllImages();
  193. }
  194. if (event.shiftKey && event.code === 'Space') {
  195. toggleViewer();
  196. }
  197. if (event.code === 'ArrowUp') {
  198. increaseVideoVolume();
  199. }
  200. if (event.code === 'ArrowDown') {
  201. decreaseVideoVolume();
  202. }
  203. if (event.code === 'ArrowRight') {
  204. forwardVideo();
  205. }
  206. if (event.code === 'ArrowLeft') {
  207. rewindVideo();
  208. }
  209. if (event.code === 'PageUp') {
  210. displayPreviousImage();
  211. }
  212. if (event.code === 'PageDown') {
  213. displayNextImage();
  214. }
  215. });
  216. }
  217.  
  218. function loadAllImages() {
  219. setViewerStatusToLoading();
  220. fetchAndCreateImageData(0);
  221. }
  222.  
  223. //----------------------------------------------------------------------------------------------------------------//
  224. //VIEWER----------------------------------------------------------------------------------------------------------//
  225. //----------------------------------------------------------------------------------------------------------------//
  226. function initViewer() {
  227. // create and add elements that will act as the image viewer when active
  228. const containerDiv = document.createElement('div');
  229. containerDiv.id = 'viewer-container';
  230. containerDiv.classList.add('viewer-container');
  231. containerDiv.classList.add('viewer-inactive');
  232. const viewerHtml = `
  233. <div id="viewer-tag-list" class="viewer-tag-list"></div>
  234. <div id="viewer-image-display" class="viewer-image-display">
  235. <img id="viewer-image" src="" alt=""></div>
  236. <div id="viewer-footer-navigation" class="viewer-footer-navigation">
  237. <button id="viewer-navigation-button-previous" class="viewer-navigation-button">Previous</button>
  238. <button id="viewer-navigation-button-source" class="viewer-navigation-button">Source</button>
  239. <button id="viewer-navigation-button-index" class="viewer-navigation-button">--</button>
  240. <button id="viewer-navigation-button-close" class="viewer-navigation-button">Close</button>
  241. <button id="viewer-navigation-button-next" class="viewer-navigation-button">Next</button></div>
  242. `;
  243. containerDiv.insertAdjacentHTML('beforeend', viewerHtml);
  244. document.body.appendChild(containerDiv);
  245. document.getElementById('viewer-navigation-button-close').addEventListener('click', toggleViewer);
  246. document.getElementById('viewer-navigation-button-next').addEventListener('click', displayNextImage);
  247. document.getElementById('viewer-navigation-button-previous').addEventListener('click', displayPreviousImage);
  248. document.getElementById('viewer-navigation-button-source').addEventListener('click', navigateToImageSource);
  249. }
  250.  
  251. function toggleViewer() {
  252. const viewerDiv = document.querySelector('div.viewer-container');
  253. if (!imageDataReady) {
  254. return;
  255. }
  256. if (viewerDiv.classList.contains('viewer-inactive')) {
  257. viewerDiv.classList.replace('viewer-inactive', 'viewer-active');
  258. displayImage();
  259. } else {
  260. viewerDiv.classList.replace('viewer-active', 'viewer-inactive');
  261. pauseVideo();
  262. }
  263. }
  264.  
  265. function displayImage() {
  266. // get image of current index and update the viewer image accordingly
  267. const viewerImage = document.getElementById('viewer-image');
  268. const currentImage = imageData.find(image => image.index === viewerImageIndex).image;
  269.  
  270. if (viewerImage.tagName !== currentImage.tagName) {
  271. const newImage = document.createElement(currentImage.tagName);
  272. if (currentImage.tagName === 'VIDEO') {
  273. newImage.id = 'viewer-image';
  274. newImage.src = currentImage.firstElementChild.src;
  275. newImage.controls = true;
  276. newImage.loop = true
  277. } else if (currentImage.tagName === 'IMG') {
  278. newImage.id = 'viewer-image';
  279. newImage.src = currentImage.src;
  280. newImage.alt = currentImage.alt;
  281. }
  282. viewerImage.replaceWith(newImage);
  283. } else if (currentImage.tagName === 'VIDEO') {
  284. viewerImage.src = currentImage.firstElementChild.src;
  285. } else if (currentImage.tagName === 'IMG') {
  286. viewerImage.src = currentImage.src;
  287. viewerImage.alt = currentImage.alt;
  288. }
  289. playVideo();
  290.  
  291. // fill the viewer tag list with the current items
  292. const viewerTagList = document.getElementById('viewer-tag-list');
  293. viewerTagList.innerHTML = '<h4 style="color:#a0a0a0;">Tags</h4>';
  294. const imageTagList = imageTagLists.find(tagList => tagList.index === viewerImageIndex).tagList;
  295. imageTagList.forEach(tagList => viewerTagList.appendChild(tagList));
  296.  
  297. // set index indicator of the viewer
  298. document.getElementById('viewer-navigation-button-index').innerText = (viewerImageIndex + 1).toString();
  299. }
  300.  
  301. function playVideo() {
  302. const image = document.getElementById('viewer-image');
  303. if (image.tagName === 'VIDEO') {
  304. image.volume = 0.05;
  305. image.play();
  306. }
  307. }
  308.  
  309. function pauseVideo() {
  310. const image = document.getElementById('viewer-image');
  311. if (image.tagName === 'VIDEO') {
  312. image.pause();
  313. }
  314. }
  315.  
  316. function increaseVideoVolume() {
  317. const image = document.getElementById('viewer-image');
  318. if (image.tagName === 'VIDEO') {
  319. image.volume += 0.05;
  320. }
  321. }
  322.  
  323. function decreaseVideoVolume() {
  324. const image = document.getElementById('viewer-image');
  325. if (image.tagName === 'VIDEO') {
  326. image.volume -= 0.05;
  327. }
  328. }
  329.  
  330. function forwardVideo() {
  331. const image = document.getElementById('viewer-image');
  332. if (image.tagName === 'VIDEO') {
  333. image.currentTime += 2;
  334. }
  335. }
  336.  
  337. function rewindVideo() {
  338. const image = document.getElementById('viewer-image');
  339. if (image.tagName === 'VIDEO') {
  340. image.currentTime -= 2;
  341. }
  342. }
  343.  
  344. function displayNextImage() {
  345. viewerImageIndex = viewerImageIndex < imageCount ? viewerImageIndex + 1 : 1;
  346. displayImage();
  347. }
  348.  
  349. function displayPreviousImage() {
  350. viewerImageIndex = viewerImageIndex > 1 ? viewerImageIndex - 1 : imageCount;
  351. displayImage();
  352. }
  353.  
  354. function navigateToImageSource() {
  355. window.open(document.getElementById('viewer-image').src, '_blank');
  356. }
  357.  
  358. //----------------------------------------------------------------------------------------------------------------//
  359. //CSS-CLASSES-----------------------------------------------------------------------------------------------------//
  360. //----------------------------------------------------------------------------------------------------------------//
  361. function addCssToHead(css, styleId) {
  362. const style = document.createElement('style');
  363. if (styleId) {
  364. style.setAttribute('id', styleId);
  365. }
  366. style.appendChild(document.createTextNode(css));
  367. return document.head.appendChild(style);
  368. }
  369.  
  370. function initButtonCss() {
  371. addCssToHead(`
  372. #button-container {
  373. display: flex;
  374. justify-content: center;
  375. align-items: center;
  376. }
  377. #button-container button {
  378. width: 180px;
  379. height: 50px;
  380. text-align: center;
  381. background-color: #84AE83;
  382. color: #FFFFFF;
  383. font-weight: bold;
  384. border: none;
  385. margin: 3px 10px;
  386. cursor: pointer;
  387. }
  388. #shortcut-display {
  389. display: flex;
  390. justify-content: center;
  391. align-items: center;
  392. display: inline-block;
  393. height: 50px;
  394. width: 180px;
  395. }
  396. #shortcut-display p {
  397. display: flex;
  398. justify-content: space-between;
  399. align-items: center;
  400. text-align: start;
  401. font-size: 10px;
  402. height: 1em;
  403. color: #666;
  404. }
  405. #viewer-container button:hover, #button-container button:hover {
  406. background: #A4CEA3;
  407. color: #FFFFFF;
  408. }
  409. #button-container button.button-disabled {
  410. background: #C0C0C0;
  411. color: #808080;
  412. cursor: default;
  413. }
  414. #button-container button.button-active {
  415. background: #84AE83;
  416. color: #FFFFFF;
  417. cursor: pointer;
  418. }
  419. #viewer-status-display {
  420. display: inline-block;
  421. width: 500px;
  422. height: 50px;
  423. line-height: 50px;
  424. text-align: center;
  425. background-color: red;
  426. color: white;
  427. font-weight: bold;
  428. border: none;
  429. margin: 3px 10px;
  430. user-select: none;
  431. pointer-events: none;
  432. }
  433. #viewer-status-display.viewer-status-loading {
  434. color: black;
  435. background-color: white;
  436. transition: background-color 0s linear;
  437. animation: turnYellow 2s linear forwards;
  438. }
  439. #viewer-status-display.viewer-status-ready {
  440. background-color: white;
  441. transition: background-color 0s linear;
  442. animation: turnGreen 2s linear forwards;
  443. }
  444. @keyframes turnYellow {
  445. 0% {
  446. background-color: white;
  447. }
  448. 50% {
  449. background-color: yellow;
  450. }
  451. 100% {
  452. background-color: yellow;
  453. }
  454. }
  455. @keyframes turnGreen {
  456. 0% {
  457. background-color: white;
  458. }
  459. 50% {
  460. background-color: green;
  461. }
  462. 100% {
  463. background-color: green;
  464. }
  465. }
  466. }
  467. `, 'button-div-css');
  468. }
  469.  
  470. function initViewerCss() {
  471. addCssToHead(`
  472. .viewer-container {
  473. position: fixed;
  474. top: 0;
  475. right: 0;
  476. bottom: 0;
  477. left: 0;
  478. z-index: 100100;
  479. background-color: #000000;
  480. }
  481. .viewer-inactive {
  482. display: none;
  483. }
  484. .viewer-active {
  485. display: block;
  486. }
  487. button.viewer-navigation-button {
  488. cursor: pointer;
  489. }
  490. .viewer-tag-list {
  491. position: absolute;
  492. width: 200px;
  493. min-width: 50px;
  494. top: 0;
  495. left: 0;
  496. overflow-y: auto;
  497. height: 100%;
  498. background-color: #303030;
  499. opacity: 0.7;
  500. transition: all 0.5s;
  501. }
  502. .viewer-tag-list:hover {
  503. opacity: 1;
  504. }
  505. .viewer-tag-list a:hover {
  506. color: #FFFFFF;
  507. }
  508. .viewer-tag-list li {
  509. list-style-type: none;
  510. line-height: 1.8em;
  511. display: block;
  512. padding-left: 4px;
  513. }
  514. .viewer-tag-list * {
  515. background-color: #303030;
  516. }
  517. .viewer-tag-list li > * {
  518. margin-right: 0.4em;
  519. }
  520. .viewer-tag-list li > span {
  521. color: #a0a0a0;
  522. }
  523. .viewer-tag-list li.tag-type-general a{
  524. color: #337ab7;
  525. }
  526. .viewer-image-display {
  527. position: absolute;
  528. top: 0;
  529. left: 200px;
  530. right: 0;
  531. bottom: 21px;
  532. display: flex;
  533. justify-content: center;
  534. align-items: center;
  535. }
  536. .viewer-image-display * {
  537. width: 100%;
  538. height: 100%;
  539. object-fit: contain;
  540. }
  541. .viewer-footer-navigation {
  542. position: absolute;
  543. bottom: 0px;
  544. left: 200px;
  545. width: calc(100% - 200px);
  546. height: 21px;
  547. display: flex;
  548. justify-content: center;
  549. align-items: center;
  550. background-color: #000000;
  551. opacity: 0.2;
  552. }
  553. .viewer-footer-navigation:hover {
  554. opacity: 1;
  555. }
  556. .viewer-footer-navigation button {
  557. color: #FFFFFF;
  558. background-color: #303030;
  559. cursor: initial;
  560. margin: 1px 1px 3px 1px;
  561. padding: 1px 5px;
  562. border: 0;
  563. font-weight: bold;
  564. }
  565. `, 'viewer-css');
  566. }
  567. })();