Greasy Fork

For Imhentai

不翻墙下,更快加载 imhentai.xxx 的图片,并提供打包下载

目前为 2023-07-29 提交的版本。查看 最新版本

// ==UserScript==
// @name         For Imhentai
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  不翻墙下,更快加载 imhentai.xxx 的图片,并提供打包下载
// @author       水母
// @match        https://imhentai.xxx/gallery/*

// @match        https://*.imhentai.xxx/*
// @match        https://*.annan-can.top:*/*
// @match        file:///D:/PROJECT/VSCode/py/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js#sha512-uVSVjE7zYsGz4ag0HEzfugJ78oHCI1KhdkivjQro8ABL/PRiEO4ROwvrolYAcZnky0Fl/baWKYilQfWvESliRA==

// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @license MIT
// ==/UserScript==
// match        https://imhentai.xxx/view/*
// require      file:///D:\PROJECT\VSCode\py\JS script\imhen.js

(function () {
  "use strict";
  // 全局数据
  let IS_INIT = false;
  let IS_RUN = false;
  let IS_DOWNLOADING = false;
  // 页码序列 [cover.jpg, 1.jpg, ..., 30.jpg] : cover不计入页数; eg.总页数定为 30; 数组 [0] 定为 cover
  let CURRENT_PAGE = 0; // 当前页码
  let MAX_BROWSE_PAGE = 0; // 最大浏览的页码,只增
  let PAGE_LOADED = 0; // 已加载的页码
  let SCALE = 1; // 图片整体缩放

  /**
   * @desc 标识当前在哪个页面
   */
  let CURRENT_URL;
  /**
   * @enum
   * @desc 页面枚举
   */
  const CURRENT_URL_TYPE = {
    /**
     * @desc 标识在 https://imhentai.xxx/gallery/? 页面
     */
    gallery: "gallery",
    /**
     * @desc 标识在 https://imhentai.xxx/view/? 页面
     */
    view: "view",
    /**
     * @desc 标识在 https://?.imhentai.xxx/?/?/cover.jpg 页面
     */
    imgPage: "imgPage",
    /**
     * @desc 标识在 测试 gallery 页面
     */
    testGallery: "testGallery",
    /**
     * @desc 标识在 测试 view 页面
     */
    testView: "testView",
    /**
     * @desc 标识为无法识别页面
     */
    unknow: "unknow",
  };

  // 用户定义的下载页码区间
  let UserCustemRange = {
    min: 0,
    max: 0,
    page_loaded: 0,
  };
  // enum
  const BtnID = {
    runBtn: "run",
    previousBtn: "pre",
    nextBtn: "next",
    downloadBtn: "down",
    scaleUpBtn: "sUp",
    scaleResetBtn: "sReset",
    scaleDownBtn: "sDown",
  };
  const BtnText = {
    runBtn: "启动🥰",
    previousBtn: "⫷",
    nextBtn: "⫸",
    downloadBtn: "下载🥵",
    scaleUpBtn: "⇲",
    scaleResetBtn: "↺◲",
    scaleDownBtn: "⇱",
  };
  /**
   * 异步 FileReader 加载完毕计算器
   */
  const CounterForFileReader = {
    is_lock: false,
    count: 0,
    /**
     * 如果更新成功,返回 true
     * @returns {boolean}
     */
    update() {
      if (this.is_lock) return false;
      else {
        this.is_lock = true;
        this.count++;
        this.is_lock = false;
        return true;
      }
    },
  };

  // 避免这两个没必要下载属性,被 JSON.stringify()
  const keyImgEle = Symbol("imgEle");
  const keyImageBase64 = Symbol("imageBase64");
  /**
   *
   * @param {string} imgName
   * @param {string} imgAlt
   * @param {string} imgUrl
   * @param {string} imgType
   * @param {number} width
   * @param {number} height
   * @param {number} SCALE
   * @param {Image} imgEle
   * @param {string} imageBase64
   */
  function ImgInfo(
    imgName,
    imgAlt,
    imgUrl = "",
    imgType = "",
    width = 0,
    height = 0,
    SCALE = 1,
    imgEle = null,
    imageBase64 = ""
  ) {
    this.imgName = imgName;
    this.imgAlt = !imgAlt ? imgName : imgAlt;
    this.imgUrl = imgUrl;
    this.imgType = imgType;
    this.width = width;
    this.height = height;
    this.SCALE = SCALE;
    this[keyImgEle] = imgEle;
    this[keyImageBase64] = imageBase64;
  }
  /**
   *
   * @param {string} name_en
   * @param {string} name_sub
   * @param {number} page
   * @param {string} root_url
   * @param {ImgInfo[]} imgInfoList
   * @param {string[]} types
   */
  function BzData(
    name_en = "Null",
    name_sub = "Null",
    page = 0,
    root_url = "",
    imgInfoList = [],
    types = [".jpg", ".png", ".gif", ".err"]
  ) {
    this.name_en = name_en;
    this.name_sub = name_sub;
    this.page = page;
    this.root_url = root_url;
    this.imgInfoList = imgInfoList;
    this.types = types;
  }
  /**
   * BzData 迭代器
   * @param {BzData} bzData
   */
  function* BzDataIterator(bzData) {
    let index = 0;
    while (index < bzData.imgInfoList.length) {
      let imgInfo = bzData.imgInfoList[index];
      yield [index++, bzData.root_url, imgInfo];
    }
  }

  /**
   * 漫画名去特殊字符处理
   * @param {string} filename 文件名
   * @return {string} 处理后的文件名
   */
  function processFilename(filename) {
    return filename
      .replaceAll("\\", "-")
      .replaceAll("/", "-")
      .replaceAll(":", ":")
      .replaceAll("*", "-")
      .replaceAll("?", "?")
      .replaceAll('"', "“")
      .replaceAll("<", "《")
      .replaceAll(">", "》")
      .replaceAll("|", "~");
  }

  /**
   * 判断图片 url 有效与否
   * @returns {Promise<Image>}
   */
  function verifyImgExists(imgUrl) {
    return new Promise((resolve, reject) => {
      let ImgObj = new Image();
      ImgObj.src = imgUrl;
      ImgObj.onload = () => resolve(ImgObj);
      ImgObj.onerror = (rej) => reject(rej);
    });
  }

  /**
   * 为 ImgInfo 测试三种后缀
   * @param {string} root_url
   * @param {ImgInfo} imgInfo
   * @param {string[]} [types=['.jpg', '.png', '.gif', '.err']]
   */
  async function processImgInfoAsync(
    root_url,
    imgInfo,
    types = [".jpg", ".png", ".gif", ".err"]
  ) {
    // 测试三种后缀
    for (let type of types) {
      imgInfo.imgUrl = root_url + imgInfo.imgName + type;
      imgInfo.imgType = type;
      try {
        let ImgObj = await verifyImgExists(imgInfo.imgUrl);

        // 图片有效,即加载图片的 base64
        // 站点的跨域策略
        // canvas 无法加载 gif
        if (
          CURRENT_URL !== CURRENT_URL_TYPE.gallery &&
          CURRENT_URL !== CURRENT_URL_TYPE.testGallery
        ) {
          if (type !== ".gif") {
            try {
              let c = document.querySelector("#can-cvs");
              let ctx = c.getContext("2d");
              c.height = ImgObj.naturalHeight;
              c.width = ImgObj.naturalWidth;
              ctx.drawImage(
                ImgObj,
                0,
                0,
                ImgObj.naturalWidth,
                ImgObj.naturalHeight
              );
              imgInfo[keyImageBase64] = c.toDataURL();
            } catch (error) {
              imgInfo[keyImageBase64] = "data:image/png;base64";
              console.log(`[Err] ${imgInfo.imgUrl} 处理 base64 error`);
            }
          } else {
            getGifBase64Async(imgInfo);
          }
        }
        imgInfo[keyImgEle] = ImgObj;
        imgInfo.width = ImgObj.width;
        imgInfo.height = ImgObj.height;
        break;
      } catch (e) {
        console.log("[Test] 尝试下一个扩展名");
        continue; // 未测试最后一个,继续
      }
    }
  }

  /**
   * 为所有图片生成正确后缀类型
   * @param {BzDataIterator} bzDataIterator
   */
  async function processImgAsync(bzDataIterator) {
    for (let [index, root_url, imgInfo] of bzDataIterator) {
      await processImgInfoAsync(root_url, imgInfo);
      PAGE_LOADED = index;
    }
  }

  /**
   * 循环数据,直至所有图片的 imageBase64 加载完全
   * @param {BzData} bzData
   * @param {number} min
   * @param {number} max
   */
  const watchImgInfoAsync = async (bzData, min, max) => {
    document.querySelector(`#${BtnID.downloadBtn}`).textContent = `Loading`;

    let is_done = false;
    let intervalID = setInterval(
      (bzData, is_done) => {
        // 检查是否加载完全
        for (let index = max; index >= min; index--) {
          if (bzData.imgInfoList[index][keyImageBase64] === "") break;
          else is_done = true;
        }
        // 更新进度
        let percentage = Math.round(
          (CounterForFileReader.count / (max - min + 1)) * 100
        );
        document.querySelector(
          `#${BtnID.downloadBtn}`
        ).textContent = `Loading ${percentage}%`;

        if (is_done) {
          // 下载开始
          clearInterval(intervalID);
          downloadZip(bzData, min, max);
        }
      },
      1500,
      bzData,
      is_done
    );
  };

  /**
   * 获取 Gif 图片的 base64 编码
   * @param {ImgInfo} imgInfo
   */
  const getGifBase64Async = async (imgInfo) => {
    try {
      let reader = new FileReader();
      reader.onloadend = function () {
        imgInfo[keyImageBase64] = reader.result;
        // 持续,直至更新计数
        let intervalID = setInterval(() => {
          if (CounterForFileReader.update()) clearInterval(intervalID);
        }, Math.round(Math.random() * 1000));
      };

      // 加载图片的 blob 类型数据
      if (imgInfo.imgType !== ".err") {
        let imgBlob = await fetch(imgInfo.imgUrl).then((respone) =>
          respone.blob()
        );
        reader.readAsDataURL(imgBlob); // 将 blob 数据转换成 DataURL 数据
      } else {
        reader.readAsDataURL(new Blob()); // 空文件
      }
    } catch (e) {
      console.error(e);
    }
  };

  /**
   * 批量下载图片
   * @param {BzData} bzData 图像数据
   * @param {number} min
   * @param {number} max
   */
  const downloadZip = async (bzData, min, max) => {
    console.log(bzData);
    document.querySelector(`#${BtnID.downloadBtn}`).textContent = "打包";

    const zip = new JSZip();
    // 图片 url json 文件
    let stringData = JSON.stringify(bzData, null, 2);
    zip.file(
      `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].json`,
      stringData
    );
    // 图片 zip
    const fileFolder = zip.folder(
      `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}]`
    ); // 创建 bzData.name_en 文件夹
    const fileList = [];
    for (let i = min; i <= max; i++) {
      let name = bzData.imgInfoList[i].imgName + bzData.imgInfoList[i].imgType;
      let imageBase64 = bzData.imgInfoList[i][keyImageBase64].substring(22); // 截取 data:image/png;base64, 后的数据
      fileList.push({ name: name, img: imageBase64 });
    }
    // 往 zip 中,添加每张图片数据
    for (let imgFile of fileList) {
      fileFolder.file(imgFile.name, imgFile.img, {
        base64: true,
      });
    }

    document.querySelector(`#${BtnID.downloadBtn}`).innerHTML =
      "浏览器酱<br />正在响应";

    zip.generateAsync({ type: "blob" }).then((content) => {
      // saveAs(
      //   content,
      //   `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].zip`
      // );
      const downloadUrl = URL.createObjectURL(content);
      console.log(downloadUrl);
      GM_download({
        url: downloadUrl,
        name: `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].zip`,
        saveAs: true,
        onload: () => {
          // 按钮还原
          document.querySelector(`#${BtnID.downloadBtn}`).textContent =
            BtnText.downloadBtn;
          document.querySelector(`#${BtnID.downloadBtn}`).disabled = false;
          IS_DOWNLOADING = false;
        },
        onerror: (error) => {
          console.log(error);
          // 按钮还原
          document.querySelector(`#${BtnID.downloadBtn}`).textContent =
            BtnText.downloadBtn;
          document.querySelector(`#${BtnID.downloadBtn}`).disabled = false;
          IS_DOWNLOADING = false;
        },
      });
    });
  };

  /**
   * 数据初始化,获取漫画名、页数、图片的 url
   */
  function initData() {
    let bzData = new BzData();
    let bzDataIterator;
    console.log(`CURRENT_URL:${CURRENT_URL}`);
    if (
      CURRENT_URL === CURRENT_URL_TYPE.gallery ||
      CURRENT_URL === CURRENT_URL_TYPE.testGallery
    ) {
      let coverUrl;
      // cover
      bzData.imgInfoList.push(new ImgInfo("cover"));
      const tag_div_main = document.querySelectorAll(
        "body > div.overlay > div.container > div.row.gallery_first > div"
      );
      // 获取漫画名

      bzData.name_en = tag_div_main[1].querySelector("h1").textContent;
      bzData.name_sub = tag_div_main[1].querySelector("p.subtitle").textContent;
      // 漫画名去特殊字符处理
      if (bzData.name_sub !== "") {
        bzData.name_sub = processFilename(bzData.name_sub);
      }
      if (bzData.name_en !== "") {
        bzData.name_en = processFilename(bzData.name_en);
      } else {
        bzData.name_en = bzData.name_sub;
      }

      // 获取页数
      let page_str = tag_div_main[1].querySelector("li.pages").textContent;
      bzData.page = Number.parseInt(page_str.match(/Pages: ([0-9]*)/i)[1]);

      // 图片序列的 url 前缀与封面 url 的相同,
      // eg.封面 url=https://m7.imhentai.xxx/023/mnsiote3jg/cover.jpg
      // eg.序列的 url=https://m7.imhentai.xxx/023/mnsiote3jg/
      coverUrl = tag_div_main[0].querySelector("img").dataset.src;
      bzData.root_url = coverUrl.slice(0, coverUrl.lastIndexOf("/") + 1);

      // 图片序列的 url 生成,
      // eg: https://m6.imhentai.xxx/021/fh5n1d304g/1.jpg
      for (let p = 1; p <= bzData.page; p++) {
        bzData.imgInfoList.push(new ImgInfo(p.toString())); // 图片名未编码,数字序列就行
      }

      bzDataIterator = BzDataIterator(bzData);
      // 初始化 cover 数据,让 CURRENT_PAGE 与 PAGE_LOADED 能够对齐
      let [index, root_url, coverInfo] = bzDataIterator.next().value;
      let ImgObj = new Image();
      ImgObj.src = coverUrl;
      ImgObj.onload = () => {
        coverInfo.width = ImgObj.width;
        coverInfo.height = ImgObj.height;
      };
      coverInfo.imgUrl = coverUrl;
      coverInfo.imgType = coverUrl
        .substring(coverUrl.lastIndexOf("."))
        .toLowerCase();

      // 在 gallary 页面保存数据,跳转翻页 view 页面后使用
      // https://m7.imhentai.xxx/024/1mezkq6gp4/cover.jpg
      let dataKey = coverUrl.substring("https://".length);
      dataKey = dataKey.substring(0, dataKey.lastIndexOf("/"));
      // dataKey = "m7.imhentai.xxx/024/1mezkq6gp4"
      GM_setValue(`BzData:${dataKey}`, bzData);
      // 测试用
      GM_setValue(`BzData:test`, bzData);
      console.log(`BzData:${dataKey}`);
      // alert(
      //   JSON.stringify(GM_getValue("BzData:m7.imhentai.xxx/024/1mezkq6gp4"))
      // );
    } else if (CURRENT_URL === CURRENT_URL_TYPE.testView) {
      bzData = GM_getValue("BzData:test");
      bzDataIterator = BzDataIterator(bzData);

      // 初始化 cover 数据,next() 让 CURRENT_PAGE 与 PAGE_LOADED 能够对齐
      let [index, root_url, coverInfo] = bzDataIterator.next().value;
      processImgInfoAsync(bzData.root_url, coverInfo);
    } else if (CURRENT_URL === CURRENT_URL_TYPE.imgPage) {
      let dataKey = window.location.href.substring("https://".length);
      dataKey = dataKey.substring(0, dataKey.lastIndexOf("/"));
      bzData = GM_getValue(`BzData:${dataKey}`);
      bzDataIterator = BzDataIterator(bzData);

      // 初始化 cover 数据,next() 让 CURRENT_PAGE 与 PAGE_LOADED 能够对齐
      let [index, root_url, coverInfo] = bzDataIterator.next().value;
      processImgInfoAsync(bzData.root_url, coverInfo);
    }
    console.log(JSON.stringify(bzData));
    console.log(bzData);
    return [bzData, bzDataIterator];
  }

  /**
   * 初始化组件
   * @param {BzData} bzData
   * @param {BzDataIterator} bzDataIterator
   */
  function initComponents(bzData, bzDataIterator) {
    // <img>
    const newImg = document.createElement("img");
    newImg.id = "can-img";
    newImg.style = `
      -webkit-user-select: none;
      margin:0 auto;
      transition: background-color 300ms;
    `;

    // <input>
    const changePageInput = document.createElement("input");
    changePageInput.id = "can-input";
    changePageInput.type = "number";
    changePageInput.value = `${CURRENT_PAGE}`;
    changePageInput.disabled = true;
    changePageInput.style = `
      width: 45%;height: 80%;
      font-size:18px;text-align:center;
    `;

    // <label>
    const pageLabel = document.createElement("label");
    pageLabel.id = "can-page";
    pageLabel.style =
      "width: 55%;height: 80%;font-size:18px;text-align:center;margin: 0;background-color: hsla(0, 0%, 90%, 90%);";
    pageLabel.textContent =
      PAGE_LOADED === bzData.page ? `${PAGE_LOADED}` : `${PAGE_LOADED}`;

    // <button>
    const runBtn = document.createElement("button");
    const previousBtn = document.createElement("button");
    const nextBtn = document.createElement("button");
    const downloadBtn = document.createElement("button");
    const scaleUpBtn = document.createElement("button");
    const scaleResetBtn = document.createElement("button");
    const scaleDownBtn = document.createElement("button");
    runBtn.id = BtnID.runBtn;
    previousBtn.id = BtnID.previousBtn;
    nextBtn.id = BtnID.nextBtn;
    downloadBtn.id = BtnID.downloadBtn;
    scaleUpBtn.id = BtnID.scaleUpBtn;
    scaleResetBtn.id = BtnID.scaleResetBtn;
    scaleDownBtn.id = BtnID.scaleDownBtn;
    runBtn.textContent = BtnText.runBtn;
    previousBtn.textContent = BtnText.previousBtn;
    nextBtn.textContent = BtnText.nextBtn;
    downloadBtn.textContent = BtnText.downloadBtn;
    scaleUpBtn.textContent = BtnText.scaleUpBtn;
    scaleResetBtn.textContent = BtnText.scaleResetBtn;
    scaleDownBtn.textContent = BtnText.scaleDownBtn;

    runBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();
      // 异步加载图片信息
      if (!IS_INIT) {
        IS_INIT = true;
        processImgAsync(bzDataIterator);
      }
      if (!IS_RUN) {
        IS_RUN = true;
        evt.target.textContent = `原页 ${bzData.page}`;
        // 生效按钮
        let btns = document
          .querySelector("#can-app")
          .querySelectorAll("button");
        for (const btn of btns) {
          btn.disabled = false;
        }
        let inputPage = document.querySelector("#can-input");
        inputPage.disabled = false;
        // 显示 新 <img>
        let _newImg = document.querySelector("#can-div-img");
        _newImg.style.display = "block";
      } else {
        IS_RUN = false;
        evt.target.textContent = BtnText.runBtn;
        // 无效按钮
        let btns = document
          .querySelector("#can-app")
          .querySelectorAll("button");
        for (const btn of btns) {
          btn.disabled =
            btn.id !== BtnID.runBtn && btn.id !== BtnID.downloadBtn
              ? true
              : false;
        }
        let inputPage = document.querySelector("#can-input");
        inputPage.disabled = true;
        // 隐藏新 <img>
        let _newImg = document.querySelector("#can-div-img");
        _newImg.style.display = "none";
      }
    });

    previousBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();
      let imgInfo =
        bzData.imgInfoList[
          CURRENT_PAGE > 0 ? --CURRENT_PAGE : (CURRENT_PAGE = MAX_BROWSE_PAGE)
        ];
      updateImgTag(imgInfo);
      let inputPage = document.querySelector("#can-input");
      let page_ = document.querySelector("#can-page");
      inputPage.value = CURRENT_PAGE;
      if (PAGE_LOADED !== bzData.page) {
        let percentage = Number.parseInt((PAGE_LOADED / bzData.page) * 100);
        if (percentage < 25) page_.textContent = `${PAGE_LOADED} ◕`;
        else if (percentage < 50) page_.textContent = `${PAGE_LOADED} ◑`;
        else if (percentage < 75) page_.textContent = `${PAGE_LOADED} ◔`;
        else if (percentage < 100) page_.textContent = `${PAGE_LOADED} ☯`;
      } else {
        page_.textContent = `${PAGE_LOADED}`;
      }
    });

    nextBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();
      let imgInfo =
        bzData.imgInfoList[
          CURRENT_PAGE < PAGE_LOADED
            ? ++CURRENT_PAGE
            : (CURRENT_PAGE = PAGE_LOADED !== bzData.page ? CURRENT_PAGE : 0) // 完全加载完前不会 '溢出跳 0'
        ];
      updateImgTag(imgInfo);
      let inputPage = document.querySelector("#can-input");
      let page_ = document.querySelector("#can-page");
      inputPage.value = CURRENT_PAGE;
      if (PAGE_LOADED !== bzData.page) {
        let percentage = Number.parseInt((PAGE_LOADED / bzData.page) * 100);
        if (percentage < 25) page_.textContent = `${PAGE_LOADED} ◕`;
        else if (percentage < 50) page_.textContent = `${PAGE_LOADED} ◑`;
        else if (percentage < 75) page_.textContent = `${PAGE_LOADED} ◔`;
        else if (percentage < 100) page_.textContent = `${PAGE_LOADED} ☯`;
      } else {
        page_.textContent = `${PAGE_LOADED}`;
      }

      if (MAX_BROWSE_PAGE < CURRENT_PAGE) MAX_BROWSE_PAGE = CURRENT_PAGE;
    });

    downloadBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();
      if (
        CURRENT_URL === CURRENT_URL_TYPE.gallery ||
        CURRENT_URL === CURRENT_URL_TYPE.testGallery
      ) {
        // 跳转到图片页面,再启动下载,避免 strict-origin-when-cross-origin
        GM_openInTab(bzData.imgInfoList[0].imgUrl, { active: true });
        return;
      }
      // 打包 zip
      if (!IS_DOWNLOADING) {
        document.querySelector(`#${BtnID.downloadBtn}`).disabled = true;
        IS_DOWNLOADING = true;
        UserCustemRange.page_loaded = PAGE_LOADED;
        if (UserCustemRange.page_loaded !== bzData.page) {
          let result = confirm(
            `当前${UserCustemRange.page_loaded}页,图片未加载完全,是否继续?🤨`
          );
          if (!result) {
            IS_DOWNLOADING = false;
            document.querySelector(`#${BtnID.downloadBtn}`).disabled = false;

            return;
          }
        }
        let result = prompt(
          "选择下载页面区间,请使用 [英文符号 - ] 隔开😇",
          `0-${UserCustemRange.page_loaded}`
        );
        if (result) {
          let rangeRegExp = result.match(/^(\d+)-(\d+)$/);
          if (rangeRegExp) {
            UserCustemRange.min = Number.parseInt(rangeRegExp[1]);
            UserCustemRange.max = Number.parseInt(rangeRegExp[2]);
            // 处理意外输入
            if (
              !rangeRegExp ||
              0 > UserCustemRange.min ||
              UserCustemRange.min > UserCustemRange.max ||
              UserCustemRange.max > UserCustemRange.page_loaded
            ) {
              alert("无效输入😥");
              IS_DOWNLOADING = false;
              document.querySelector(`#${BtnID.downloadBtn}`).disabled = false;

              return;
            }
          } else {
            alert("无效输入😥");
            IS_DOWNLOADING = false;
            document.querySelector(`#${BtnID.downloadBtn}`).disabled = false;

            return;
          }
          watchImgInfoAsync(bzData, UserCustemRange.min, UserCustemRange.max);
          // getImageBase64Async(bzData, UserCustemRange.min, UserCustemRange.max);
        } else {
          IS_DOWNLOADING = false;
          document.querySelector(`#${BtnID.downloadBtn}`).disabled = false;
        }
      }
    });

    scaleUpBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();
      SCALE += SCALE < 3 ? 0.1 : 0;
      let imgInfo = bzData.imgInfoList[CURRENT_PAGE];
      updateImgTag(imgInfo);
    });

    scaleResetBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();

      SCALE = 1;
      let imgInfo = bzData.imgInfoList[CURRENT_PAGE];
      updateImgTag(imgInfo);
    });

    scaleDownBtn.addEventListener("click", (evt) => {
      evt.stopPropagation();
      SCALE -= SCALE > 0.3 ? 0.1 : 0;
      let imgInfo = bzData.imgInfoList[CURRENT_PAGE];
      updateImgTag(imgInfo);
    });

    changePageInput.addEventListener("change", (evt) => {
      evt.stopPropagation();
      if (0 <= evt.target.value && evt.target.value <= bzData.page) {
        CURRENT_PAGE = evt.target.value;
        let imgInfo = bzData.imgInfoList[CURRENT_PAGE];
        updateImgTag(imgInfo);
      }
    });

    const app = document.createElement("div");
    app.id = "can-app";
    app.style = `
      font-size:20px; color:HotPink;
      width:120px; height:220px; background-color:hsla(0, 0%, 90%, 50%);
      display:flex; flex-direction:column; justify-content:space-between;
      position:fixed; top:40%; z-index:1000002; transform:translateX(calc(-50% * var(--direction))) translateY(-50%);
    `;
    const div_tool = document.createElement("div");
    div_tool.style = `
      display:flex; flex-direction:column; justify-content:space-between;
      height:180px; background-color:hsla(0, 0%, 90%, 50%);
    `;
    const div_scale = document.createElement("div");
    div_scale.style = `
      display:flex; flex-direction:row; justify-content:space-between;
    `;
    const div_page = document.createElement("div");
    div_page.style = `
      align-items:center;
      display:flex; flex-direction:row; justify-content:space-between;
    `;

    app.appendChild(runBtn);

    div_tool.appendChild(previousBtn);
    div_tool.appendChild(nextBtn);

    div_scale.appendChild(scaleUpBtn);
    div_scale.appendChild(scaleResetBtn);
    div_scale.appendChild(scaleDownBtn);
    div_tool.appendChild(div_scale);

    div_page.appendChild(changePageInput);
    div_page.appendChild(pageLabel);
    div_tool.appendChild(div_page);

    div_tool.appendChild(downloadBtn);
    app.appendChild(div_tool);

    document.body.appendChild(app);

    // 包裹 <img> 并悬浮居中
    const div_img = document.createElement("div");
    div_img.id = "can-div-img";

    // 粉色
    div_img.style = `
        display:none;
        position: fixed;overflow: auto;
        width: 80%;height: 100%;top: 0%;z-index: 1000001;
        left: 0;right: 0;margin:0 auto;text-align: center;
        background-color: hsla(338, 100%, 70%, 0.8);
      `;

    div_img.appendChild(newImg);
    document.body.appendChild(div_img);

    let btns = document.querySelector("#can-app").querySelectorAll("button");
    for (const btn of btns) {
      btn.style = "font-size:20px; color:HotPink;";
      btn.disabled =
        btn.id !== BtnID.runBtn && btn.id !== BtnID.downloadBtn ? true : false;
    }

    // canvas
    const cvs = document.createElement("canvas");
    cvs.id = "can-cvs";
    cvs.style = `display:none;`;
    document.body.appendChild(cvs);
  }

  // 更新 <img>
  function updateImgTag(imgInfo) {
    let div_img = document.querySelector("#can-div-img");
    let newImg_ = document.querySelector("#can-img");
    newImg_.src = imgInfo.imgUrl;
    newImg_.alt = imgInfo.imgAlt;
    if (imgInfo.imgType !== ".err") {
      newImg_.width = imgInfo.width * SCALE;
      newImg_.height = imgInfo.height * SCALE;
    } else {
      newImg_.style.removeProperty("width");
      newImg_.style.removeProperty("height");
    }
  }

  // 标识当前页面
  const currentUrl = window.location.href;
  if (currentUrl.match(/https:\/\/imhentai.xxx\/gallery\/\S*/g) !== null)
    CURRENT_URL = CURRENT_URL_TYPE.gallery;
  else if (currentUrl.match(/https:\/\/imhentai.xxx\/view\/\S*/g) !== null)
    CURRENT_URL = CURRENT_URL_TYPE.view;
  else if (currentUrl.match(/https:\/\/\w*.imhentai.xxx\/\S*/g) !== null)
    CURRENT_URL = CURRENT_URL_TYPE.imgPage;
  else if (currentUrl.match(/file:\/\/\/D:\/\S*\/\S*\/py\S*/g) !== null)
    CURRENT_URL = CURRENT_URL_TYPE.testGallery;
  else if (currentUrl.match(/https:\/\/www.anna\S*/g) !== null)
    CURRENT_URL = CURRENT_URL_TYPE.testView;
  else {
    CURRENT_URL = CURRENT_URL_TYPE.unknow;
    return;
  }
  // 启动
  initComponents(...initData());
})();