Greasy Fork

Greasy Fork is available in English.

For Imhentai

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

当前为 2023-08-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         For Imhentai
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  不翻墙下,更快加载 imhentai.xxx 的图片,并提供打包下载
// @author       水母
// @match        https://imhentai.xxx/gallery/*
// @match        https://*.imhentai.xxx/*
// @icon         https://imhentai.xxx/images/logo.png
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js#sha512-uVSVjE7zYsGz4ag0HEzfugJ78oHCI1KhdkivjQro8ABL/PRiEO4ROwvrolYAcZnky0Fl/baWKYilQfWvESliRA==
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_openInTab
// @license      MIT
// ==/UserScript==

(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;

  /**
   * @desc 标识当前在哪个页面
   */
  let CURRENT_URL;
  /**
   * @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 CanEleID = {
    /**
     * 主体
     */
    app: "can-app",
    runBtn: "can-run",
    previousBtn: "can-pre",
    nextBtn: "can-next",
    downloadBtn: "can-down",
    scaleUpBtn: "can-sUp",
    scaleResetBtn: "can-sReset",
    scaleDownBtn: "can-sDown",
    /**
     * 页码
     */
    pageLabel: "can-page",
    /**
     * 页码跳转
     */
    changePageInput: "can-input",
    /**
     * 图片显示
     */
    showImg: "can-img",
    /**
     * 图片显示外包 <div>
     */
    showImgDiv: "can-img-div",
    /**
     * 转 base64
     */
    canvas: "can-cvs",
  };
  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 keyImageBase64 = Symbol("imageBase64");
  const keyImageScale = Symbol("imageScale");
  /**
   *
   * @param {string} imgName
   * @param {string} imgUrl
   * @param {string} imgType
   * @param {number} width
   * @param {number} height
   * @param {number} scale 仅用于页面浏览
   * @param {string} imageBase64
   */
  function ImgInfo(
    imgName,
    imgUrl = "",
    imgType = "",
    width = 0,
    height = 0,
    scale = 1.0,
    imageBase64 = ""
  ) {
    this.imgName = imgName;
    this.imgUrl = imgUrl;
    this.imgType = imgType;
    this.width = width;
    this.height = height;
    this[keyImageScale] = scale;
    this[keyImageBase64] = imageBase64;
  }
  /**
   * 本子数据
   * @param {string} name_en
   * @param {string} name_sub
   * @param {number} page
   * @param {string} root_url
   * @param {ImgInfo[]} imgInfoList
   */
  function BzData(
    name_en = "Null",
    name_sub = "Null",
    page = 0,
    root_url = "",
    imgInfoList = []
  ) {
    this.name_en = name_en;
    this.name_sub = name_sub;
    this.page = page;
    this.root_url = root_url;
    this.imgInfoList = imgInfoList;
  }
  /**
   * 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];
    }
  }

  // <style>
  ((t) => {
    const e = document.createElement("style");
    e.textContent = t;
    document.head.append(e);
  })(
    `
    #${CanEleID.app} {
      top:40%; 
      width:120px; 
      height:200px;
      font-size:20px; 
      color: #d71989;
      background-color:hsla(0, 0%, 90%, 50%);
      display:flex; 
      flex-direction:column; 
      justify-content:space-between;
      position:fixed; 
      z-index:1000002; 
      transform:translateX(calc(-50% * var(--direction))) translateY(-50%);
    }
  
    .can-button-sm {
      height: 30px;
      font-size: 20px; 
      color: #d71989;
      flex: 1;
    }
  
    .can-button-lg {
      height: 34px;
      font-size:20px; 
      color: #d71989;
    }
  
    #${CanEleID.app} div {
      width: 100%;
    }

    #${CanEleID.app} svg {
      display: none;
      width: 24px;
      height: 24px;
    }

    #${CanEleID.showImg} {
      -webkit-user-select: none;
      margin:0 auto;
      transition: background-color 300ms;
    }
  
    #${CanEleID.changePageInput} {
      width: 90%;
      height: 24px;
      font-size:18px;
      text-align:center;
    }
  
    #${CanEleID.pageLabel} {
      font-size:18px;
      text-align:center;
      margin: 0px 3px;
      background-color: hsla(0, 0%, 90%, 90%);
      flex: 1;
    }
  
    #${CanEleID.showImgDiv} {
      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);
    }
    `
  );

  /**
   * 漫画名去特殊字符处理
   * @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 保存正确的 URL 和后缀格式,并生成 base64
   * @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
        // 避开站点的跨域策略
        if (
          CURRENT_URL !== CURRENT_URL_TYPE.gallery &&
          CURRENT_URL !== CURRENT_URL_TYPE.testGallery
        ) {
          // canvas 无法加载 gif
          if (type !== ".gif") {
            try {
              let c = document.createElement("canvas");
              let ctx = c.getContext("2d");
              c.height = ImgObj.naturalHeight;
              c.width = ImgObj.naturalWidth;
              ctx.drawImage(
                ImgObj,
                0,
                0,
                ImgObj.naturalWidth,
                ImgObj.naturalHeight
              );
              // 图片格式的mime类型:image/png, image/jpeg
              imgInfo[keyImageBase64] =
                type === ".jpg"
                  ? c.toDataURL("image/jpeg", 1.0)
                  : c.toDataURL();
            } catch (e1) {
              imgInfo[keyImageBase64] = "data:image/png;base64,null";
              console.log(`[ERR] ${imgInfo.imgUrl} 无法处理为 base64 : ${e1}`);
            }
          } else {
            getGifBase64Async(imgInfo);
          }
        }
        imgInfo.width = ImgObj.width;
        imgInfo.height = ImgObj.height;
        break; // 结束循环
      } catch (e2) {
        if (type !== ".err") {
          console.log(`[TEST] ${imgInfo.imgUrl} 不存在,尝试下一个扩展名`);
        } else {
          imgInfo[keyImageBase64] = "data:image/png;base64,null";
          console.log(`[ERR] ${imgInfo.imgUrl} 不存在`);
        }
        // 继续循环
      }
    }
  }

  /**
   * 处理所有图片
   * @param {BzDataIterator} bzDataIterator
   */
  async function processImgAsync(bzDataIterator) {
    let page_ = document.querySelector(`#${CanEleID.pageLabel}`);
    let div_img = document.querySelector(`#${CanEleID.showImgDiv}`);

    for (let [index, root_url, imgInfo] of bzDataIterator) {
      await processImgInfoAsync(root_url, imgInfo);
      updateImgInfoScale(imgInfo, false, div_img);
      PAGE_LOADED = index;
      page_.textContent = `${PAGE_LOADED}`;
    }
    document.querySelector(`#${CanEleID.app} svg`).style.display = "none";
  }

  /**
   * 获取图片的 base64 编码,此处指定 Gif ,其他格式由 canvas 方式获取
   * @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 类型数据
      let imgBlob = await fetch(imgInfo.imgUrl).then((respone) =>
        respone.blob()
      );
      reader.readAsDataURL(imgBlob); // 将 blob 数据转换成 DataURL 数据
    } 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(`#${CanEleID.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
    );
    // 创建图片文件夹
    const fileFolder = zip.folder(
      `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}]`
    );
    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(`#${CanEleID.downloadBtn}`).innerHTML =
      "<div style='font-size: 10px;'>浏览器酱正在响应</div>";

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

      // 在 gallary 页面保存数据,跳转 imgPage 页面后使用
      // https://m7.imhentai.xxx/023/mnsiote3jg/cover.jpg
      let dataKey = coverUrl.substring("https://".length);
      dataKey = dataKey.substring(0, dataKey.lastIndexOf("/"));
      // dataKey = "m7.imhentai.xxx/023/mnsiote3jg"
      GM_setValue(`${dataKey}`, bzData);
    } else if (
      CURRENT_URL === CURRENT_URL_TYPE.imgPage ||
      CURRENT_URL === CURRENT_URL_TYPE.testView
    ) {
      let dataKey = window.location.href.substring("https://".length);
      dataKey = dataKey.substring(0, dataKey.lastIndexOf("/"));
      bzData = GM_getValue(`${dataKey}`);
      if (!bzData) alert("数据为空,请先访问,预览页面 gallarg");
    }

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

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

    console.log(bzData);
    // alert(JSON.stringify(bzData));
    return [bzData, bzDataIterator];
  }

  /**
   * 初始化组件
   * @param {BzData} bzData
   * @param {BzDataIterator} bzDataIterator
   */
  function initComponents(bzData, bzDataIterator) {
    document.body.insertAdjacentHTML(
      "beforeend",
      `
      <div id="${CanEleID.app}">
        <button id="${CanEleID.runBtn}" class="can-button-lg">${BtnText.runBtn}</button>
        <div style="display: flex; flex-direction: column; justify-content: space-around; align-items: center; flex: 1;">
          <div style="display: flex; flex-direction: row;">
            <button id="${CanEleID.previousBtn}" class="can-button-sm" disabled>${BtnText.previousBtn}</button>
            <button id="${CanEleID.nextBtn}" class="can-button-sm" disabled>${BtnText.nextBtn}</button>
          </div>
          <div style="display: flex; flex-direction: row;">
            <button id="${CanEleID.scaleUpBtn}" class="can-button-sm" disabled>${BtnText.scaleUpBtn}</button>
            <button id="${CanEleID.scaleResetBtn}" class="can-button-sm" disabled>${BtnText.scaleResetBtn}</button>
            <button id="${CanEleID.scaleDownBtn}" class="can-button-sm" disabled>${BtnText.scaleDownBtn}</button>
          </div>
          <input id="${CanEleID.changePageInput}" value="0" disabled>
          <div style="display: flex; flex-direction: row; align-items: center;">
            <label id="${CanEleID.pageLabel}">0</label>
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="#ff7cc9">
              <path opacity=".25" d="M16 0 A16 16 0 0 0 16 32 A16 16 0 0 0 16 0 M16 4 A12 12 0 0 1 16 28 A12 12 0 0 1 16 4"/>
              <path d="M16 0 A16 16 0 0 1 32 16 L28 16 A12 12 0 0 0 16 4z">
                <animateTransform attributeName="transform" type="rotate" from="0 16 16" to="360 16 16" dur="0.8s" repeatCount="indefinite" />
              </path>
            </svg>
          </div>
        </div>
        <button id="${CanEleID.downloadBtn}" class="can-button-lg">${BtnText.downloadBtn}</button>
      </div>

      <div id="can-img-div" style="display: none;">
        <img id="can-img" alt="null">
      </div>

      <canvas id="can-cvs" style="display: none;"></canvas>
      `
    );

    document
      .querySelector(`#${CanEleID.runBtn}`)
      .addEventListener("click", (evt) => {
        evt.stopPropagation();
        if (!IS_INIT) {
          IS_INIT = true;

          // 初始显示封面
          document.querySelector(`#${CanEleID.showImgDiv}`).style.display =
            "block";
          updateImgInfoScale(bzData.imgInfoList[0]);
          updateShowImgTag(bzData.imgInfoList[0]);

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

    document
      .querySelector(`#${CanEleID.previousBtn}`)
      .addEventListener("click", (evt) => {
        evt.stopPropagation();
        let imgInfo =
          bzData.imgInfoList[
            CURRENT_PAGE > 0 ? --CURRENT_PAGE : (CURRENT_PAGE = MAX_BROWSE_PAGE)
          ];
        updateShowImgTag(imgInfo);
        let inputPage = document.querySelector(`#${CanEleID.changePageInput}`);
        let page_ = document.querySelector(`#${CanEleID.pageLabel}`);
        inputPage.value = CURRENT_PAGE;
      });

    document
      .querySelector(`#${CanEleID.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'
          ];
        updateShowImgTag(imgInfo);
        let inputPage = document.querySelector(`#${CanEleID.changePageInput}`);
        let page_ = document.querySelector(`#${CanEleID.pageLabel}`);
        inputPage.value = CURRENT_PAGE;
        if (MAX_BROWSE_PAGE < CURRENT_PAGE) MAX_BROWSE_PAGE = CURRENT_PAGE;
      });

    document
      .querySelector(`#${CanEleID.downloadBtn}`)
      .addEventListener("click", (evt) => {
        evt.stopPropagation();
        if (
          CURRENT_URL === CURRENT_URL_TYPE.gallery ||
          CURRENT_URL === CURRENT_URL_TYPE.testGallery
        ) {
          // 跳转到图片页面,再启动下载,避免 strict-origin-when-cross-origin
          console.log(`跳转至:${bzData.imgInfoList[0].imgUrl}`);
          GM_openInTab(bzData.imgInfoList[0].imgUrl, { active: true });
          return;
        }
        // 打包 zip
        if (!IS_DOWNLOADING) {
          IS_DOWNLOADING = true;
          document.querySelector(`#${CanEleID.downloadBtn}`).disabled = 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(
                `#${CanEleID.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(
                  `#${CanEleID.downloadBtn}`
                ).disabled = false;

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

              return;
            }
            downloadZip(bzData, UserCustemRange.min, UserCustemRange.max);
          } else {
            IS_DOWNLOADING = false;
            document.querySelector(`#${CanEleID.downloadBtn}`).disabled = false;
          }
        }
      });

    document
      .querySelector(`#${CanEleID.scaleUpBtn}`)
      .addEventListener("click", (evt) => {
        evt.stopPropagation();
        let imgInfo = bzData.imgInfoList[CURRENT_PAGE];
        imgInfo[keyImageScale] += 0.1;
        updateShowImgTag(imgInfo);
      });

    document
      .querySelector(`#${CanEleID.scaleResetBtn}`)
      .addEventListener("click", (evt) => {
        evt.stopPropagation();

        let imgInfo = bzData.imgInfoList[CURRENT_PAGE];

        // 切换 原始尺寸-平铺尺寸
        if (imgInfo[keyImageScale] !== 1.0) {
          imgInfo[keyImageScale] = 1.0;
        } else {
          updateImgInfoScale(imgInfo, true);
        }

        updateShowImgTag(imgInfo);
      });

    document
      .querySelector(`#${CanEleID.scaleDownBtn}`)
      .addEventListener("click", (evt) => {
        evt.stopPropagation();
        let imgInfo = bzData.imgInfoList[CURRENT_PAGE];
        imgInfo[keyImageScale] -= 0.1;
        updateShowImgTag(imgInfo);
      });

    document
      .querySelector(`#${CanEleID.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];
          updateShowImgTag(imgInfo);
        }
      });
  }

  /**
   * @desc 更新显示图片
   * @param {ImgInfo} imgInfo
   */
  function updateShowImgTag(imgInfo) {
    let newImg_ = document.querySelector(`#${CanEleID.showImg}`);
    newImg_.src = imgInfo.imgUrl;
    newImg_.alt = imgInfo.imgName + imgInfo.imgType;
    if (imgInfo.imgType !== ".err") {
      newImg_.width = imgInfo.width * imgInfo[keyImageScale];
      newImg_.height = imgInfo.height * imgInfo[keyImageScale];
    } else {
      newImg_.style.removeProperty("width");
      newImg_.style.removeProperty("height");
    }
  }

  /**
   * @desc 更新图片的缩放,自动平铺
   * @param {ImgInfo} imgInfo
   * @param {boolean} isUseToSm 是否将小图放大,默认:false
   * @param {HTMLDivElement} showImgDiv 包装 \<img\> 的 \<div\> 元素对象
   */
  function updateImgInfoScale(imgInfo, isUseToSm = false, showImgDiv = null) {
    let div_img = showImgDiv
      ? showImgDiv
      : document.querySelector(`#${CanEleID.showImgDiv}`);
    let HScale = imgInfo.height / div_img.offsetHeight;
    let WScale = imgInfo.width / div_img.offsetWidth;
    if (
      imgInfo.height > div_img.offsetHeight ||
      imgInfo.width > div_img.offsetWidth
    ) {
      if (HScale > 1 && HScale > WScale) imgInfo[keyImageScale] = 1 / HScale;
      if (WScale > 1 && WScale > HScale) imgInfo[keyImageScale] = 1 / WScale;
      // 误差
      imgInfo[keyImageScale] -= 0.003;
    } else {
      if (isUseToSm) {
        if (HScale < 1 && HScale > WScale) imgInfo[keyImageScale] = 1 / HScale;
        if (WScale < 1 && WScale > HScale) imgInfo[keyImageScale] = 1 / WScale;
      }
    }
  }

  // 标识当前页面
  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;
  }
  // 清除脚本存储数据
  (() => {
    // 当所有页面关闭时,才清除数据;同时段浏览多个页面时,数据应该缓存;Be like 共享指针
    // 直接关闭浏览器,unload 不会触发;使用变化的 LinkCountSign,保证某个未来时间会清除,依赖用户操作
    const dateNow = new Date();
    const LinkCountSign = `LC:${dateNow.getDate()}`;
    GM_setValue(LinkCountSign, GM_getValue(LinkCountSign, 0) + 1);
    window.addEventListener("unload", (evt) => {
      GM_setValue(LinkCountSign, GM_getValue(LinkCountSign, 0) - 1);
      if (GM_getValue(LinkCountSign, 0) <= 0) {
        let expireData = GM_listValues();
        expireData.forEach((data) => GM_deleteValue(data));
      }
    });
  })();
  // 脚本~ 启动!
  initComponents(...initData());
})();