ImageDownloaderLib.la

Image downloader for manga download scripts.

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/469732/1404432/ImageDownloaderLibla.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

/*
 * Dependencies:
 *
 * GM_info(optional)
 * Docs: https://violentmonkey.github.io/api/gm/#gm_info
 * 
 * GM_xmlhttpRequest(optional)
 * Docs: https://violentmonkey.github.io/api/gm/#gm_xmlhttprequest
 *
 * JSZIP
 * Github: https://github.com/Stuk/jszip
 * CDN: https://unpkg.com/[email protected]/dist/jszip.min.js
 *
 * FileSaver
 * Github: https://github.com/eligrey/FileSaver.js
 * CDN: https://unpkg.com/[email protected]/dist/FileSaver.min.js
 */

;const ImageDownloader = (({ JSZip, saveAs }) => {
  let maxNum = 0;
  let promiseCount = 0;
  let fulfillCount = 0;
  let isErrorOccurred = false;

  // elements
  let startNumInputElement = null;
  let endNumInputElement = null;
  let downloadButtonElement = null;
  let panelElement = null;

  // svg icons
  const externalLinkSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16"><path fill-rule="evenodd" d="M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z"></path></svg>`;
  const reloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>`;

  // initialization
  function init({
    maxImageAmount,
    getImagePromises,
    title = `package_${Date.now()}`,
    imageSuffix = 'jpg',
    zipOptions = {},
    positionOptions = {}
  }) {
    // assign value
    maxNum = maxImageAmount;

    // setup UI
    setupUI(positionOptions);

    // setup update notification
    setupUpdateNotification();

    // add click event listener to download button
    downloadButtonElement.onclick = function () {
      if (!isOKToDownload()) return;

      this.disabled = true;
      this.textContent = "Processing";
      this.style.backgroundColor = '#aaa';
      this.style.cursor = 'not-allowed';
      download(getImagePromises, title, imageSuffix, zipOptions);
    }
  }

  // setup UI
  function setupUI(positionOptions) {
    // common input element style
    const inputElementStyle = `
      box-sizing: content-box;
      padding: 1px 2px;
      width: 40%;
      height: 26px;

      border: 1px solid #aaa;
      border-radius: 4px;

      font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
      text-align: center;
    `;

    // create start number input element
    startNumInputElement = document.createElement('input');
    startNumInputElement.id = 'ImageDownloader-StartNumInput';
    startNumInputElement.style = inputElementStyle;
    startNumInputElement.type = 'text';
    startNumInputElement.value = 1;

    // create end number input element
    endNumInputElement = document.createElement('input');
    endNumInputElement.id = 'ImageDownloader-EndNumInput';
    endNumInputElement.style = inputElementStyle;
    endNumInputElement.type = 'text';
    endNumInputElement.value = maxNum;

    // prevent keyboard input from being blocked
    startNumInputElement.onkeydown = (e) => e.stopPropagation();
    endNumInputElement.onkeydown = (e) => e.stopPropagation();

    // create 'to' span element
    const toSpanElement = document.createElement('span');
    toSpanElement.id = 'ImageDownloader-ToSpan';
    toSpanElement.textContent = 'to';
    toSpanElement.style = `
      margin: 0 6px;
      color: black;
      line-height: 1;
      word-break: keep-all;
      user-select: none;
    `;

    // create download button element
    downloadButtonElement = document.createElement('button');
    downloadButtonElement.id = 'ImageDownloader-DownloadButton';
    downloadButtonElement.textContent = 'Download';
    downloadButtonElement.style = `
      margin-top: 8px;
      width: 128px;
      height: 48px;

      display: flex;
      justify-content: center;
      align-items: center;

      font-size: 14px;
      font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
      color: #fff;
      line-height: 1.2;

      background-color: #0984e3;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    `;

    // create range input container element
    const rangeInputContainerElement = document.createElement('div');
    rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
    rangeInputContainerElement.style = `
      display: flex;
      justify-content: center;
      align-items: baseline;
    `;

    // create panel element
    panelElement = document.createElement('div');
    panelElement.id = 'ImageDownloader-Panel';
    panelElement.style = `
      position: fixed;
      top: 72px;
      left: 72px;
      z-index: 999999999;

      box-sizing: border-box;
      padding: 8px;
      width: 146px;
      height: 106px;

      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: baseline;

      font-size: 14px;
      font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
      letter-spacing: normal;

      background-color: #f1f1f1;
      border: 1px solid #aaa;
      border-radius: 4px;
    `;

    // modify panel position according to 'positionOptions'
    for (const [key, value] of Object.entries(positionOptions)) {
      if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
        panelElement.style[key] = value;
      }
    }

    // assemble and then insert into document
    rangeInputContainerElement.appendChild(startNumInputElement);
    rangeInputContainerElement.appendChild(toSpanElement);
    rangeInputContainerElement.appendChild(endNumInputElement);
    panelElement.appendChild(rangeInputContainerElement);
    panelElement.appendChild(downloadButtonElement);
    document.body.appendChild(panelElement);
  }

  // setup update notification
  async function setupUpdateNotification() {
    if (typeof GM_info === 'undefined' || typeof GM_xmlhttpRequest === 'undefined') return;

    // get local version
    const localVersion = Number(GM_info.script.version);

    // get latest version
    const scriptID = (GM_info.script.homepageURL || GM_info.script.homepage).match(/scripts\/(?<id>\d+)-/)?.groups?.id;
    const scriptURL = `https://update.greasyfork.org/scripts/${scriptID}/raw.js`;
    const latestVersionString = await new Promise(resolve => {
      GM_xmlhttpRequest({
        method: 'GET',
        url: scriptURL,
        responseType: 'text',
        onload: res => resolve(res.response.match(/@version\s+(?<version>[0-9\.]+)/)?.groups?.version)
      });
    });
    const latestVersion = Number(latestVersionString);

    if (Number.isNaN(localVersion) || Number.isNaN(latestVersion)) return;
    if (latestVersion <= localVersion) return;

    // show update notification
    const updateLinkElement = document.createElement('a');
    updateLinkElement.id = 'ImageDownloader-UpdateLink';
    updateLinkElement.href = scriptURL.replace('raw.js', 'raw.user.js');
    updateLinkElement.innerHTML = `Update to V${latestVersionString}${externalLinkSVG}`;
    updateLinkElement.style = `
      position: absolute;
      bottom: -38px;
      left: -1px;

      display: flex;
      justify-content: space-around;
      align-items: center;

      box-sizing: border-box;
      padding: 8px;
      width: 146px;
      height: 32px;

      font-size: 14px;
      font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
      text-decoration: none;
      color: white;

      background-color: #32CD32;
      border-radius: 4px;
    `;
    updateLinkElement.onclick = () => setTimeout(() => {
      updateLinkElement.removeAttribute('href');
      updateLinkElement.innerHTML = `Please Reload${reloadSVG}`;
      updateLinkElement.style.cursor = 'default';
    }, 1000);

    panelElement.appendChild(updateLinkElement);
  }

  // check validity of page nums from input
  function isOKToDownload() {
    const startNum = Number(startNumInputElement.value);
    const endNum = Number(endNumInputElement.value);

    if (Number.isNaN(startNum) || Number.isNaN(endNum)) { alert("请正确输入数值\nPlease enter page number correctly."); return false; }
    if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) { alert("请正确输入数值\nPlease enter page number correctly."); return false; }
    if (startNum < 1 || endNum < 1) { alert("页码的值不能小于1\nPage number should not smaller than 1."); return false; }
    if (startNum > maxNum || endNum > maxNum) { alert(`页码的值不能大于${maxNum}\nPage number should not bigger than ${maxNum}.`); return false; }
    if (startNum > endNum) { alert("起始页码的值不能大于终止页码的值\nNumber of start should not bigger than number of end."); return false; }

    return true;
  }

  // start downloading
  async function download(getImagePromises, title, imageSuffix, zipOptions) {
    const startNum = Number(startNumInputElement.value);
    const endNum = Number(endNumInputElement.value);
    promiseCount = endNum - startNum + 1;

    // start downloading images, max amount of concurrent requests is limited to 4
    let images = [];
    for (let num = startNum; num <= endNum; num += 4) {
      const from = num;
      const to = Math.min(num + 3, endNum);
      try {
        const result = await Promise.all(getImagePromises(from, to));
        images = images.concat(result);
      } catch (error) {
        return; // cancel downloading
      }
    }

    // configure file structure of zip archive
    JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
    const zip = new JSZip();
    const title0 = title.replaceAll(/(\\|\*|\"|\<|\>|\|)/g, '_'); // remove some characters
    let zipTitle = title0.replace(/(\?|\~|\/|\:)/gi,  function ($0, $1) {
        return {
            '?':'?',
            '~':'~',
            '/':'/',
            ':':':',
            }[$1];
            });
    const folder = zip.folder(zipTitle);
    for (const [index, image] of images.entries()) {
      const filename = `${String(index + 1).padStart(4, '0')}.${imageSuffix}`;
      folder.file(filename, image, zipOptions);
    }

    // start zipping & show progress
    const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `Zipping<br>(${metadata.percent.toFixed()}%)`; }
    const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);

    // open 'Save As' window to save
    saveAs(content, `${zipTitle}.zip`);

    // all completed
    downloadButtonElement.textContent = "Completed";
  }

  // handle promise fulfilled
  function fulfillHandler(res) {
    if (!isErrorOccurred) {
      fulfillCount++;
      downloadButtonElement.innerHTML = `Processing<br>(${fulfillCount}/${promiseCount})`;
    }

    return res;
  }

  // handle promise rejected
  function rejectHandler(err) {
    isErrorOccurred = true;
    console.error(err);

    downloadButtonElement.textContent = 'Error Occurred';
    downloadButtonElement.style.backgroundColor = 'red';

    return Promise.reject(err);
  }

  return { init, fulfillHandler, rejectHandler };
})(window);