LibImgDown

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

目前為 2025-03-25 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/528949/1559633/LibImgDown.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

/*
 * 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";
    let zip = null; // ZIPオブジェクトの初期化
    let imageDataArray = []; //imageDataArrayの初期化
    // 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;

    // 初期化関数
    function init({
        maxImageAmount,
        getImagePromises,
        title = `package_${Date.now()}`,
        WidthText = 0,
        HeightText = 0,
        imageSuffix = 'jpg',
        zipOptions = {},
        positionOptions = {}
    }) {
        // 値を割り当てる
        maxNum = maxImageAmount;
        // UIをセットアップする
        setupUI(positionOptions, title, WidthText, HeightText);
        // ダウンロードボタンにクリックイベントリスナーを追加
        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);
        };
    }

// スタイルを定義
const style = document.createElement('style');
style.textContent = `
    .input-element {
        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;
    }
    .button-element {
        margin-top: 8px;
        margin-left: auto;
        width: 128px;
        height: 48px;
        padding: 5px 5px;
        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;
    }
    .toggle-button {
        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: #000000;
        border: 1px solid #aaa;
        border-radius: 4px;
        cursor: pointer;
    }
    .panel-element {
        position: fixed;
        top: 80px;
        left: 5px;
        z-index: 999999999;
        box-sizing: border-box;
        padding: 0px;
        width: auto;
        min-width: 400px;
        max-width: 600px;
        height: auto;
        display: none;
        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;
    }
    .range-container, .radio-container {
        display: flex;
        justify-content: center;
        align-items: baseline;
    }
    .textarea-element {
        box-sizing: content-box;
        padding: 0px 0px;
        width: 99%;
        min-height: 45px;
        max-height: 200px;
        border: 1px solid #aaa;
        border-radius: 1px;
        font-size: 11px;
        font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
        text-align: left;
        resize: vertical;
        height: auto;
    }
    .to-span {
        margin: 0 6px;
        color: black;
        line-height: 1;
        word-break: keep-all;
        user-select: none;
    }
`;
document.head.appendChild(style);

// UIセットアップ関数
function setupUI(positionOptions, title, WidthText, HeightText) {
    title = sanitizeFileName(title);
    // 開始番号入力欄の作成
    startNumInputElement = document.createElement('input');
    startNumInputElement.id = 'ImageDownloader-StartNumInput';
    startNumInputElement.className = 'input-element';
    startNumInputElement.type = 'text';
    startNumInputElement.value = 1;
    // 終了番号入力欄の作成
    endNumInputElement = document.createElement('input');
    endNumInputElement.id = 'ImageDownloader-EndNumInput';
    endNumInputElement.className = 'input-element';
    endNumInputElement.type = 'text';
    endNumInputElement.value = maxNum;
    // キーボード入力がブロックされないようにする
    startNumInputElement.onkeydown = (e) => e.stopPropagation();
    endNumInputElement.onkeydown = (e) => e.stopPropagation();
    // 「to」スパン要素の作成
    const toSpanElement = document.createElement('span');
    toSpanElement.id = 'ImageDownloader-ToSpan';
    toSpanElement.className = 'to-span';
    toSpanElement.textContent = 'から'; // to → から
    // ダウンロードボタン要素の作成
    downloadButtonElement = document.createElement('button');
    downloadButtonElement.id = 'ImageDownloader-DownloadButton';
    downloadButtonElement.className = 'button-element';
    downloadButtonElement.textContent = 'ダウンロード'; // Download → ダウンロード
    // トグルボタンの作成
    const toggleButton = document.createElement('button');
    toggleButton.id = 'ImageDownloader-ToggleButton';
    toggleButton.className = 'toggle-button';
    toggleButton.textContent = 'UI OPEN';
    document.body.appendChild(toggleButton);
    let isUIVisible = false; // 初期状態を非表示に設定
    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);
    // 範囲入力コンテナ要素の作成
    const rangeInputContainerElement = document.createElement('div');
    rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
    rangeInputContainerElement.className = 'range-container';
    // ラジオボタンコンテナ要素の作成
    const rangeInputRadioElement = document.createElement('div');
    rangeInputRadioElement.id = 'ImageDownloader-RadioChecker';
    rangeInputRadioElement.className = 'radio-container';
    // パネル要素の作成
    panelElement = document.createElement('div');
    panelElement.id = 'ImageDownloader-Panel';
    panelElement.className = 'panel-element';
    // 「positionOptions」に従ってパネルの位置を変更する。
    for (const [key, value] of Object.entries(positionOptions)) {
        if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
            panelElement.style[key] = value;
        }
    }

    // フォルダラジオボタンを作成
    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.className = 'textarea-element';
    folderNameInput.value = title; // 初期値としてタイトルを使用
    folderNameInput.disabled = true;
    // ZIPファイル名入力欄の作成
    zipFileNameInput = document.createElement('textarea');
    zipFileNameInput.id = 'zipFileNameInput';
    zipFileNameInput.className = 'textarea-element';
    zipFileNameInput.value = `${title}.zip`; // titleを使用してZIPファイル名を設定
    // ラジオボタンのイベントリスナーを追加
    folderRadioYes.addEventListener('change', () => {
        createFolder = true;
        folderNameInput.disabled = false; // フォルダ名入力欄を有効化
    });
    folderRadioNo.addEventListener('change', () => {
        createFolder = false;
        folderNameInput.disabled = true; // フォルダ名入力欄を無効化
    });
    // 組み立ててドキュメントに挿入
    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.createTextNode('ZIPファイル名: '));
    panelElement.appendChild(zipFileNameInput);
    panelElement.appendChild(document.createTextNode(` サイズ: ${WidthText} x `));
    panelElement.appendChild(document.createTextNode(`${HeightText}`));
    panelElement.appendChild(downloadButtonElement);
    document.body.appendChild(panelElement);
}

    // ページ番号が正しいか確認する関数
    function isOKToDownload() {
        const startNum = Number(startNumInputElement.value);
        const endNum = Number(endNumInputElement.value);

        if (Number.isNaN(startNum) || Number.isNaN(endNum)) {
            alert("正しい値を入力してください。\nPlease enter page numbers correctly.");
            return false;
        }
        if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) {
            alert("ページ番号は整数である必要があります。\nPage numbers must be integers.");
            return false;
        }
        if (startNum < 1 || endNum < 1) {
            alert("ページ番号は1以上である必要があります。\nPage numbers must be greater than or equal to 1.");
            return false;
        }
        if (startNum > maxNum || endNum > maxNum) {
            alert(`ページ番号は最大値(${maxNum})以下である必要があります。\nPage numbers must not exceed ${maxNum}.`);
            return false;
        }
        if (startNum > endNum) {
            alert("開始ページ番号は終了ページ番号以下である必要があります。\nStart page number must not exceed end page number.");
            return false;
        }

        return true; // 全ての条件が満たされている場合、trueを返す
    }


    // ダウンロード処理の開始
    async function download(getImagePromises, title, imageSuffix, zipOptions) {
        const startNum = Number(startNumInputElement.value);
        const endNum = Number(endNumInputElement.value);
        promiseCount = endNum - startNum + 1;
        // 画像のダウンロードを開始、同時リクエスト数の上限は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
            }
        }

        // ZIPアーカイブのファイル構造を設定
        JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
        zip = new JSZip();
        const { folderName, zipFileName } = sanitizeInputs(folderNameInput, zipFileNameInput);
        if (createFolder) {
            const folder = zip.folder(folderName);
            for (const [index, image] of images.entries()) {
                const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
                folder.file(filename, image, zipOptions);
            }
        } else {
            for (const [index, image] of images.entries()) {
                const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
                zip.file(filename, image, zipOptions);
            }
        }

        // ZIP化を開始し、進捗状況を表示
        const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `ZIP書庫作成中(${metadata.percent.toFixed()}%)`; };
        const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);
        // 「名前を付けて保存」ウィンドウを開く
        saveAs(content, zipFileName);
        // 全て完了
        downloadButtonElement.textContent = "完了しました"; // Completed → 完了しました
        downloadButtonElement.disabled = false;
        downloadButtonElement.style.backgroundColor = '#0984e3';
        downloadButtonElement.style.cursor = 'pointer';
    }

    // ファイル名整形用の関数
    function sanitizeFileName(str) {
        return str.trim()
            // 全角英数字を半角に変換
            .replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0))
            // 連続する空白(全角含む)を半角スペース1つに統一
            .replace(/[\s\u3000]+/g, ' ')
            // 「!?」または「?!」を「⁉」に置換
            .replace(/[!?][!?]/g, '⁉')
            // 特定の全角記号を対応する半角記号に変換
            .replace(/[!#$%&’,.()+-=@^_{}]/g, s => {
                const from = '!#$%&’,.()+-=@^_{}';
                const to = "!#$%&',.()+-=@^_{}";
                return to[from.indexOf(s)];
            })
            // ファイル名に使えない文字をハイフンに置換
            .replace(/[\\/:*?"<>|]/g, '-');
    }

    // folderNameとzipFileNameの整形処理関数
    function sanitizeInputs(folderNameInput, zipFileNameInput) {
        const folderName = sanitizeFileName(folderNameInput.value);
        const zipFileName = sanitizeFileName(zipFileNameInput.value);
        return { folderName, zipFileName };
    }

    // プロミスが成功した場合の処理
    function fulfillHandler(res) {
        if (!isErrorOccurred) {
            fulfillCount++;
            downloadButtonElement.innerHTML = `処理中(${fulfillCount}/${promiseCount})`;
        }
        return res;
    }

    // プロミスが失敗した場合の処理
    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);