Sleazy Fork is available in English.

Let's panda!

A login, view, download tool for exhentai & e-hentai

Stan na 02-07-2023. Zobacz najnowsza wersja.

  1. // ==UserScript==
  2. // @name Let's panda!
  3. // @namespace https://github.com/Sean2525/Let-s-panda
  4. // @author sean2525, strong-Ting
  5. // @description A login, view, download tool for exhentai & e-hentai
  6. // @description:zh-tw 一個用於exhentai和e-hentai的登入、查看、下載的工具
  7. // @description:zh-cn 一个用于exhentai和e-hentai的登录、查看、下载的工具
  8. // @license MIT
  9. // @require https://code.jquery.com/jquery-3.2.1.slim.min.js
  10. // @include https://exhentai.org/
  11. // @include https://exhentai.org/g/*
  12. // @include https://e-hentai.org/g/*
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js
  15. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM.xmlHttpRequest
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM.setValue
  21. // @grant GM.getValue
  22. // @grant GM_notification
  23. // @grant GM.notification
  24. // @connect *
  25. // @run-at document-end
  26. // @version 0.2.16
  27. // ==/UserScript==
  28.  
  29. jQuery(function ($) {
  30. /**
  31. * Output extension
  32. * @type {String} zip
  33. * cbz
  34. *
  35. * Tips: Convert .zip to .cbz
  36. * Windows
  37. * $ ren *.zip *.cbz
  38. * Linux
  39. * $ rename 's/\.zip$/\.cbz/' *.zip
  40. */
  41. var outputExt = "zip"; // or 'cbz'
  42.  
  43. /**
  44. * Multithreading
  45. * @type {Number} [1 -> 32]
  46. */
  47. var threading = 8;
  48.  
  49. /**
  50. * Logging
  51. * @type {Boolean}
  52. */
  53. var debug = false;
  54.  
  55.  
  56. var viewed = false;
  57. const loginPage = () => {
  58. let div = document.createElement("div");
  59. div.className = "main";
  60. let username = document.createElement("input");
  61. let style = document.createElement("style");
  62. style.innerHTML = `
  63. body {
  64. background-color: #212121;
  65. }
  66. .main {
  67. display: -webkit-flex;
  68. display: flex;
  69. -webkit-flex-direction: column;
  70. flex-direction: column;
  71. -webkit-align-items: center;
  72. align-items: center;
  73. -webkit-justify-content: center;
  74. justify-content: center;
  75. height: ${window.innerHeight}px;
  76. }
  77. .flex-center{
  78. display: -webkit-flex;
  79. display: flex;
  80. -webkit-align-items: center;
  81. align-items: center;
  82. -webkit-justify-content: center;
  83. justify-content: center;
  84. }
  85. form {
  86. display: -webkit-flex;
  87. display: flex;
  88. -webkit-flex-direction: column;
  89. flex-direction: column;
  90. -webkit-align-items: center;
  91. align-items: center;
  92. -webkit-justify-content: center;
  93. justify-content: center;
  94. }
  95. .image {
  96. position: relative;
  97. margin: 0;
  98. }
  99. .input {
  100. margin-top: 10px;
  101. display: block;
  102. height: 34px;
  103. padding: 6px 12px;
  104. font-size: 14px;
  105. line-height: 1.42857143;
  106. color: #555;
  107. background-color: #fff;
  108. background-image: none;
  109. border: 1px solid #ccc;
  110. border-radius: 4px;
  111. -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
  112. box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
  113. -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
  114. -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
  115. transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
  116. }
  117. .btn {
  118. color: #fff;
  119. background-color: #5cb85c;
  120. border-color: #4cae4c;
  121. margin-top: 10px;
  122. display: inline-block;
  123. font-weight: 400;
  124. line-height: 1.25;
  125. text-align: center;
  126. white-space: nowrap;
  127. vertical-align: middle;
  128. -webkit-user-select: none;
  129. -moz-user-select: none;
  130. -ms-user-select: none;
  131. user-select: none;
  132. border: 1px solid transparent;
  133. padding: .5rem 1rem;
  134. font-size: 1rem;
  135. border-radius: .25rem;
  136. -webkit-transition: all .2s ease-in-out;
  137. -o-transition: all .2s ease-in-out;
  138. transition: all .2s ease-in-out;
  139. }
  140. .btn:hover {
  141. background-color: #4da64d;
  142. }
  143. .btn-blue {
  144. color: #fff;
  145. background-color: #3832dd;
  146. border-color: #3832dd;
  147. display: inline-block;
  148. font-weight: 400;
  149. line-height: 1.0;
  150. text-align: center;
  151. white-space: nowrap;
  152. vertical-align: middle;
  153. -webkit-user-select: none;
  154. -moz-user-select: none;
  155. -ms-user-select: none;
  156. user-select: none;
  157. border: 1px solid transparent;
  158. padding: .5rem 1rem;
  159. font-size: 1rem;
  160. border-radius: .25rem;
  161. -webkit-transition: all .2s ease-in-out;
  162. -o-transition: all .2s ease-in-out;
  163. transition: all .2s ease-in-out;
  164. }
  165. .btn-blue:hover {
  166. background-color: #1c15c8;
  167. }
  168. `;
  169. $("head").append(style);
  170. const setCookie = (headers) => {
  171. //
  172. try {
  173. headers
  174. .split("\r\n")
  175. .find((x) => x.match("cookie"))
  176. .replace("set-cookie: ", "")
  177. .split("\n")
  178. .map(
  179. (x) =>
  180. (document.cookie = x.replace(".e-hentai.org", ".exhentai.org") + " secure")
  181. );
  182. } catch (err) {
  183. if (debug) console.log(err);
  184. }
  185. document.cookie =
  186. "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/; secure";
  187.  
  188. setTimeout(function () { window.location.reload() }, 3000);
  189. };
  190. const clearCookie = () => {
  191. if (debug) console.log("Clearning cookies");
  192. document.cookie =
  193. "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/; secure";
  194. window.location.reload();
  195. };
  196. let form = document.createElement("form");
  197. let login = document.createElement("button");
  198. let wrapper = document.createElement("div");
  199. let loadding = document.createElement("img");
  200. let password = document.createElement("input");
  201. username.placeholder = "Username"
  202. password.placeholder = "Password"
  203. let info = document.createElement("p");
  204. let error = document.createElement("p");
  205. info.innerHTML = `
  206. <center>
  207. If you can't log in, please visit the <a target="_blank" href="https://forums.e-hentai.org/index.php?act=Login&CODE=00" class='btn-blue'>Forums</a> and log in from there. <br >
  208. Please make sure you are logged in successfully and then click this <button class="clearCookie btn-blue">button</button>
  209. </center>
  210. `;
  211. info.style.color = "white";
  212. username.type = "text";
  213. username.className = "input";
  214. password.type = "password";
  215. password.className = "input";
  216. loadding.src =
  217. "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==";
  218. loadding.style.position = "relative";
  219. info.hidden = true;
  220. loadding.hidden = true;
  221. login.addEventListener("click", () => {
  222. loadding.hidden = false;
  223. GM.xmlHttpRequest({
  224. method: "POST",
  225. url: "https://forums.e-hentai.org/index.php?act=Login&CODE=01",
  226. data: `referer=https://forums.e-hentai.org/index.php?&b=&bt=&UserName=${username.value}&PassWord=${password.value}&CookieDate=1"}`,
  227. headers: {
  228. "Content-Type": "application/x-www-form-urlencoded",
  229. },
  230. onload: function (response) {
  231. if (debug) console.log(response);
  232. if (/You are now logged/.exec(response.responseText)) {
  233. error.style = "color:green";
  234. error.innerText = "Login succeeded: you will be redirected to exhentai.org in 3 seconds, if you can't access exhentai, don't use private browsing. "
  235. GM.notification("You will be redirected to exhentai.org in 3 seconds; if you can't access exhentai, don't use private browsing", "Login succeeded");
  236. setCookie(response.responseHeaders);
  237. } else if (/IF YOU DO NOT SEE THE CAPTCHA/.exec(response.responseText)) {
  238. error.style = "color:red";
  239. error.innerText = "Login failed: Please visit the forums directly and log in from there; reCaptcha has been enabled."
  240. }
  241. else {
  242. error.style = "color:red";
  243. error.innerText = "Login failed: Please check that your username and password are correct.";
  244. }
  245. info.hidden = false;
  246. loadding.hidden = true;
  247. },
  248. onerror: function (err) {
  249. console.error(err);
  250. error.style = "color:red";
  251. error.innerText("Login got error: Please contact me at https://github.com/MinoLiu/Let-s-panda/issues");
  252. loadding.hidden = true;
  253. },
  254. });
  255. });
  256. login.className = "btn";
  257. login.innerHTML = "Login";
  258. form.append(username);
  259. form.append(password);
  260. wrapper.className = "flex-center";
  261. wrapper.append(loadding);
  262. wrapper.append(login);
  263. form.append(wrapper);
  264. form.addEventListener("submit", (e) => {
  265. e.preventDefault();
  266. });
  267. var image = document.createElement("img");
  268. image.className = "image";
  269. image.src = "https://i.imgur.com/oX86mGf.png"
  270. div.append(image);
  271. div.append(form);
  272. div.append(error);
  273. div.append(info);
  274. $("body").append(div);
  275. $(".clearCookie").on("click", clearCookie);
  276. };
  277.  
  278. const downloadPage = () => {
  279. var zip = new JSZip(),
  280. doc = document,
  281. tit = doc.title,
  282. $win = $(window),
  283. loc = /https?:\/\/e[x-]hentai\.org\/g\/\d+\/\w+/.exec(doc.location.href)[0],
  284. prevZip = false,
  285. current = 0,
  286. images = [],
  287. total = 0,
  288. final = 0,
  289. failed = 0,
  290. hrefs = [],
  291. comicId = location.pathname.match(/\d+/)[0],
  292. download = document.createElement("p");
  293.  
  294. const dlImg = ({ index, url, _ }, success, error) => {
  295. var filename = url.replace(/.*\//g, "");
  296. var extension = filename.split(".").pop();
  297. filename = ("0000" + index).slice(-4) + "." + extension;
  298. if (debug) console.log(filename, "progress");
  299. GM.xmlHttpRequest({
  300. method: "GET",
  301. url: url,
  302. responseType: "arraybuffer",
  303. onload: function (response) {
  304. final++;
  305. success(response, filename);
  306. },
  307. onerror: function (err) {
  308. final++;
  309. error(err, filename);
  310. },
  311. });
  312. };
  313.  
  314. const next = () => {
  315. download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="#"> Downloading ${final}/${total}</a>`;
  316. if (debug) console.log(final, current);
  317. if (final < current) return;
  318. final < total ? addZip() : genZip();
  319. };
  320.  
  321. const end = () => {
  322. $win.off("beforeunload");
  323. if (failed > 0) {
  324. alert("Some pages download failed, please unzip and check!");
  325. }
  326. if (debug) console.timeEnd("eHentai");
  327. };
  328.  
  329. const genZip = () => {
  330. zip
  331. .generateAsync({
  332. type: "blob",
  333. })
  334. .then(function (blob) {
  335. var zipName =
  336. tit.replace(/\s/g, "_") + "." + comicId + "." + outputExt;
  337.  
  338. if (prevZip) window.URL.revokeObjectURL(prevZip);
  339. prevZip = blob;
  340.  
  341. saveAs(blob, zipName);
  342. if (debug) console.log("COMPLETE");
  343. download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="${window.URL.createObjectURL(
  344. prevZip
  345. )}" download="${zipName}"> Download completed!</a>`;
  346. end();
  347. });
  348. };
  349.  
  350. const addZip = () => {
  351. total = images.length;
  352. var max = current + threading;
  353. if (max > total) max = total;
  354. for (current; current < max; current++) {
  355. let _href = images[current];
  356. dlImg(
  357. _href,
  358. function (response, filename) {
  359. zip.file(filename, response.response);
  360. if (debug) console.log(filename, "image success");
  361. next();
  362. },
  363. function (err, filename) {
  364. final--;
  365. // retry backupUrl for once
  366. GM.xmlHttpRequest({
  367. method: "GET",
  368. url: _href.backupUrl,
  369. onload: function (response) {
  370. let imgNo = parseInt(
  371. response.responseText.match("startpage=(\\d+)").pop()
  372. );
  373. let img = new DOMParser()
  374. .parseFromString(response.responseText, "text/html")
  375. .querySelector("#img");
  376. if (debug) console.log(imgNo, "backupUrl success");
  377. _href.url = img.src;
  378. dlImg(
  379. _href,
  380. function (response, filename) {
  381. zip.file(filename, response.response);
  382. if (debug) console.log(filename, "backupUrl image success");
  383. next();
  384. },
  385. function (err, filename) {
  386. failed++;
  387. zip.file(
  388. filename + "_" + comicId + "_error.gif",
  389. "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=",
  390. {
  391. base64: true,
  392. }
  393. );
  394. if (debug) console.log(filename, "backupUrl image error");
  395. next();
  396. }
  397. );
  398. },
  399. onerror: function (err, filename) {
  400. dlImg(
  401. _href,
  402. function (response, filename) {
  403. zip.file(filename, response.response);
  404. if (debug) console.log(filename, "retry image success");
  405. next();
  406. },
  407. function (err, filename) {
  408. failed++;
  409. zip.file(
  410. filename + "_" + comicId + "_error.gif",
  411. "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=",
  412. {
  413. base64: true,
  414. }
  415. );
  416. if (debug) console.log(filename, "retry url error");
  417. next();
  418. }
  419. );
  420. }
  421. });
  422. }
  423. );
  424. }
  425. };
  426.  
  427. /**
  428. * Update image download status.
  429. */
  430. const getImageNext = () => {
  431. download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="#">Getting images ${final}/${hrefs.length}</a>`;
  432. if (debug) console.log(final, current);
  433. if (final < current) return;
  434. final < hrefs.length
  435. ? getImage()
  436. : (() => {
  437. current = 0;
  438. final = 0;
  439. addZip();
  440. })();
  441. };
  442.  
  443. /**
  444. * Get all images from hrefs.
  445. */
  446. const getImage = () => {
  447. let max = current + threading;
  448. if (max > hrefs.length) max = hrefs.length;
  449. for (current; current < max; current++) {
  450. if (debug) console.log(hrefs[current]);
  451. let href = hrefs[current];
  452. GM.xmlHttpRequest({
  453. method: "GET",
  454. url: hrefs[current],
  455. onload: function (response) {
  456. let imgNo = parseInt(
  457. response.responseText.match("startpage=(\\d+)").pop()
  458. );
  459. let img = new DOMParser()
  460. .parseFromString(response.responseText, "text/html")
  461. .querySelector("#img");
  462. if (debug) console.log(imgNo, "url success");
  463. let src = href + "?nl=" + /nl\(\'(.*)\'\)/.exec(img.attributes.onerror.value)[1];
  464. images.push({
  465. index: imgNo,
  466. url: img.src,
  467. backupUrl: src,
  468. });
  469. final++;
  470. getImageNext();
  471. },
  472. onerror: function (err) {
  473. final++;
  474. getImageNext();
  475. if (debug) console.log(err);
  476. },
  477. });
  478. }
  479. };
  480.  
  481. /**
  482. * Get the href of all images from all pages.
  483. */
  484. const getHref = () => {
  485. childNodes = document.querySelector("table[class=ptt] tbody tr")
  486. .childNodes;
  487. let page = parseInt(
  488. childNodes[childNodes.length - 2].textContent.replace(",", "")
  489. );
  490. for (let i = 0; i < page; i++) {
  491. GM.xmlHttpRequest({
  492. method: "GET",
  493. url: `${loc}?p=${i}`,
  494. onload: function (response) {
  495. if (debug)
  496. console.log(`page ${loc}?p=${i} detect ${response.responseText}`);
  497. let imgs = [
  498. ...new DOMParser()
  499. .parseFromString(response.responseText, "text/html")
  500. .querySelectorAll(".gdtm a"),
  501. ];
  502. if (!imgs.length)
  503. imgs = [
  504. ...new DOMParser()
  505. .parseFromString(response.responseText, "text/html")
  506. .querySelectorAll(".gdtl a"),
  507. ];
  508. if (!imgs.length) {
  509. alert(
  510. "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
  511. );
  512. }
  513. imgs.forEach((v) => {
  514. hrefs.push(v.href);
  515. });
  516. if (i == page - 1) {
  517. getImage();
  518. }
  519. },
  520. onerror: function (err) {
  521. download.innerHTML =
  522. '<span style="margin-left:10px;">▶</span> <a href="#">Get href failed</a>';
  523. if (i == page - 1) {
  524. getImage();
  525. }
  526. if (debug) console.log(err);
  527. },
  528. });
  529. }
  530. };
  531.  
  532. download.className = "g3";
  533. download.innerHTML = `<span style="margin-left:10px;">▶</span> <a class="panda_download" href="#">Download</a>`;
  534. $("#gd5").append(download);
  535. $(".panda_download").on("click", () => {
  536. if (threading < 1) threading = 1;
  537. if (threading > 32) threading = 32;
  538. if (debug) console.time("eHentai");
  539. $win.on("beforeunload", function () {
  540. return "Progress is running...";
  541. });
  542. download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="#">Start Download</a>`;
  543. getHref();
  544. });
  545. };
  546.  
  547.  
  548. function view() {
  549. viewed = true;
  550. if (threading < 1) threading = 1;
  551. if (threading > 32) threading = 32;
  552. var gdt = document.querySelector("#gdt");
  553. var gdd = document.querySelector("#gdd");
  554. var gdo4 = document.querySelector("#gdo4");
  555. let childNodes = document.querySelector("table[class=ptt] tbody tr")
  556. .childNodes;
  557. let lpPage = parseInt(
  558. childNodes[childNodes.length - 2].textContent.replace(",", "")
  559. );
  560.  
  561. var data = document
  562. .querySelector("body div.gtb p.gpc")
  563. .textContent.split(" ");
  564.  
  565. var minPic = parseInt(data[1].replace(",", ""));
  566. var maxPic = parseInt(data[3].replace(",", ""));
  567.  
  568. var imgNum = parseInt(
  569. gdd
  570. .querySelector("#gdd tr:nth-child(n+6) td.gdt2")
  571. .textContent.split(" ")[0]
  572. );
  573.  
  574.  
  575. viewer(lpPage, imgNum, minPic, maxPic);
  576.  
  577. async function viewer(lpPage, imgNum, minPic, maxPic) {
  578. var Gallery = function (pageNum, imgNum, minPic, maxPic) {
  579. this.pageNum = pageNum || 0;
  580. this.imgNum = imgNum || 0;
  581. this.loc = /https?:\/\/e[x-]hentai\.org\/g\/\d+\/\w+/.exec(location.href)[0];
  582. this.padding = false;
  583. this.current = 0;
  584. this.final = 0;
  585. };
  586. var viewAll = await GM.getValue("view_all", true);
  587. Gallery.prototype = {
  588. imgHref: [],
  589. imgList: [],
  590. retry: 0,
  591. getAllHref: function (nextID) {
  592. if (nextID >= this.pageNum) {
  593. this.loadNextImage();
  594. return;
  595. }
  596. var that = this;
  597. GM.xmlHttpRequest({
  598. method: "GET",
  599. url: `${this.loc}?p=${nextID}`,
  600. onload: function (response) {
  601. if (debug)
  602. console.log(`page ${that.loc}?p=${nextID} detect ${response.responseText}`);
  603. let imgs = [
  604. ...new DOMParser()
  605. .parseFromString(response.responseText, "text/html")
  606. .querySelectorAll(".gdtm a"),
  607. ];
  608. if (!imgs.length)
  609. imgs = [
  610. ...new DOMParser()
  611. .parseFromString(response.responseText, "text/html")
  612. .querySelectorAll(".gdtl a"),
  613. ];
  614. if (!imgs.length) {
  615. alert(
  616. "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
  617. );
  618. }
  619. imgs.forEach((v) => {
  620. that.imgHref.push(v.href);
  621. });
  622. that.getAllHref(nextID + 1);
  623. },
  624. onerror: function (err) {
  625. if (debug) console.log(err);
  626. that.retry++;
  627. if (that.retry > 2) {
  628. alert(`Page number ${nextID + 1} load failed for 3 times.`);
  629. that.getAllHref(nextID + 1);
  630. } else {
  631. that.getAllHref(nextID);
  632. }
  633. },
  634. });
  635. },
  636. getHref: function (pageID) {
  637. var that = this;
  638. GM.xmlHttpRequest({
  639. method: "GET",
  640. url: `${this.loc}?p=${pageID}`,
  641. onload: function (response) {
  642. if (debug)
  643. console.log(`page ${that.loc}?p=${pageID} detect ${response.responseText}`);
  644. let imgs = [
  645. ...new DOMParser()
  646. .parseFromString(response.responseText, "text/html")
  647. .querySelectorAll(".gdtm a"),
  648. ];
  649. if (!imgs.length)
  650. imgs = [
  651. ...new DOMParser()
  652. .parseFromString(response.responseText, "text/html")
  653. .querySelectorAll(".gdtl a"),
  654. ];
  655. if (!imgs.length) {
  656. alert(
  657. "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
  658. );
  659. }
  660. imgs.forEach((v) => {
  661. that.imgHref.push(v.href);
  662. });
  663. that.loadNextImage();
  664. },
  665. onerror: function (err) {
  666. if (debug) console.log(err);
  667. that.retry++;
  668. if (that.retry > 2) {
  669. alert(`Page number ${nextID + 1} load failed for 3 times.`);
  670. that.loadNextImage();
  671. } else {
  672. that.getHref(nextID);
  673. }
  674. },
  675. });
  676. },
  677. checkFunctional: function () {
  678. return (this.imgNum > 41 && this.pageNum < 2) || this.imgNum !== 0;
  679. },
  680. loadNextImage: function () {
  681. if (this.final < this.current) {
  682. return;
  683. }
  684. this.loadPageUrls();
  685. },
  686. onSucceed: async function (response, href) {
  687. let imgNo = parseInt(
  688. response.responseText.match("startpage=(\\d+)").pop()
  689. );
  690. let img = new DOMParser()
  691. .parseFromString(response.responseText, "text/html")
  692. .querySelector("#img");
  693. if (debug) console.log(imgNo, "success");
  694. let src = href + "?nl=" + /nl\(\'(.*)\'\)/.exec(img.attributes.onerror.value)[1];
  695. Gallery.prototype.imgList[imgNo - 1].setAttribute(
  696. "data-href",
  697. src
  698. );
  699.  
  700. let timeoutId;
  701. let timeoutDuration = 10000; // 10s
  702.  
  703. timeoutId = setTimeout(function () {
  704. // timeout trigger error
  705. Gallery.prototype.imgList[imgNo - 1].childNodes[0].dispatchEvent(new Event('error'));
  706. }, timeoutDuration);
  707.  
  708. $(Gallery.prototype.imgList[imgNo - 1].childNodes[0]).on("load", function () {
  709. // success clear timeoutId
  710. clearTimeout(timeoutId);
  711. });
  712.  
  713. $(Gallery.prototype.imgList[imgNo - 1].childNodes[0]).on(
  714. "error",
  715. function () {
  716. var ajax = new XMLHttpRequest();
  717. ajax.onreadystatechange = async function () {
  718. if (debug) {
  719. console.log(`Failed load ${Number(imgNo)}, getting backup image from ${src}.`);
  720. }
  721. if (4 == ajax.readyState && 200 == ajax.status) {
  722. var _imgNo = parseInt(
  723. ajax.responseText.match("startpage=(\\d+)").pop()
  724. );
  725. var imgDom = new DOMParser()
  726. .parseFromString(ajax.responseText, "text/html")
  727. .getElementById("img");
  728. Gallery.prototype.imgList[_imgNo - 1].childNodes[0].src =
  729. imgDom.src;
  730. }
  731. };
  732. ajax.open("GET", src);
  733. ajax.send(null);
  734. }
  735. );
  736.  
  737. Gallery.prototype.imgList[imgNo - 1].childNodes[0].src = img.src;
  738.  
  739. this.loadNextImage();
  740. },
  741. onFailed: function (err, href) {
  742. GM.xmlHttpRequest({
  743. method: "GET",
  744. url: href,
  745. responseType: "document",
  746. onload: function (response) {
  747. that.onSucceed(response, href);
  748. },
  749. onerror: function (err) {
  750. if (debug) console.log(err);
  751. this.loadNextImage();
  752. },
  753. });
  754. },
  755. loadPageUrls: function () {
  756. if (debug) {
  757. console.log("load work");
  758. }
  759. let max = threading + this.current > this.imgHref.length ? this.imgHref.length : threading + this.current;
  760. for (this.current; this.current < max; this.current++) {
  761. let that = this;
  762. let href = this.imgHref[this.current];
  763. GM.xmlHttpRequest({
  764. method: "GET",
  765. url: href,
  766. responseType: "document",
  767. onload: function (response) {
  768. that.final++;
  769. that.onSucceed(response, href);
  770. },
  771. onerror: function (err) {
  772. if (debug) console.log(err);
  773. that.final++;
  774. that.onFailed(err, href);
  775. },
  776. });
  777. }
  778. },
  779. cleanGDT: function () {
  780. while (gdt.firstChild && gdt.firstChild.className)
  781. gdt.removeChild(gdt.firstChild);
  782. },
  783.  
  784. generateImg: function (callback) {
  785. for (var i = 0; i < this.imgNum; i++) {
  786. if (i < maxPic && i >= minPic - 1) {
  787. var img = document.createElement("img");
  788. var a = document.createElement("a");
  789. img.setAttribute("src", "https://ehgt.org/g/roller.gif");
  790. img.setAttribute("loadding", "lazy");
  791. a.appendChild(img);
  792. this.imgList.push(a);
  793.  
  794. gdt.appendChild(a);
  795. } else {
  796. var img = document.createElement("img");
  797. var a = document.createElement("a");
  798.  
  799. img.setAttribute("src", "https://ehgt.org/g/roller.gif");
  800. img.setAttribute("loadding", "lazy");
  801. a.appendChild(img);
  802.  
  803. this.imgList.push(a);
  804. if (viewAll) gdt.appendChild(a);
  805. }
  806. }
  807.  
  808. gdt.style.textAlign = "center";
  809. gdt.style.maxWidth = "100%";
  810.  
  811. gdo4.innerHTML = ""; //clear origin button(Normal Large)
  812.  
  813. var style = document.createElement("style");
  814. style.type = "text/css";
  815. style.innerHTML = `
  816. div#gdo4{
  817. position:fixed;
  818. width: 212px;
  819. height:32px;
  820. left:unset;
  821. right:10px;
  822. bottom:0px;
  823. top:unset;
  824. text-align:right;
  825. z-index:1;
  826. background:#34353b;
  827. border-radius:5%;
  828. }
  829.  
  830.  
  831.  
  832.  
  833. .double {
  834. font-weight: bold;
  835. // margin: 0 2px 4px 2px;
  836. float: left;
  837. border-radius: 5px;
  838. height:32px;
  839. width: 32px;
  840. //border: 1px solid #989898;
  841. //background: #4f535b;
  842. background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/2_32.png);
  843. }
  844.  
  845. .double:hover{
  846. background: #4f535b;
  847. background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/2_32.png);
  848. }
  849.  
  850. .single{
  851. font-weight: bold;
  852. // margin: 0 2px 4px 2px;
  853. float: left;
  854. border-radius: 5px;
  855. height:32px;
  856. width: 32px;
  857. //border: 1px solid #989898;
  858. // background: #4f535b;
  859. background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/1_32.png);
  860. }
  861.  
  862. .size_pic{
  863. font-weight: bold;
  864. // margin: 0 2px 4px 2px;
  865. float: left;
  866. border-radius: 2px;
  867. height:16px;
  868. width: 16px;
  869. //border: 1px solid #989898;
  870. // background: #4f535b;
  871. }
  872.  
  873. .single:hover{
  874. background: #4f535b;
  875. background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/1_32.png);
  876.  
  877. }
  878.  
  879. .size_btn {
  880. height: 32px;
  881. width: 32px;
  882. border-radius: 100%;
  883. //font-family: Arial;
  884. color: #ffffff;
  885. font-size: 16px;
  886. background: #4f535b;
  887. text-decoration: none;
  888. }
  889.  
  890.  
  891. .pad_pic {
  892. height: 32px;
  893. width: 32px;
  894. border-radius: 100%;
  895. //font-family: Arial;
  896. color: #ffffff;
  897. font-size: 16px;
  898. background: #4f535b;
  899. text-decoration: none;
  900. }
  901.  
  902. .size_btn:hover {
  903. background: #a9adb1;
  904. text-decoration: none;
  905. }
  906. `;
  907. document.getElementsByTagName("head")[0].appendChild(style);
  908.  
  909. //show
  910.  
  911. var single_pic = document.createElement("div"); //create single button
  912. single_pic.className = "single";
  913. single_pic.innerHTML += "";
  914. gdo4.appendChild(single_pic);
  915.  
  916. var double_pic = document.createElement("div"); //create double button
  917. double_pic.className = "double";
  918. double_pic.innerHTML = "";
  919. gdo4.appendChild(double_pic);
  920.  
  921. var pad_pic = document.createElement("button");
  922. pad_pic.className = "pad_pic";
  923. pad_pic.innerHTML += "p";
  924. gdo4.appendChild(pad_pic);
  925.  
  926. var full_pic = document.createElement("button");
  927. full_pic.className = "pad_pic";
  928. full_pic.innerHTML += "f";
  929. gdo4.appendChild(full_pic);
  930.  
  931. var size_pic_reduce = document.createElement("button");
  932. size_pic_reduce.className = "size_btn";
  933. size_pic_reduce.innerHTML += "-";
  934. gdo4.appendChild(size_pic_reduce);
  935.  
  936. var size_pic_add = document.createElement("button");
  937. size_pic_add.className = "size_btn";
  938. size_pic_add.innerHTML += "+";
  939. gdo4.appendChild(size_pic_add);
  940.  
  941. document
  942. .getElementById("gdo4")
  943. .children[0] //when single button click change value of width
  944. .addEventListener("click", async function (event) {
  945. await GM.setValue("width", "0.7");
  946. await GM.setValue("mode", "single");
  947. await pic_width(await GM.getValue("width"));
  948. $("wrap").remove();
  949.  
  950. wrap(await GM.getValue("width"));
  951. });
  952.  
  953. document
  954. .getElementById("gdo4")
  955. .children[1] //when double button click change value of width
  956. .addEventListener("click", async function (event) {
  957. await GM.setValue("width", "0.49");
  958. await GM.setValue("mode", "double");
  959. let view_reverse = await GM.getValue("view_reverse", true);
  960. GM.setValue("view_reverse", !view_reverse);
  961. await pic_width(await GM.getValue("width"));
  962. $("wrap").remove();
  963.  
  964. wrap(await GM.getValue("mode"));
  965. });
  966.  
  967. var pad_img = document.createElement("img");
  968. var pad_a = document.createElement("a");
  969. pad_a.appendChild(pad_img);
  970.  
  971. document
  972. .getElementById("gdo4")
  973. .children[2].addEventListener("click", async (event) => {
  974. this.padding = !this.padding;
  975. const view_reverse = await GM.getValue("view_reverse", true);
  976. await GM.setValue("view_reverse", false);
  977. $("wrap").remove();
  978. await wrap(await GM.getValue("mode"));
  979. $("wrap").remove();
  980. if (this.padding) {
  981. this.imgList.unshift(pad_a);
  982. gdt.insertBefore(pad_a, gdt.firstChild);
  983. } else {
  984. this.imgList.shift();
  985. gdt.removeChild(pad_a);
  986. }
  987. await GM.setValue("view_reverse", view_reverse);
  988. await wrap(await GM.getValue("mode"));
  989. });
  990.  
  991. document
  992. .getElementById("gdo4")
  993. .children[3].addEventListener("click", async function (event) {
  994. await GM.setValue("full_image", true);
  995. await pic_width(0);
  996. });
  997.  
  998. document
  999. .getElementById("gdo4")
  1000. .children[4].addEventListener("click", async function (event) {
  1001. await GM.setValue("full_image", false);
  1002. var size_width = parseFloat(await GM.getValue("width"));
  1003. if (size_width > 0.2 && size_width < 1.5) {
  1004. size_width = size_width - 0.1;
  1005. GM.setValue("width", size_width);
  1006. }
  1007. let _width = await GM.getValue("width");
  1008. await pic_width(_width);
  1009. console.log(_width);
  1010. });
  1011.  
  1012. document
  1013. .getElementById("gdo4")
  1014. .children[5].addEventListener("click", async function (event) {
  1015. await GM.setValue("full_image", false);
  1016. var size_width = parseFloat(await GM.getValue("width"));
  1017. if (size_width > 0.1 && size_width < 1.4) {
  1018. size_width = size_width + 0.1;
  1019. GM.setValue("width", size_width);
  1020. }
  1021. let _width = await GM.getValue("width");
  1022. await pic_width(_width);
  1023. console.log(_width);
  1024. });
  1025.  
  1026. async function pic_width(
  1027. width //change width of pics
  1028. ) {
  1029. for (var i = maxPic - minPic + 1; i > 0; i--) {
  1030. await resizeImg(width);
  1031. }
  1032. }
  1033.  
  1034. callback && callback();
  1035. },
  1036. };
  1037. var g = new Gallery(lpPage, imgNum, minPic, maxPic);
  1038.  
  1039. if (g.checkFunctional()) {
  1040. var viewAll = await GM.getValue("view_all", true);
  1041. g.generateImg(function () {
  1042. if (g.pageNum && viewAll) {
  1043. g.getAllHref(0);
  1044. } else {
  1045. g.getHref(Number(document.querySelector("td.ptds").childNodes[0].text) - 1);
  1046. }
  1047. g.cleanGDT();
  1048. });
  1049.  
  1050. document.addEventListener("keydown", (e) => {
  1051. let nextImg = null;
  1052.  
  1053. if (e.code === "ArrowUp") {
  1054. for (let i = g.imgList.length - 1; i >= 0; i--) {
  1055. const img = g.imgList[i].childNodes[0];
  1056. const rect = img.getBoundingClientRect();
  1057. if (rect.top < -1) {
  1058. nextImg = img;
  1059. break;
  1060. }
  1061. }
  1062. }
  1063.  
  1064. if (e.code === "ArrowDown") {
  1065. for (let i = 0; i < g.imgList.length; i++) {
  1066. const img = g.imgList[i].childNodes[0];
  1067. const rect = img.getBoundingClientRect();
  1068. if (rect.top > 1) {
  1069. nextImg = img;
  1070. break;
  1071. }
  1072. }
  1073. }
  1074.  
  1075. if (nextImg !== null) {
  1076. e.preventDefault();
  1077. window.scrollTo({
  1078. top: nextImg.offsetTop,
  1079. });
  1080. }
  1081. })
  1082.  
  1083. await wrap(await GM.getValue("mode"));
  1084. } else {
  1085. alert(
  1086. "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
  1087. );
  1088. }
  1089. }
  1090. }
  1091.  
  1092. var switchWrap = false;
  1093.  
  1094. const wrap = async (width) => {
  1095. let img = $("#gdt").find("a");
  1096. let gdt = document.getElementById("gdt");
  1097. if (switchWrap == true) {
  1098. for (let i = 0; i < img.length - 1; i++) {
  1099. if (i % 2 !== 1) {
  1100. gdt.insertBefore(img[i + 1], img[i]);
  1101. }
  1102. }
  1103. switchWrap = false;
  1104. }
  1105.  
  1106. if ((await GM.getValue("width")) == undefined) {
  1107. await GM.setValue("width", "0.49");
  1108. console.log("set width:0.49");
  1109. }
  1110.  
  1111. if ((await GM.getValue("mode")) == undefined) {
  1112. await GM.setValue("mode", "double");
  1113. console.log("set mode:double");
  1114. }
  1115. if ((await GM.getValue("view_reverse")) == undefined) {
  1116. await GM.setValue("view_reverse", true);
  1117. console.log("set view_reverse:true");
  1118. }
  1119.  
  1120.  
  1121. img = $("#gdt").find("a");
  1122. let view_reverse = (await GM.getValue("view_reverse", true));
  1123. for (let i = 0; i < img.length; i++) {
  1124. let wrap = document.createElement("wrap");
  1125. wrap.innerHTML = "<br>";
  1126. if ((await GM.getValue("mode")) == "single") {
  1127. gdt.insertBefore(wrap, img[i]);
  1128. } else if ((await GM.getValue("mode")) == "double") {
  1129. if (i % 2 !== 1) {
  1130. gdt.insertBefore(wrap, img[i]);
  1131. if (view_reverse && i != img.length - 1) {
  1132. switchWrap = true;
  1133. gdt.insertBefore(img[i + 1], img[i]);
  1134. }
  1135. }
  1136. }
  1137. }
  1138.  
  1139. await resizeImg(await GM.getValue("width"));
  1140. };
  1141.  
  1142. const resizeImg = async (width) => {
  1143. const full_image = (await GM.getValue("full_image"));
  1144. if (full_image == true) {
  1145. $("#gdt")
  1146. .find("img")
  1147. .css({ "height": "100vh", "width": "auto" });
  1148. } else {
  1149. $("#gdt")
  1150. .find("img")
  1151. .css({ "height": "auto", "width": $(window).width() * width });
  1152. }
  1153. }
  1154.  
  1155. const adjustGmid = () => {
  1156. var height = $("#gd5").outerHeight(true);
  1157. height = height >= 330 ? height : 330;
  1158. $("#gmid").height(height);
  1159. $("#gd4").height(height);
  1160. };
  1161.  
  1162. const viewAllMode = async () => {
  1163. var view_all_btn = document.createElement("p");
  1164. var view_all = await GM.getValue("view_all", true);
  1165.  
  1166. view_all_btn.className = "g3";
  1167. view_all_btn.innerHTML = `<span style="margin-left:10px;">▶</span> <a class="panda_view_all" href="#">Viewer page(s): ${view_all ? "All" : "One"}</a>`;
  1168. $("#gd5").append(view_all_btn);
  1169.  
  1170. $(".panda_view_all").on("click", async () => {
  1171. view_all = await GM.getValue("view_all", true);
  1172. GM.setValue("view_all", !view_all);
  1173. $(".panda_view_all").html(
  1174. `Viewer page(s): ${view_all ? "All" : "One"}`
  1175. );
  1176. window.location.reload(true);
  1177. });
  1178.  
  1179. adjustGmid();
  1180. };
  1181. const viewMode = async () => {
  1182. var view_mode = await GM.getValue("view_mode", true);
  1183. var view_btn = document.createElement("p");
  1184. view_btn.className = "g3";
  1185. view_btn.innerHTML = `<span style="margin-left:10px;">▶</span> <a class="panda_view" href="#">Viewer ${view_mode ? "Enabled" : "Disabled"
  1186. }</a>`;
  1187.  
  1188. $("#gd5").append(view_btn);
  1189.  
  1190. $(".panda_view").on("click", async () => {
  1191. view_mode = await GM.getValue("view_mode", true);
  1192. GM.setValue("view_mode", !view_mode);
  1193. $(".panda_view").html(`Viewer ${!view_mode ? "Enabled" : "Disabled"}`);
  1194. if (view_mode) {
  1195. window.location.reload();
  1196. }
  1197. if (!view_mode && !viewed) {
  1198. viewAllMode();
  1199. view();
  1200. }
  1201. });
  1202.  
  1203. if (view_mode) {
  1204. viewAllMode();
  1205. }
  1206.  
  1207. adjustGmid();
  1208. if (view_mode) {
  1209. // Stop image loadding for thumbnails.
  1210. var imageToStop = document.querySelector("#gdt").querySelectorAll("img");
  1211. imageToStop.forEach((img, key) => {
  1212. // Only load the first thumbnail.
  1213. if (key == 0) {
  1214. return;
  1215. }
  1216. img.src = "";
  1217. })
  1218. view();
  1219. }
  1220. };
  1221.  
  1222. if ((e = $("img")).length === 0 && (e = $("dev")).length === 0) {
  1223. loginPage();
  1224. } else if (window.location.href.match(/^https:\/\/e[x-]hentai\.org\/g/)) {
  1225. downloadPage();
  1226. viewMode();
  1227. }
  1228. });