Modal Image in rule

Add a modal image and thumbnails vertical-carrousel to rule34

  1. // ==UserScript==
  2. // @name Modal Image in rule
  3. // @namespace http://tampermonkey.net/
  4. // @version 6.4
  5. // @description Add a modal image and thumbnails vertical-carrousel to rule34
  6. // @author falaz
  7. // @match https://rule34.xxx/index.php?page=po*
  8. // @icon https://www.google.com/s2/favicons?domain=rule34.xxx
  9. // @grant none
  10. // ==/UserScript==
  11. /*jshint esversion: 6 */
  12. // @ts-check
  13. class Falaz {
  14. /**
  15. * Like querySelector, but small
  16. * @param {String} selector
  17. * @param {Element|Document} element
  18. * @returns {HTMLElement}
  19. */
  20. q(selector, element = document) {
  21. return element.querySelector(selector);
  22. }
  23. /**
  24. * Like querySelectorAll, but small
  25. * @param {String} selector
  26. * @param {Element|Document} element
  27. * @returns
  28. */
  29. qa(selector, element = document) {
  30. return element.querySelectorAll(selector);
  31. }
  32. }
  33.  
  34. class Modal {
  35. constructor(document, dp) {
  36. this.pointer = 0;
  37. this.dp = dp;
  38. this.infinityScroll = new InfinityScroll(document, dp);
  39. /**
  40. * @property {Media[]} medias
  41. */
  42. this.medias = this.infinityScroll.getMedias(document, 0);
  43. this.thumbsGallery = new ThumbsGallery(this.medias);
  44. this.createModalNode(document, this.thumbsGallery.render());
  45. this.bodyParent = F.q('body');
  46. this.visible = false;
  47. }
  48. createConfigBar(){
  49. return `<div id="menuModal" style="display: flex;">
  50. <div><input type="checkbox" id="night">Night Theme</div>
  51. <div><input type="checkbox" id="night">Only videos</div></div>`
  52. }
  53. createModalNode(document, extraDiv=null) {
  54. const div = document.createElement("div");
  55. const css = document.createElement("style");
  56. div.innerHTML =
  57. `<div id="modal-container" style="display:none"><div id="modal"></div>${extraDiv?extraDiv:''}${this.createButtons()}</div>`;
  58. css.innerHTML =
  59. "#modal-container{background: #000000a8;width: 100%;height: 100%;position: fixed;z-index: 10;}" +
  60. "#modal{height: 90%;width: 80%;background: transparent;padding: 0% 5%;margin: 2% 5% 2% 0;position: fixed}" +
  61. "#modal img{width: auto;border: none;vertical-align: middle;height: 100%;margin: 0 auto;display:block;}" +
  62. "#modal video{width: 100%;height:100%}"+
  63. "#modal-container #thumbGallery{float:right;overflow-y: scroll;height: 900px;} #modal-container #thumbGallery ul{list-style:none}"+
  64. "#modal-container .gallery-item{cursor: pointer;}"+
  65. '#navigationModal{margin-bottom: 0px;font-size: 20px;color: white;display: flex;justify-content: center;bottom: 0px;width: 100%; position:absolute}'+
  66. '#navigationModal svg:hover g g {fill: white;}'+
  67. '.fluid_video_wrapper video{cursor:default!important}'+
  68. '@media (max-width: 1024px) {#thumbGallery {display: none;} #modal{width:100%;padding:0%;overflow-x:scroll}}';
  69. if(debug){
  70. css.innerHTML+= "img,video{filter:blur(30px)}";
  71. }
  72. document.body.prepend(div);
  73. document.head.prepend(css);
  74. this.modalContainer = F.q("#modal-container");
  75. this.modal = F.q("#modal");
  76. this.infinityScroll.nextPage =this.infinityScroll.getNextPageHref(document);
  77. }
  78. createButtons(){
  79. return `<div id="navigationModal"">
  80. <div onclick={document.modalObj.prevMedia()}><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="52" height="52" viewBox="0 0 172 172" style=" fill:#000000;"><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,172v-172h172v172z" fill="none"></path><g fill="#1abc9c"><path d="M125.69231,19.84615h-79.38462c-14.54868,0 -26.46154,11.91286 -26.46154,26.46154v79.38462c0,14.54868 11.91286,26.46154 26.46154,26.46154h79.38462c14.54868,0 26.46154,-11.91286 26.46154,-26.46154v-79.38462c0,-14.54868 -11.91286,-26.46154 -26.46154,-26.46154zM112.46154,59.53846l-47.6256,26.46154l47.6256,26.46154v13.23077l-59.53846,-33.07692v-13.23077l59.53846,-33.07692z"></path></g></g></svg></div>
  81. <div onclick={document.modalObj.nextMedia()}><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="52" height="52" viewBox="0 0 172 172" style=" fill:#000000;"><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,172v-172h172v172z" fill="none"></path><g fill="#1abc9c"><path d="M125.69231,19.84615h-79.38462c-14.54868,0 -26.46154,11.91286 -26.46154,26.46154v79.38462c0,14.54868 11.91286,26.46154 26.46154,26.46154h79.38462c14.54868,0 26.46154,-11.91286 26.46154,-26.46154v-79.38462c0,-14.54868 -11.91286,-26.46154 -26.46154,-26.46154zM119.07692,92.61538l-59.53846,33.07692v-13.23077l47.6256,-26.46154l-47.6256,-26.46154v-13.23077l59.53846,33.07692z"></path></g></g></svg></div>
  82. <div onclick={document.modalObj.close()}><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="60" height="60" viewBox="0 0 172 172" style=" fill:#000000;"><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,172v-172h172v172z" fill="none"></path><g id="original-icon" fill="#1abc9c"><path d="M86,17.2c-37.9948,0 -68.8,30.8052 -68.8,68.8c0,37.9948 30.8052,68.8 68.8,68.8c37.9948,0 68.8,-30.8052 68.8,-68.8c0,-37.9948 -30.8052,-68.8 -68.8,-68.8zM112.9868,104.87987c2.24173,2.24173 2.24173,5.8652 0,8.10693c-1.118,1.118 -2.58573,1.67987 -4.05347,1.67987c-1.46773,0 -2.93547,-0.56187 -4.05347,-1.67987l-18.87987,-18.87987l-18.87987,18.87987c-1.118,1.118 -2.58573,1.67987 -4.05347,1.67987c-1.46773,0 -2.93547,-0.56187 -4.05347,-1.67987c-2.24173,-2.24173 -2.24173,-5.8652 0,-8.10693l18.87987,-18.87987l-18.87987,-18.87987c-2.24173,-2.24173 -2.24173,-5.8652 0,-8.10693c2.24173,-2.24173 5.8652,-2.24173 8.10693,0l18.87987,18.87987l18.87987,-18.87987c2.24173,-2.24173 5.8652,-2.24173 8.10693,0c2.24173,2.24173 2.24173,5.8652 0,8.10693l-18.87987,18.87987z"></path></g></g></svg></div>
  83. </div>`
  84. }
  85. /**
  86. * @param {Media} media
  87. */
  88. async render(media) {
  89. if (!this.modalContainer) {
  90. this.createModalNode(document,this.thumbsGallery.render());
  91. }
  92. if (media.src == "Retry") {
  93. await this.reloadMediaSrc(media);
  94. }
  95. if (media.type == "video") {
  96. this.modal.innerHTML = `<video src="${media.src}" ${debug?'':"autoplay"} controls loop id="modalVideo"></video>`;
  97. F.q('#modalVideo').volume = this.getCurrentVolume();
  98. F.q('#modalVideo').onvolumechange = e=>this.saveCurrentVolume(e.target.volume);
  99. fluidPlayer(document.querySelector('#modal video'),{
  100. layoutControls:{
  101. autoPlay:true,
  102. allowDownload:true,
  103. loop:true,
  104. fillToContainer: true
  105. }
  106. })
  107. F.q('#modal video').loop = true
  108. } else {
  109. this.modal.innerHTML = `<img src="${media.src}"/>`;
  110. }
  111. this.modalContainer.style.display = "block";
  112. this.visible = true;
  113. this.toggleBodyScroll(false);
  114. modalObj.scrollIntoCurrentMedia();
  115. }
  116.  
  117. toggleBodyScroll(visible){
  118. this.bodyParent.style.overflow = visible?'inherit':'hidden';
  119. }
  120.  
  121. getCurrentVolume(){
  122. return localStorage.volume? localStorage.volume: 0.0;
  123. }
  124. /**
  125. * Attach to event video.onvolumechange
  126. * @param {number} value
  127. */
  128. saveCurrentVolume(value){
  129. localStorage.volume = value;
  130. }
  131.  
  132. async getNextPage() {
  133. if (!this.infinityScroll.nextSended) {
  134. this.infinityScroll.nextSended = true;
  135. const docResponse = await this.infinityScroll.getNextPage();
  136. if (docResponse) {
  137. const medias = this.infinityScroll.getMedias(
  138. docResponse,
  139. this.medias.length
  140. );
  141. this.addMedias(medias);
  142. this.infinityScroll.nextSended = false;
  143. }else{
  144. c('The page requested is on the medias');
  145. }
  146. }
  147. }
  148.  
  149. close() {
  150. if (!this.modalContainer) {
  151. this.createModalNode(document);
  152. }
  153. try {
  154. this.modal.querySelector("video").pause();
  155. } catch (e) {}
  156. this.modalContainer.style.display = "none";
  157. this.toggleBodyScroll(true);
  158. this.visible = false;
  159. }
  160. nextMedia() {
  161. if (this.pointer < this.medias.length - 1) {
  162. this.pointer++;
  163. this.render(this.medias[this.pointer]);
  164. } else {
  165. this.getNextPage();
  166. this.pointer++;
  167. this.render(this.medias[this.pointer]);
  168. }
  169. }
  170. goToMedia(index){
  171. if(index<this.medias.length-1){
  172. this.pointer = index;
  173. this.render(this.medias[index])
  174. }
  175. }
  176. prevMedia() {
  177. if (this.pointer > 0) {
  178. this.pointer--;
  179. this.render(this.medias[this.pointer]);
  180. } else {
  181. console.error("Reach the start of the medias");
  182. }
  183. }
  184. getCurrentMedia(){
  185. this.medias[this.pointer];
  186. }
  187. getCurrentThumb(){
  188. return F.q(`span [data-index="${this.pointer}"]`).parentElement
  189. }
  190. /**
  191. *
  192. * @param {Media[]} medias
  193. */
  194. addMedias(medias) {
  195. const mediasTemp = [...this.medias, ...medias];
  196. // @ts-ignore
  197. this.medias = mediasTemp;
  198. this.thumbsGallery.updateMedias(mediasTemp);
  199. }
  200. async reloadMediaSrcFromMedias(index) {
  201. await this.medias[index].reloadSrc();
  202. }
  203. async reloadMediaSrc(media) {
  204. await media.reloadSrc();
  205. }
  206. scrollIntoCurrentMedia(){
  207. c('fired')
  208. this.getCurrentThumb().scrollIntoView();
  209. this.thumbsGallery.scrollToThumb(modalObj.pointer)
  210. }
  211. }
  212. class Media {
  213. /**
  214. * @param {string} _page This is the page of the media. Not the real src.
  215. * @param {string} _type
  216. * @param {string} _thumb
  217. */
  218. constructor(_page, _type, _thumb, dp) {
  219. this.page = _page;
  220. this.type = _type;
  221. this.thumb = _thumb;
  222. this.dp = dp;
  223. this.getSrc().then((src) => {
  224. this.src = src;
  225. });
  226. }
  227. async getSrc() {
  228. const response = await fetch(this.page);
  229. if ([202, 200].includes(response.status)) {
  230. const body = await response.text();
  231. const dp = new DOMParser();
  232. const pageDocument = dp.parseFromString(body, "text/html");
  233. const video = F.q("video source", pageDocument);
  234. const image = F.q(".flexi img", pageDocument);
  235. if (video) {
  236. this.type = "video";
  237. // @ts-ignore
  238. return video.src;
  239. } else {
  240. this.type = "image";
  241. // @ts-ignore
  242. return image.src;
  243. }
  244. } else {
  245. return "Retry";
  246. }
  247. }
  248. async reloadSrc() {
  249. this.src = await this.getSrc();
  250. }
  251. }
  252. class InfinityScroll {
  253. /**
  254. * @param {Document} document
  255. * @param {DOMParser} _dp
  256. */
  257. constructor(document, _dp) {
  258. this.nextSended = false;
  259. this.pagesParsed = [this.getPageNumber(document)];
  260. this.nextPage = this.getNextPageHref(document);
  261. this.dp = _dp;
  262. }
  263. /**
  264. * @param {Document} doc
  265. */
  266. getPageNumber(doc) {
  267. return parseInt(doc.querySelector("#paginator div b").innerHTML);
  268. }
  269. /**
  270. * @param {Document} doc
  271. */
  272. getNextPageHref(doc) {
  273. const nextNode = doc.querySelector('#paginator [alt="next"]');
  274. // @ts-ignore
  275. return nextNode ? nextNode.href : null;
  276. }
  277. async getNextPage() {
  278. c("getNextPage");
  279. if (!this.nextPage) {
  280. // @ts-ignore
  281. this.nextPage = F.q('#paginator [alt="next"]').href;
  282. }
  283. const response = await fetch(this.nextPage);
  284. if([204,202,200,201].includes(response.status)){
  285. const body = await response.text();
  286. const dp = new DOMParser();
  287. const pageDocument = dp.parseFromString(body, "text/html");
  288. const pageNum = this.getPageNumber(pageDocument);
  289. this.nextPage = this.getNextPageHref(pageDocument);
  290. if (!this.pagesParsed.includes(pageNum)) { // remove the page allready parsed
  291. this.pagesParsed.push(pageNum);
  292. return pageDocument;
  293. } else {
  294. return false;
  295. }
  296. }else{
  297. await this.sleep(500);
  298. return await this.getNextPage();
  299. }
  300. }
  301. sleep(ms){
  302. return new Promise(resolve=>setTimeout(resolve,ms));
  303. }
  304. /**
  305. *
  306. * @param {Document} document
  307. * @param {number} pad The length of medias in the Modal Object
  308. * @returns {Media[]}
  309. */
  310. getMedias(document, pad) {
  311. const thumbsNode = F.qa("#content .thumb", document);
  312. const medias = [];
  313. const nodes = [];
  314. //const pad = this.medias? this.medias.length : 0;
  315. for (let i = 0; i < thumbsNode.length; i++) {
  316. const node = thumbsNode[i];
  317. /**
  318. * @type {HTMLImageElement} img
  319. */
  320. // @ts-ignore
  321. const img = F.q("img", node);
  322. const title = img.title;
  323. const anchor = node.querySelector("a");
  324. const media = new Media(
  325. anchor.href,
  326. /animated|video/.test(title) ? "video" : "image",
  327. img.src,
  328. this.dp
  329. );
  330. anchor.dataset.index = (i + pad).toString();
  331. img.dataset.index = (i + pad).toString();
  332. //node.dataset.index = (i+pad).toString();
  333. medias.push(media);
  334. nodes.push(node);
  335. }
  336. this.addElementsToCurrentContentView(nodes); // async
  337. return medias;
  338. }
  339. async addElementsToCurrentContentView(nodes) {
  340. for (const node of nodes) {
  341. this.clickFunction(node);
  342. F.q(".content").insertBefore(node, F.q("#paginator"));
  343. }
  344. }
  345. clickFunction(element){
  346. element.addEventListener("click", (e) => {
  347. e.preventDefault();
  348. const index = e.target.dataset.index
  349. ? e.target.dataset.index
  350. : modalObj.pointer;
  351. modalObj.pointer = index;
  352. modalObj.render(modalObj.medias[index]);
  353. });
  354. }
  355. }
  356. class ThumbsGallery{
  357. /**
  358. * @param {Media[]} medias
  359. */
  360. constructor(medias){
  361. this.medias = medias;
  362. }
  363. /**
  364. * @param {number} index
  365. */
  366. scrollToThumb(index){
  367. F.q(`.gallery-item[data-index="${index}"]`).scrollIntoView();
  368. }
  369. /**
  370. * @param {Media[]} medias
  371. */
  372. updateMedias(medias){
  373. this.medias = medias;
  374. F.q('#thumbGallery').innerHTML = `<ul>${this.buildItems()}</ul>`;
  375. }
  376. render(){
  377. return `<div id="thumbGallery"><ul>${this.buildItems()}</ul></div>`
  378. }
  379. buildItems(){
  380. let li = ''
  381. this.medias.forEach((e,i)=>{
  382. li+=`<li class="gallery-item" data-index="${i}" onclick="{document.modalObj.goToMedia(${i})}","_blank")}">
  383. <img src="${e.thumb}"\\>
  384. </li>`
  385. })
  386. return li;
  387. }
  388.  
  389. }
  390. const dp = new DOMParser();
  391. const F = new Falaz();
  392. let modalObj;
  393. const loadingSVG = "https://samherbert.net/svg-loaders/svg-loaders/puff.svg";
  394. const debug = false;
  395. const c = (...e)=>{
  396. if(debug){
  397. console.log(e);
  398. }
  399. }
  400.  
  401. (function () {
  402. "use strict";
  403. modalObj = new Modal(document, dp);
  404. if(debug){document.title = 'New Tab'}
  405. // @ts-ignore
  406. document.modalObj = modalObj;
  407. F.qa(".content .thumb").forEach((element) => {
  408. modalObj.infinityScroll.clickFunction(element);
  409. });
  410. document.addEventListener("keydown", (e) => {
  411. if (e.key == "ArrowRight") {
  412. modalObj.nextMedia();
  413. modalObj.scrollIntoCurrentMedia();
  414. } else if (e.key == "ArrowLeft") {
  415. modalObj.prevMedia();
  416. modalObj.scrollIntoCurrentMedia();
  417. } else if (e.key == "Escape") {
  418. modalObj.close();
  419. }
  420. });
  421. // @ts-ignore
  422. document.addEventListener("scroll", (e) => {
  423. if (!modalObj.visible){
  424. const inner = window.innerHeight;
  425. if (
  426. window.scrollY + inner >
  427. document.documentElement.scrollHeight - inner * 2
  428. ) {
  429. modalObj.getNextPage();
  430. }
  431. }else{
  432. e.preventDefault();
  433. modalObj.scrollIntoCurrentMedia();
  434. }
  435. });
  436. })();