Greasy Fork

Greasy Fork is available in English.

For Imhentai

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

当前为 2023-06-09 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         For Imhentai
// @namespace    http://tampermonkey.net/
// @version      1.0
// @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_RUN = false;
  let IS_PAGE_LOADING = 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 PAGE_LOAD_STEP = IS_DEBUG ? 2 : 5; // 每次加载页数
  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) {
    let loadCount;
    for (let i = 0; i < PAGE_LOAD_STEP; i++) {
      const [index, root_url, imgInfo] = bzDataIterator.next().value;
      if (!index) break;
      await processImgInfoAsync(root_url, imgInfo);
      loadCount = index;
      if (IS_DEBUG) console.log(`${index}:`, imgInfo);
    }
    PAGE_LOADED = loadCount;
    IS_PAGE_LOADING = false;
  }

  // 获取漫画名、页数、图片的 url
  function initData() {
    let bzData = new BzData();
    let coverUrl;
    // cover
    if (!IS_DEBUG) {
      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} ]`;

    // <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_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;
      page_.textContent = `[ ${PAGE_LOADED} ]`;
    });
    // next 会触发图片的 lazy load
    nextBtn.addEventListener('click', (evt) => {
      evt.stopPropagation();
      // 如果
      // 未加锁 && 页码加载未完全 && 正访问超出已加载的页码
      // 添加步数个新 imgInfo
      if (
        !IS_PAGE_LOADING &&
        PAGE_LOADED !== bzData.page &&
        CURRENT_PAGE === PAGE_LOADED
      ) {
        IS_PAGE_LOADING = true;
        processImgAsync(bzDataIterator);
      } else {
        let imgInfo =
          bzData.imgInfoList[
            CURRENT_PAGE < PAGE_LOADED ? ++CURRENT_PAGE : (CURRENT_PAGE = 0)
          ];
        updateImgTag(imgInfo);
        let inputPage = document.querySelector('#can-input');
        let page_ = document.querySelector('#can-page');
        inputPage.value = CURRENT_PAGE;
        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());
})();