Greasy Fork

For Imhentai

不翻墙下,更快加载 imhentai.xxx 的图片

目前为 2023-06-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         For Imhentai
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  不翻墙下,更快加载 imhentai.xxx 的图片
// @author       水母
// @match        https://imhentai.xxx/gallery/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // Your code here...
  const IS_DEBUG = false;
  // 全局数据
  let IS_INIT = false;
  let IS_RUN = false;
  // 页码序列 [cover.jpg, 1.jpg, ..., 30.jpg] : 页数总数定为 30; 数组 [0] 定为 cover; cover 不一定加载
  let CURRENT_PAGE = 0; // 当前页码
  let MAX_BROWSE_PAGE = 0; // 最大浏览的页码,只增
  let PAGE_LOADED = 0; // 已加载的页码
  let SCALE = IS_DEBUG ? 0.3 : 1; // 图片整体缩放
  // class
  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;
  }
  // imgInfoList[ImgInfo]
  function ImgInfo(
    imgName,
    imgAlt,
    imgUrl = '',
    imgType = '',
    width = 0,
    height = 0,
    SCALE = 1
  ) {
    this.imgName = imgName;
    this.imgAlt = !imgAlt ? imgName : imgAlt;
    this.imgUrl = imgUrl;
    this.imgType = imgType;
    this.width = width;
    this.height = height;
    this.SCALE = SCALE;
  }
  // 迭代器
  function* BzDataIterator(bzData) {
    let index = 0;
    while (index < bzData.imgInfoList.length) {
      let imgInfo = bzData.imgInfoList[index];
      yield [index++, bzData.root_url, imgInfo];
    }
    yield [undefined, undefined, undefined];
  }
  // let bzDataIterator;

  //漫画名去特殊字符处理
  function processFilename(filename) {
    return filename
      .replaceAll('\\', '-')
      .replaceAll('/', '-')
      .replaceAll(':', ':')
      .replaceAll('*', '-')
      .replaceAll('?', '?')
      .replaceAll('"', '“')
      .replaceAll('<', '《')
      .replaceAll('>', '》')
      .replaceAll('|', '~');
  }
  // 判断图片 url 有效与否
  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
  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);
        imgInfo.width = ImgObj.width;
        imgInfo.height = ImgObj.height;
        // console.log(imgInfo);
        break;
      } catch (e) {
        continue; // 未测试最后一个,继续
      }
    }
  }

  // 迭代器处理图片序列
  async function processImgAsync(bzDataIterator) {
    for (let [index, root_url, imgInfo] of bzDataIterator) {
      await processImgInfoAsync(root_url, imgInfo);
      PAGE_LOADED = index;
      if (IS_DEBUG) console.log(`${index}:`, imgInfo);
    }
  }

  // 获取漫画名、页数、图片的 url
  function initData() {
    let bzData = new BzData();
    let coverUrl;
    // cover
    bzData = new BzData();
    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_en !== '') {
      bzData.name_en = processFilename(bzData.name_en);
    }
    if (bzData.name_sub !== '') {
      bzData.name_sub = processFilename(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())); // 图片名未编码,数字序列就行
    }

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

    if (IS_DEBUG) console.log(coverInfo);
    return [bzData, 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 = 'run';
    previousBtn.id = 'pre';
    nextBtn.id = 'next';
    downloadBtn.id = 'down';
    scaleUpBtn.id = 'sUp';
    scaleResetBtn.id = 'sReset';
    scaleDownBtn.id = 'sDown';
    runBtn.textContent = '启动';
    previousBtn.textContent = '◁';
    nextBtn.textContent = '▷';
    downloadBtn.textContent = '辅助下载';
    scaleUpBtn.textContent = '△';
    scaleResetBtn.textContent = '○';
    scaleDownBtn.textContent = '▽';

    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 = '启动';
        // 无效按钮
        let btns = document
          .querySelector('#can-app')
          .querySelectorAll('button');
        for (const btn of btns) {
          btn.disabled = btn.id !== 'run' ? 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();
      const stringData = JSON.stringify(bzData, null, 2);
      // dada 表示要转换的字符串数据,type 表示要转换的数据格式
      const blob = new Blob([stringData], {
        type: 'application/json',
      });
      // 根据 blob生成 url链接
      const objectURL = URL.createObjectURL(blob);
      // 通过 a 标签下载
      const aTag = document.createElement('a');
      aTag.href = objectURL;
      aTag.download = 'PicLinks.json';
      aTag.click();
      aTag.remove();
      // 释放 URL 对象
      URL.revokeObjectURL(objectURL);
    });

    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:200px; 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:160px; 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);

    // loading 初始隐藏
    // document.body.insertAdjacentHTML('beforeend', LOADING_SVG);

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

  // 更新 <img>
  function updateImgTag(imgInfo) {
    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');
    }
  }

  initComponents(...initData());
})();