LibImgDown

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

Ajankohdalta 25.3.2025. Katso uusin versio.

Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @require https://update.greasyfork.org/scripts/528949/1559529/LibImgDown.js.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

/*
 * 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);
        };
    }

    // UIセットアップ関数
    function setupUI(positionOptions, title, WidthText, HeightText) {
        title = sanitizeFileName(title);
        // 共通の入力要素スタイル
        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;
        `;
        // 開始番号入力欄の作成
        startNumInputElement = document.createElement('input');
        startNumInputElement.id = 'ImageDownloader-StartNumInput';
        startNumInputElement.style = inputElementStyle;
        startNumInputElement.type = 'text';
        startNumInputElement.value = 1;
        // 終了番号入力欄の作成
        endNumInputElement = document.createElement('input');
        endNumInputElement.id = 'ImageDownloader-EndNumInput';
        endNumInputElement.style = inputElementStyle;
        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.textContent = 'から'; // to → から
        toSpanElement.style = `
            margin: 0 6px;
            color: black;
            line-height: 1;
            word-break: keep-all;
            user-select: none;
        `;
        // ダウンロードボタン要素の作成
        downloadButtonElement = document.createElement('button');
        downloadButtonElement.id = 'ImageDownloader-DownloadButton';
        downloadButtonElement.textContent = 'ダウンロード'; // Download → ダウンロード
        downloadButtonElement.style = `
            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;
        `;
        const toggleButton = document.createElement('button');
        toggleButton.id = 'ImageDownloader-ToggleButton';
        toggleButton.textContent = 'UI OPEN';
        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: #000000;
            border: 1px solid #aaa;
            border-radius: 4px;
            cursor: pointer;
        `;
        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)
        // 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: 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;
        `;
        // 「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.value = 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;
        `;
        // ラジオボタンのイベントリスナーを追加
        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.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);