Let's panda!

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

2024-10-25 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

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