Sleazy Fork is available in English.

Let's panda!

一个用于exhentai和e-hentai的登录、查看、下载的工具

// ==UserScript==
// @name         Let's panda!
// @namespace    https://github.com/Sean2525/Let-s-panda
// @author       sean2525, strong-Ting
// @description  A login, view, download tool for exhentai & e-hentai
// @description:zh-tw 一個用於exhentai和e-hentai的登入、查看、下載的工具
// @description:zh-cn 一个用于exhentai和e-hentai的登录、查看、下载的工具
// @license      MIT
// @require      https://code.jquery.com/jquery-3.2.1.slim.min.js
// @include      https://exhentai.org/
// @include      https://exhentai.org/g/*
// @include      https://e-hentai.org/g/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js
// @require      https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_notification
// @grant        GM.notification
// @connect      *
// @run-at       document-end
// @version      0.2.17
// ==/UserScript==

jQuery(function ($) {
  /**
   * Output extension
   * @type {String} zip
   *                cbz
   *
   * Tips: Convert .zip to .cbz
   * Windows
   * $ ren *.zip *.cbz
   * Linux
   * $ rename 's/\.zip$/\.cbz/' *.zip
   */
  var outputExt = "zip"; // or 'cbz'

  /**
   * Multithreading
   * @type {Number} [1 -> 32]
   */
  var threading = 8;

  /**
   * Logging
   * @type {Boolean}
   */
  var debug = false;


  var viewed = false;
  const loginPage = () => {
    let div = document.createElement("div");
    div.className = "main";
    let username = document.createElement("input");
    let style = document.createElement("style");
    style.innerHTML = `
body {
 background-color: #212121;
}
.main {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
height: ${window.innerHeight}px;
}
.flex-center{
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
}
form {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
}
.image {
position: relative;
margin: 0;
}
.input {
margin-top: 10px;
display: block;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
.btn {
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c;
margin-top: 10px;
display: inline-block;
font-weight: 400;
line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid transparent;
padding: .5rem 1rem;
font-size: 1rem;
border-radius: .25rem;
-webkit-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
transition: all .2s ease-in-out;
}
.btn:hover {
  background-color: #4da64d;
}
.btn-blue {
  color: #fff;
  background-color: #3832dd;
  border-color: #3832dd;
  display: inline-block;
  font-weight: 400;
  line-height: 1.0;
  text-align: center;
  white-space: nowrap;
  vertical-align: middle;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  border: 1px solid transparent;
  padding: .5rem 1rem;
  font-size: 1rem;
  border-radius: .25rem;
  -webkit-transition: all .2s ease-in-out;
  -o-transition: all .2s ease-in-out;
  transition: all .2s ease-in-out;
  }
.btn-blue:hover {
  background-color: #1c15c8;
}
`;
    $("head").append(style);
    const setCookie = (headers) => {
      //
      try {
        headers
          .split("\r\n")
          .find((x) => x.match("cookie"))
          .replace("set-cookie: ", "")
          .split("\n")
          .map(
            (x) =>
              (document.cookie = x.replace(".e-hentai.org", ".exhentai.org") + " secure")
          );
      } catch (err) {
        if (debug) console.log(err);
      }
      document.cookie =
        "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/; secure";

      setTimeout(function () { window.location.reload() }, 3000);
    };
    const clearCookie = () => {
      if (debug) console.log("Clearning cookies");
      document.cookie =
        "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/; secure";
      window.location.reload();
    };
    let form = document.createElement("form");
    let login = document.createElement("button");
    let wrapper = document.createElement("div");
    let loadding = document.createElement("img");
    let password = document.createElement("input");
    username.placeholder = "Username"
    password.placeholder = "Password"
    let info = document.createElement("p");
    let error = document.createElement("p");
    info.innerHTML = `
    <center>
    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 >
Please make sure you are logged in successfully and then click this <button class="clearCookie btn-blue">button</button>
    </center>
`;
    info.style.color = "white";
    username.type = "text";
    username.className = "input";
    password.type = "password";
    password.className = "input";
    loadding.src =
      "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==";
    loadding.style.position = "relative";
    info.hidden = true;
    loadding.hidden = true;
    login.addEventListener("click", () => {
      loadding.hidden = false;
      GM.xmlHttpRequest({
        method: "POST",
        url: "https://forums.e-hentai.org/index.php?act=Login&CODE=01",
        data: `referer=https://forums.e-hentai.org/index.php?&b=&bt=&UserName=${username.value}&PassWord=${password.value}&CookieDate=1"}`,
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        onload: function (response) {
          if (debug) console.log(response);
          if (/You are now logged/.exec(response.responseText)) {
            error.style = "color:green";
            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. "
            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");
            setCookie(response.responseHeaders);
          } else if (/IF YOU DO NOT SEE THE CAPTCHA/.exec(response.responseText)) {
            error.style = "color:red";
            error.innerText = "Login failed: Please visit the forums directly and log in from there; reCaptcha has been enabled."
          }
          else {
            error.style = "color:red";
            error.innerText = "Login failed: Please check that your username and password are correct.";
          }
          info.hidden = false;
          loadding.hidden = true;
        },
        onerror: function (err) {
          console.error(err);
          error.style = "color:red";
          error.innerText("Login got error: Please contact me at https://github.com/MinoLiu/Let-s-panda/issues");
          loadding.hidden = true;
        },
      });
    });
    login.className = "btn";
    login.innerHTML = "Login";
    form.append(username);
    form.append(password);
    wrapper.className = "flex-center";
    wrapper.append(loadding);
    wrapper.append(login);
    form.append(wrapper);
    form.addEventListener("submit", (e) => {
      e.preventDefault();
    });
    var image = document.createElement("img");
    image.className = "image";
    image.src = "https://i.imgur.com/oX86mGf.png"
    div.append(image);
    div.append(form);
    div.append(error);
    div.append(info);
    $("body").append(div);
    $(".clearCookie").on("click", clearCookie);
  };

  const downloadPage = () => {
    var zip = new JSZip(),
      doc = document,
      tit = doc.title,
      $win = $(window),
      loc = /https?:\/\/e[x-]hentai\.org\/g\/\d+\/\w+/.exec(doc.location.href)[0],
      prevZip = false,
      current = 0,
      images = [],
      total = 0,
      final = 0,
      failed = 0,
      hrefs = [],
      comicId = location.pathname.match(/\d+/)[0],
      download = document.createElement("p");

    const dlImg = ({ index, url, _ }, success, error) => {
      var filename = url.replace(/.*\//g, "");
      var extension = filename.split(".").pop();
      filename = ("0000" + index).slice(-4) + "." + extension;
      if (debug) console.log(filename, "progress");
      GM.xmlHttpRequest({
        method: "GET",
        url: url,
        responseType: "arraybuffer",
        onload: function (response) {
          final++;
          success(response, filename);
        },
        onerror: function (err) {
          final++;
          error(err, filename);
        },
      });
    };

    const next = () => {
      download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="#"> Downloading ${final}/${total}</a>`;
      if (debug) console.log(final, current);
      if (final < current) return;
      final < total ? addZip() : genZip();
    };

    const end = () => {
      $win.off("beforeunload");
      if (failed > 0) {
        alert("Some pages download failed, please unzip and check!");
      }
      if (debug) console.timeEnd("eHentai");
    };

    const genZip = () => {
      zip
        .generateAsync({
          type: "blob",
        })
        .then(function (blob) {
          var zipName =
            tit.replace(/\s/g, "_") + "." + comicId + "." + outputExt;

          if (prevZip) window.URL.revokeObjectURL(prevZip);
          prevZip = blob;

          saveAs(blob, zipName);
          if (debug) console.log("COMPLETE");
          download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="${window.URL.createObjectURL(
            prevZip
          )}" download="${zipName}"> Download completed!</a>`;
          end();
        });
    };

    const addZip = () => {
      total = images.length;
      var max = current + threading;
      if (max > total) max = total;
      for (current; current < max; current++) {
        let _href = images[current];
        dlImg(
          _href,
          function (response, filename) {
            zip.file(filename, response.response);
            if (debug) console.log(filename, "image success");
            next();
          },
          function (err, filename) {
            final--;
            // retry backupUrl for once
            GM.xmlHttpRequest({
              method: "GET",
              url: _href.backupUrl,
              onload: function (response) {
                let imgNo = parseInt(
                  response.responseText.match("startpage=(\\d+)").pop()
                );
                let img = new DOMParser()
                  .parseFromString(response.responseText, "text/html")
                  .querySelector("#img");
                if (debug) console.log(imgNo, "backupUrl success");
                _href.url = img.src;
                dlImg(
                  _href,
                  function (response, filename) {
                    zip.file(filename, response.response);
                    if (debug) console.log(filename, "backupUrl image success");
                    next();
                  },
                  function (err, filename) {
                    failed++;
                    zip.file(
                      filename + "_" + comicId + "_error.gif",
                      "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=",
                      {
                        base64: true,
                      }
                    );
                    if (debug) console.log(filename, "backupUrl image error");
                    next();
                  }
                );
              },
              onerror: function (err, filename) {
                dlImg(
                  _href,
                  function (response, filename) {
                    zip.file(filename, response.response);
                    if (debug) console.log(filename, "retry image success");
                    next();
                  },
                  function (err, filename) {
                    failed++;
                    zip.file(
                      filename + "_" + comicId + "_error.gif",
                      "R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=",
                      {
                        base64: true,
                      }
                    );
                    if (debug) console.log(filename, "retry url error");
                    next();
                  }
                );
              }
            });
          }
        );
      }
    };

    /**
     * Update image download status.
     */
    const getImageNext = () => {
      download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="#">Getting images ${final}/${hrefs.length}</a>`;
      if (debug) console.log(final, current);
      if (final < current) return;
      final < hrefs.length
        ? getImage()
        : (() => {
          current = 0;
          final = 0;
          addZip();
        })();
    };

    /**
     * Get all images from hrefs.
     */
    const getImage = () => {
      let max = current + threading;
      if (max > hrefs.length) max = hrefs.length;
      for (current; current < max; current++) {
        if (debug) console.log(hrefs[current]);
        let href = hrefs[current];
        GM.xmlHttpRequest({
          method: "GET",
          url: hrefs[current],
          onload: function (response) {
            let imgNo = parseInt(
              response.responseText.match("startpage=(\\d+)").pop()
            );
            let img = new DOMParser()
              .parseFromString(response.responseText, "text/html")
              .querySelector("#img");
            if (debug) console.log(imgNo, "url success");
            let src = href + "?nl=" + /nl\(\'(.*)\'\)/.exec(img.attributes.onerror.value)[1];
            images.push({
              index: imgNo,
              url: img.src,
              backupUrl: src,
            });
            final++;
            getImageNext();
          },
          onerror: function (err) {
            final++;
            getImageNext();
            if (debug) console.log(err);
          },
        });
      }
    };

    /**
     * Get the href of all images from all pages.
     */
    const getHref = () => {
      childNodes = document.querySelector("table[class=ptt] tbody tr")
        .childNodes;
      let page = parseInt(
        childNodes[childNodes.length - 2].textContent.replace(",", "")
      );
      for (let i = 0; i < page; i++) {
        GM.xmlHttpRequest({
          method: "GET",
          url: `${loc}?p=${i}`,
          onload: function (response) {
            if (debug)
              console.log(`page ${loc}?p=${i} detect ${response.responseText}`);
            let imgs = [
              ...new DOMParser()
                .parseFromString(response.responseText, "text/html")
                .querySelectorAll(".gdtm a"),
            ];
            if (!imgs.length)
              imgs = [
                ...new DOMParser()
                  .parseFromString(response.responseText, "text/html")
                  .querySelectorAll(".gdtl a"),
              ];
            if (!imgs.length) {
              alert(
                "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
              );
            }
            imgs.forEach((v) => {
              hrefs.push(v.href);
            });
            if (i == page - 1) {
              getImage();
            }
          },
          onerror: function (err) {
            download.innerHTML =
              '<span style="margin-left:10px;">▶</span> <a href="#">Get href failed</a>';
            if (i == page - 1) {
              getImage();
            }
            if (debug) console.log(err);
          },
        });
      }
    };

    download.className = "g3";
    download.innerHTML = `<span style="margin-left:10px;">▶</span> <a class="panda_download" href="#">Download</a>`;
    $("#gd5").append(download);
    $(".panda_download").on("click", () => {
      if (threading < 1) threading = 1;
      if (threading > 32) threading = 32;
      if (debug) console.time("eHentai");
      $win.on("beforeunload", function () {
        return "Progress is running...";
      });
      download.innerHTML = `<span style="margin-left:10px;">▶</span> <a href="#">Start Download</a>`;
      getHref();
    });
  };


  function view() {
    viewed = true;
    if (threading < 1) threading = 1;
    if (threading > 32) threading = 32;
    var gdt = document.querySelector("#gdt");
    var gdd = document.querySelector("#gdd");
    var gdo4 = document.createElement("div");
    gdo4.setAttribute("id", "gdo4");
    $("body").append(gdo4);

    let childNodes = document.querySelector("table[class=ptt] tbody tr")
      .childNodes;
    let lpPage = parseInt(
      childNodes[childNodes.length - 2].textContent.replace(",", "")
    );

    var data = document
      .querySelector("body div.gtb p.gpc")
      .textContent.split(" ");

    var minPic = parseInt(data[1].replace(",", ""));
    var maxPic = parseInt(data[3].replace(",", ""));

    var imgNum = parseInt(
      gdd
        .querySelector("#gdd tr:nth-child(n+6) td.gdt2")
        .textContent.split(" ")[0]
    );


    viewer(lpPage, imgNum, minPic, maxPic);

    async function viewer(lpPage, imgNum, minPic, maxPic) {
      var Gallery = function (pageNum, imgNum, minPic, maxPic) {
        this.pageNum = pageNum || 0;
        this.imgNum = imgNum || 0;
        this.loc = /https?:\/\/e[x-]hentai\.org\/g\/\d+\/\w+/.exec(location.href)[0];
        this.padding = false;
        this.current = 0;
        this.final = 0;
      };
      var viewAll = await GM.getValue("view_all", true);
      Gallery.prototype = {
        imgHref: [],
        imgList: [],
        retry: 0,
        getAllHref: function (nextID) {
          if (nextID >= this.pageNum) {
            this.loadNextImage();
            return;
          }
          var that = this;
          GM.xmlHttpRequest({
            method: "GET",
            url: `${this.loc}?p=${nextID}`,
            onload: function (response) {
              if (debug)
                console.log(`page ${that.loc}?p=${nextID} detect ${response.responseText}`);
              let imgs = [
                ...new DOMParser()
                  .parseFromString(response.responseText, "text/html")
                  .querySelectorAll(".gdtm a"),
              ];
              if (!imgs.length)
                imgs = [
                  ...new DOMParser()
                    .parseFromString(response.responseText, "text/html")
                    .querySelectorAll(".gdtl a"),
                ];
              if (!imgs.length) {
                alert(
                  "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
                );
              }
              imgs.forEach((v) => {
                that.imgHref.push(v.href);
              });
              that.getAllHref(nextID + 1);
            },
            onerror: function (err) {
              if (debug) console.log(err);
              that.retry++;
              if (that.retry > 2) {
                alert(`Page number ${nextID + 1} load failed for 3 times.`);
                that.getAllHref(nextID + 1);
              } else {
                that.getAllHref(nextID);
              }
            },
          });
        },
        getHref: function (pageID) {
          var that = this;
          GM.xmlHttpRequest({
            method: "GET",
            url: `${this.loc}?p=${pageID}`,
            onload: function (response) {
              if (debug)
                console.log(`page ${that.loc}?p=${pageID} detect ${response.responseText}`);
              let imgs = [
                ...new DOMParser()
                  .parseFromString(response.responseText, "text/html")
                  .querySelectorAll(".gdtm a"),
              ];
              if (!imgs.length)
                imgs = [
                  ...new DOMParser()
                    .parseFromString(response.responseText, "text/html")
                    .querySelectorAll(".gdtl a"),
                ];
              if (!imgs.length) {
                alert(
                  "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
                );
              }
              imgs.forEach((v) => {
                that.imgHref.push(v.href);
              });
              that.loadNextImage();
            },
            onerror: function (err) {
              if (debug) console.log(err);
              that.retry++;
              if (that.retry > 2) {
                alert(`Page number ${nextID + 1} load failed for 3 times.`);
                that.loadNextImage();
              } else {
                that.getHref(nextID);
              }
            },
          });
        },
        checkFunctional: function () {
          return (this.imgNum > 41 && this.pageNum < 2) || this.imgNum !== 0;
        },
        loadNextImage: function () {
          if (this.final < this.current) {
            return;
          }
          this.loadPageUrls();
        },
        onSucceed: async function (response, href) {
          let imgNo = parseInt(
            response.responseText.match("startpage=(\\d+)").pop()
          );
          let img = new DOMParser()
            .parseFromString(response.responseText, "text/html")
            .querySelector("#img");
          if (debug) console.log(imgNo, "success");
          let src = href + "?nl=" + /nl\(\'(.*)\'\)/.exec(img.attributes.onerror.value)[1];
          Gallery.prototype.imgList[imgNo - 1].setAttribute(
            "data-href",
            src
          );

          let timeoutId;
          let timeoutDuration = 10000; // 10s

          timeoutId = setTimeout(function () {
            // timeout trigger error
            Gallery.prototype.imgList[imgNo - 1].childNodes[0].dispatchEvent(new Event('error'));
          }, timeoutDuration);

          $(Gallery.prototype.imgList[imgNo - 1].childNodes[0]).on("load", function () {
            // success clear timeoutId
            clearTimeout(timeoutId);
          });

          $(Gallery.prototype.imgList[imgNo - 1].childNodes[0]).on(
            "error",
            function () {
              var ajax = new XMLHttpRequest();
              ajax.onreadystatechange = async function () {
                if (debug) {
                  console.log(`Failed load ${Number(imgNo)}, getting backup image from ${src}.`);
                }
                if (4 == ajax.readyState && 200 == ajax.status) {
                  var _imgNo = parseInt(
                    ajax.responseText.match("startpage=(\\d+)").pop()
                  );
                  var imgDom = new DOMParser()
                    .parseFromString(ajax.responseText, "text/html")
                    .getElementById("img");
                  Gallery.prototype.imgList[_imgNo - 1].childNodes[0].src =
                    imgDom.src;
                }
              };
              ajax.open("GET", src);
              ajax.send(null);
            }
          );

          Gallery.prototype.imgList[imgNo - 1].childNodes[0].src = img.src;

          this.loadNextImage();
        },
        onFailed: function (err, href) {
          GM.xmlHttpRequest({
            method: "GET",
            url: href,
            responseType: "document",
            onload: function (response) {
              that.onSucceed(response, href);
            },
            onerror: function (err) {
              if (debug) console.log(err);
              this.loadNextImage();
            },
          });
        },
        loadPageUrls: function () {
          if (debug) {
            console.log("load work");
          }
          let max = threading + this.current > this.imgHref.length ? this.imgHref.length : threading + this.current;
          for (this.current; this.current < max; this.current++) {
            let that = this;
            let href = this.imgHref[this.current];
            GM.xmlHttpRequest({
              method: "GET",
              url: href,
              responseType: "document",
              onload: function (response) {
                that.final++;
                that.onSucceed(response, href);
              },
              onerror: function (err) {
                if (debug) console.log(err);
                that.final++;
                that.onFailed(err, href);
              },
            });
          }
        },
        cleanGDT: function () {
          while (gdt.firstChild && gdt.firstChild.className)
            gdt.removeChild(gdt.firstChild);
        },

        generateImg: function (callback) {
          for (var i = 0; i < this.imgNum; i++) {
            if (i < maxPic && i >= minPic - 1) {
              var img = document.createElement("img");
              var a = document.createElement("a");
              img.setAttribute("src", "https://ehgt.org/g/roller.gif");
              img.setAttribute("loadding", "lazy");
              a.appendChild(img);
              this.imgList.push(a);

              gdt.appendChild(a);
            } else {
              var img = document.createElement("img");
              var a = document.createElement("a");

              img.setAttribute("src", "https://ehgt.org/g/roller.gif");
              img.setAttribute("loadding", "lazy");
              a.appendChild(img);

              this.imgList.push(a);
              if (viewAll) gdt.appendChild(a);
            }
          }

          gdt.style.textAlign = "center";
          gdt.style.maxWidth = "100%";

          gdo4.innerHTML = ""; //clear origin button(Normal Large)

          var style = document.createElement("style");
          style.type = "text/css";
          style.innerHTML = `
div#gdo4{
position:fixed;
width: 212px;
height:32px;
left:unset;
right:10px;
bottom:0px;
top:unset;
text-align:right;
z-index:1;
background:#34353b;
border-radius:5%;
}




.double {
font-weight: bold;
//    margin: 0 2px 4px 2px;
float: left;
border-radius: 5px;
height:32px;
width: 32px;
//border: 1px solid #989898;
//background: #4f535b;
background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/2_32.png);
}

.double:hover{
background: #4f535b;
background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/2_32.png);
}

.single{
font-weight: bold;
//   margin: 0 2px 4px 2px;
float: left;
border-radius: 5px;
height:32px;
width: 32px;
//border: 1px solid #989898;
// background: #4f535b;
background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/1_32.png);
}

.size_pic{
font-weight: bold;
//  margin: 0 2px 4px 2px;
float: left;
border-radius: 2px;
height:16px;
width: 16px;
//border: 1px solid #989898;
// background: #4f535b;
}

.single:hover{
background: #4f535b;
background-image: url(https://raw.githubusercontent.com/MinoLiu/Let-s-panda/master/icons/1_32.png);

}

.size_btn {
height: 32px;
width: 32px;
border-radius: 100%;
//font-family: Arial;
color: #ffffff;
font-size: 16px;
background: #4f535b;
text-decoration: none;
}


.pad_pic {
height: 32px;
width: 32px;
border-radius: 100%;
//font-family: Arial;
color: #ffffff;
font-size: 16px;
background: #4f535b;
text-decoration: none;
}

.size_btn:hover {
background: #a9adb1;
text-decoration: none;
}
`;
          document.getElementsByTagName("head")[0].appendChild(style);

          //show

          var single_pic = document.createElement("div"); //create single button
          single_pic.className = "single";
          single_pic.innerHTML += "";
          gdo4.appendChild(single_pic);

          var double_pic = document.createElement("div"); //create double button
          double_pic.className = "double";
          double_pic.innerHTML = "";
          gdo4.appendChild(double_pic);

          var pad_pic = document.createElement("button");
          pad_pic.className = "pad_pic";
          pad_pic.innerHTML += "p";
          gdo4.appendChild(pad_pic);

          var full_pic = document.createElement("button");
          full_pic.className = "pad_pic";
          full_pic.innerHTML += "f";
          gdo4.appendChild(full_pic);

          var size_pic_reduce = document.createElement("button");
          size_pic_reduce.className = "size_btn";
          size_pic_reduce.innerHTML += "-";
          gdo4.appendChild(size_pic_reduce);

          var size_pic_add = document.createElement("button");
          size_pic_add.className = "size_btn";
          size_pic_add.innerHTML += "+";
          gdo4.appendChild(size_pic_add);

          document
            .getElementById("gdo4")
            .children[0] //when single button click change value of width
            .addEventListener("click", async function (event) {
              await GM.setValue("width", "0.7");
              await GM.setValue("mode", "single");
              await pic_width(await GM.getValue("width"));
              $("wrap").remove();

              wrap(await GM.getValue("width"));
            });

          document
            .getElementById("gdo4")
            .children[1] //when double button click change value of width
            .addEventListener("click", async function (event) {
              await GM.setValue("width", "0.49");
              await GM.setValue("mode", "double");
              let view_reverse = await GM.getValue("view_reverse", true);
              GM.setValue("view_reverse", !view_reverse);
              await pic_width(await GM.getValue("width"));
              $("wrap").remove();

              wrap(await GM.getValue("mode"));
            });

          var pad_img = document.createElement("img");
          var pad_a = document.createElement("a");
          pad_a.appendChild(pad_img);

          document
            .getElementById("gdo4")
            .children[2].addEventListener("click", async (event) => {
              this.padding = !this.padding;
              const view_reverse = await GM.getValue("view_reverse", true);
              await GM.setValue("view_reverse", false);
              $("wrap").remove();
              await wrap(await GM.getValue("mode"));
              $("wrap").remove();
              if (this.padding) {
                this.imgList.unshift(pad_a);
                gdt.insertBefore(pad_a, gdt.firstChild);
              } else {
                this.imgList.shift();
                gdt.removeChild(pad_a);
              }
              await GM.setValue("view_reverse", view_reverse);
              await wrap(await GM.getValue("mode"));
            });

          document
            .getElementById("gdo4")
            .children[3].addEventListener("click", async function (event) {
              await GM.setValue("full_image", true);
              await pic_width(0);
            });

          document
            .getElementById("gdo4")
            .children[4].addEventListener("click", async function (event) {
              await GM.setValue("full_image", false);
              var size_width = parseFloat(await GM.getValue("width"));
              if (size_width > 0.2 && size_width < 1.5) {
                size_width = size_width - 0.1;
                GM.setValue("width", size_width);
              }
              let _width = await GM.getValue("width");
              await pic_width(_width);
              console.log(_width);
            });

          document
            .getElementById("gdo4")
            .children[5].addEventListener("click", async function (event) {
              await GM.setValue("full_image", false);
              var size_width = parseFloat(await GM.getValue("width"));
              if (size_width > 0.1 && size_width < 1.4) {
                size_width = size_width + 0.1;
                GM.setValue("width", size_width);
              }
              let _width = await GM.getValue("width");
              await pic_width(_width);
              console.log(_width);
            });

          async function pic_width(
            width //change width of pics
          ) {
            for (var i = maxPic - minPic + 1; i > 0; i--) {
              await resizeImg(width);
            }
          }

          callback && callback();
        },
      };
      var g = new Gallery(lpPage, imgNum, minPic, maxPic);

      if (g.checkFunctional()) {
        var viewAll = await GM.getValue("view_all", true);
        g.generateImg(function () {
          if (g.pageNum && viewAll) {
            g.getAllHref(0);
          } else {
            g.getHref(Number(document.querySelector("td.ptds").childNodes[0].text) - 1);
          }
          g.cleanGDT();
        });

        document.addEventListener("keydown", (e) => {
          let nextImg = null;

          if (e.code === "ArrowUp") {
            for (let i = g.imgList.length - 1; i >= 0; i--) {
              const img = g.imgList[i].childNodes[0];
              const rect = img.getBoundingClientRect();
              if (rect.top < -1) {
                nextImg = img;
                break;
              }
            }
          }

          if (e.code === "ArrowDown") {
            for (let i = 0; i < g.imgList.length; i++) {
              const img = g.imgList[i].childNodes[0];
              const rect = img.getBoundingClientRect();
              if (rect.top > 1) {
                nextImg = img;
                break;
              }
            }
          }

          if (nextImg !== null) {
            e.preventDefault();
            window.scrollTo({
              top: nextImg.offsetTop,
            });
          }
        })

        await wrap(await GM.getValue("mode"));
      } else {
        alert(
          "There are some issue in the script\nplease open an issue on Github\nhttps://github.com/MinoLiu/Let-s-panda/issues"
        );
      }
    }
  }

  var switchWrap = false;

  const wrap = async (width) => {
    let img = $("#gdt").find("a");
    let gdt = document.getElementById("gdt");
    if (switchWrap == true) {
      for (let i = 0; i < img.length - 1; i++) {
        if (i % 2 !== 1) {
          gdt.insertBefore(img[i + 1], img[i]);
        }
      }
      switchWrap = false;
    }

    if ((await GM.getValue("width")) == undefined) {
      await GM.setValue("width", "0.49");
      console.log("set width:0.49");
    }

    if ((await GM.getValue("mode")) == undefined) {
      await GM.setValue("mode", "double");
      console.log("set mode:double");
    }
    if ((await GM.getValue("view_reverse")) == undefined) {
      await GM.setValue("view_reverse", true);
      console.log("set view_reverse:true");
    }


    img = $("#gdt").find("a");
    let view_reverse = (await GM.getValue("view_reverse", true));
    for (let i = 0; i < img.length; i++) {
      let wrap = document.createElement("wrap");
      wrap.innerHTML = "<br>";
      if ((await GM.getValue("mode")) == "single") {
        gdt.insertBefore(wrap, img[i]);
      } else if ((await GM.getValue("mode")) == "double") {
        if (i % 2 !== 1) {
          gdt.insertBefore(wrap, img[i]);
          if (view_reverse && i != img.length - 1) {
            switchWrap = true;
            gdt.insertBefore(img[i + 1], img[i]);
          }
        }
      }
    }

    await resizeImg(await GM.getValue("width"));
  };

  const resizeImg = async (width) => {
    const full_image = (await GM.getValue("full_image"));
    if (full_image == true) {
      $("#gdt")
        .find("img")
        .css({ "height": "100vh", "width": "auto" });
    } else {
      $("#gdt")
        .find("img")
        .css({ "height": "auto", "width": $(window).width() * width });
    }
  }

  const adjustGmid = () => {
    var height = $("#gd5").outerHeight(true);
    height = height >= 330 ? height : 330;
    $("#gmid").height(height);
    $("#gd4").height(height);
  };

  const viewAllMode = async () => {
    var view_all_btn = document.createElement("p");
    var view_all = await GM.getValue("view_all", true);

    view_all_btn.className = "g3";
    view_all_btn.innerHTML = `<span style="margin-left:10px;">▶</span> <a class="panda_view_all" href="#">Viewer page(s): ${view_all ? "All" : "One"}</a>`;
    $("#gd5").append(view_all_btn);

    $(".panda_view_all").on("click", async () => {
      view_all = await GM.getValue("view_all", true);
      GM.setValue("view_all", !view_all);
      $(".panda_view_all").html(
        `Viewer page(s): ${view_all ? "All" : "One"}`
      );
      window.location.reload(true);
    });

    adjustGmid();
  };
  const viewMode = async () => {
    var view_mode = await GM.getValue("view_mode", true);
    var view_btn = document.createElement("p");
    view_btn.className = "g3";
    view_btn.innerHTML = `<span style="margin-left:10px;">▶</span> <a class="panda_view" href="#">Viewer ${view_mode ? "Enabled" : "Disabled"
      }</a>`;

    $("#gd5").append(view_btn);

    $(".panda_view").on("click", async () => {
      view_mode = await GM.getValue("view_mode", true);
      GM.setValue("view_mode", !view_mode);
      $(".panda_view").html(`Viewer ${!view_mode ? "Enabled" : "Disabled"}`);
      if (view_mode) {
        window.location.reload();
      }
      if (!view_mode && !viewed) {
        viewAllMode();
        view();
      }
    });

    if (view_mode) {
      viewAllMode();
    }

    adjustGmid();
    if (view_mode) {
      // Stop image loadding for thumbnails.
      var imageToStop = document.querySelector("#gdt").querySelectorAll("img");
      imageToStop.forEach((img, key) => {
        // Only load the first thumbnail.
        if (key == 0) {
          return;
        }
        img.src = "";
      })
      view();
    }
  };

  if ((e = $("img")).length === 0 && (e = $("dev")).length === 0) {
    loginPage();
  } else if (window.location.href.match(/^https:\/\/e[x-]hentai\.org\/g/)) {
    downloadPage();
    viewMode();
  }
});