LibImgDown

WEBのダウンロードライブラリ

Tính đến 06-03-2025. Xem phiên bản mới nhất.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greatest.deepsurf.us/scripts/528949/1548368/LibImgDown.js

/*
* 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;
  let createFolder = false;
  let folderName = "images";
  let zipFileName = "download.zip";

  // elements
  let startNumInputElement = null;
  let endNumInputElement = null;
  let downloadButtonElement = null;
  let panelElement = null;
  let folderRadioYes = null;
  let folderRadioNo = null;
  let folderNameInput = null;
  let zipFileNameInput = 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, title);

    // 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, title) {
    // common input element style
    const inputElementStyle = `
      box-sizing: content-box;
      padding: 0px 0px;
      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;
      margin-left: auto; // 追加
      width: 128px;
      height: 48px;
      display: block;
      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;
    `;

    const toggleButton = document.createElement('button');
    toggleButton.id = 'ImageDownloader-ToggleButton';
    toggleButton.textContent = 'UI CLOSE';
    toggleButton.style = `
      position: fixed;
      top: 45px;
      left: 5px;
      z-index: 999999999;
      padding: 2px 5px;
      font-size: 14px;
      font-weight: 'bold';
      font-family: 'Monaco', 'Microsoft YaHei';
      color: #fff;
      background-color: #0984e3;
      border: 1px solid #aaa;
      border-radius: 4px;
      cursor: pointer;
    `;
    document.body.appendChild(toggleButton);

    let isUIVisible = true;

    function toggleUI() {
      if (isUIVisible) {
        panelElement.style.display = 'none';
        toggleButton.textContent = 'UI OPEN';
      } else {
        panelElement.style.display = 'flex';
        toggleButton.textContent = 'UI CLOSE';
      }
      isUIVisible = !isUIVisible;
    }

    toggleButton.addEventListener('click', toggleUI)

    // 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 range input container element
    const rangeInputRadioElement = document.createElement('div');
    rangeInputRadioElement.id = 'ImageDownloader-RadioChecker';
    rangeInputRadioElement.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: 80px;
      left: 5px;
      z-index: 999999999;
      box-sizing: border-box;
      padding: 0px;
      width: auto;
      min-width: 200px;
      max-width: 300px;
      height: auto;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: baseline;
      font-size: 12px;
      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;
      }
    }

    // create folder radio buttons
    folderRadioYes = document.createElement('input');
    folderRadioYes.type = 'radio';
    folderRadioYes.name = 'createFolder';
    folderRadioYes.value = 'yes';
    folderRadioYes.id = 'createFolderYes';

    folderRadioNo = document.createElement('input');
    folderRadioNo.type = 'radio';
    folderRadioNo.name = 'createFolder';
    folderRadioNo.value = 'no';
    folderRadioNo.id = 'createFolderNo';
    folderRadioNo.checked = true;

    // フォルダ名入力欄の作成
    folderNameInput = document.createElement('textarea');
    folderNameInput.id = 'folderNameInput';
    folderNameInput.value = title; // titleを初期値として使用
    folderNameInput.disabled = true;
    folderNameInput.style = `
      ${inputElementStyle}
      resize: vertical;
      height: auto;
      width: 99%;
      min-height: 45px;
      max-height: 200px;
      padding: 0px 0px;
      border: 1px solid #aaa;
      border-radius: 1px;
      font-size: 11px;
      font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
      text-align: left;
    `;

    // ZIPファイル名入力欄の作成
    zipFileNameInput = document.createElement('textarea');
    zipFileNameInput.id = 'zipFileNameInput';
    zipFileNameInput.value = `${title}.zip`; // titleを使用してZIPファイル名を設定
    zipFileNameInput.style = `
      ${inputElementStyle}
      resize: vertical;
      height: auto;
      width: 99%;
      min-height: 45px;
      max-height: 200px;
      padding: 0px 0px;
      border: 1px solid #aaa;
      border-radius: 1px;
      font-size: 11px;
      font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
      text-align: left;
    `;

    // add event listeners for radio buttons
    folderRadioYes.addEventListener('change', () => {
      createFolder = true;
      folderNameInput.disabled = false;
    });

    folderRadioNo.addEventListener('change', () => {
      createFolder = false;
      folderNameInput.disabled = true;
    });

    // assemble and then insert into document
    rangeInputContainerElement.appendChild(startNumInputElement);
    rangeInputContainerElement.appendChild(toSpanElement);
    rangeInputContainerElement.appendChild(endNumInputElement);
    panelElement.appendChild(rangeInputContainerElement);
    rangeInputRadioElement.appendChild(document.createTextNode('フォルダ:'));
    rangeInputRadioElement.appendChild(folderRadioYes);
    rangeInputRadioElement.appendChild(document.createTextNode('作成 '));
    rangeInputRadioElement.appendChild(folderRadioNo);
    rangeInputRadioElement.appendChild(document.createTextNode('不要'));
    panelElement.appendChild(rangeInputRadioElement);
    panelElement.appendChild(document.createTextNode('フォルダ名: '));
    panelElement.appendChild(folderNameInput);
    panelElement.appendChild(document.createElement('br'));
    panelElement.appendChild(document.createTextNode('ZIPファイル名: '));
    panelElement.appendChild(zipFileNameInput);
    panelElement.appendChild(document.createElement('br'));
    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.greatest.deepsurf.us/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();
    folderName = folderNameInput.value;
    zipFileName = zipFileNameInput.value;

    folderName = folderName.trim()
    folderName = folderName.replace(/[A-Za-z0-9]/g, function(s) {
        return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
    });
    folderName = folderName.replace(/ /g, ' ');
    folderName = folderName.replace(/[!?][!?]/g, '⁉');
    folderName = folderName.replace(/[!#$%&()+*]/g, function(s) {
        return '!#$%&()+*'['!#$%&()+*'.indexOf(s)];
    });
    folderName = folderName.replace(/[\\/:*?"<>|]/g, '-');

    zipFileName = zipFileName.trim()
    zipFileName = zipFileName.replace(/[A-Za-z0-9]/g, function(s) {
        return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
    });
    zipFileName = zipFileName.replace(/ /g, ' ');
    zipFileName = zipFileName.replace(/[!?][!?]/g, '⁉');
    zipFileName = zipFileName.replace(/[!#$%&()+*]/g, function(s) {
        return '!#$%&()+*'['!#$%&()+*'.indexOf(s)];
    });
    zipFileName = zipFileName.replace(/[\\/:*?"<>|]/g, '-');

    if (createFolder) {
      const folder = zip.folder(folderName);
      for (const [index, image] of images.entries()) {
        const filename = `${String(index + 1).padStart(images.length >= 100 ? String(images.length).length : 2, '0')}.${imageSuffix}`;
        folder.file(filename, image, zipOptions);
      }
    } else {
      for (const [index, image] of images.entries()) {
        const filename = `${String(index + 1).padStart(images.length >= 100 ? String(images.length).length : 2, '0')}.${imageSuffix}`;
        zip.file(filename, image, zipOptions);
      }
    }

    // start zipping & show progress
    const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `Zipping(${metadata.percent.toFixed()}%)`; };
    const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);
    
    // open 'Save As' window to save
    saveAs(content, zipFileName);
    
    // 全て完了
    downloadButtonElement.textContent = "Completed";

    // ボタンを再度押せるようにする
    downloadButtonElement.disabled = false;
    downloadButtonElement.style.backgroundColor = '#0984e3';
    downloadButtonElement.style.cursor = 'pointer';
  }

  // handle promise fulfilled
  function fulfillHandler(res) {
    if (!isErrorOccurred) {
      fulfillCount++;
      downloadButtonElement.innerHTML = `Processing(${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);