LibImgDown

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

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

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/528949/1559462/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";
    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);
    
        // 共通CSSクラスの定義
        const style = document.createElement('style');
        style.textContent = `
            .input-element {
                box-sizing: content-box;
                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;
                width: 128px;
                height: 48px;
                padding: 5px;
                display: block;
                font-size: 14px;
                color: #fff;
                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: 10px; /* パディングを追加 */
                min-width: 200px;
                max-width: 300px;
                display: none; /* 初期状態は非表示 */
                flex-direction: column;
                justify-content: center;
                align-items: baseline;
                font-size: 12px;
                font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
                background-color: #f1f1f1;
                border-radius: 4px;
            }
            .range-container, .radio-container {
                display: flex;
                justify-content: center;
            }
            .textarea-element {
                resize: vertical;
                width: calc(100% - 10px);
                min-height: 45px;
                max-height: 200px;
                border-radius: 4px; /* 統一された角丸 */
            }
        `;
        document.head.appendChild(style);
    
        // 開始番号入力欄
        startNumInputElement = document.createElement('input');
        startNumInputElement.className = 'input-element';
        startNumInputElement.type = 'text';
        startNumInputElement.value = 1;
    
        // 終了番号入力欄
        endNumInputElement = document.createElement('input');
        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.textContent = 'から'; // "to" → "から"
    
        // ダウンロードボタン
        downloadButtonElement = document.createElement('button');
        downloadButtonElement.className = 'button-element';
        downloadButtonElement.textContent = 'ダウンロード';
    
        // トグルボタン
        const toggleButton = document.createElement('button');
        toggleButton.className = 'toggle-button';
        toggleButton.textContent = 'UI OPEN';
    
        // パネル要素
        panelElement = document.createElement('div');
        panelElement.className = 'panel-element';
    
        // トグルボタンのイベントリスナー
        let isUIVisible = false; // 初期状態は非表示
        toggleButton.addEventListener('click', () => {
            isUIVisible = !isUIVisible; 
            panelElement.style.display = isUIVisible ? 'flex' : 'none';
            toggleButton.textContent = isUIVisible ? 'UI CLOSE' : 'UI OPEN';
        });
    
        // 範囲入力コンテナ
        const rangeInputContainerElement = document.createElement('div');
        rangeInputContainerElement.className = 'range-container';

        // ラジオボタンコンテナ
        const rangeInputRadioElement = document.createElement('div');
        rangeInputRadioElement.className = 'radio-container';
    
        folderRadioYes = document.createElement('input');
        folderRadioYes.type = 'radio';
        folderRadioYes.name = 'createFolder';
        folderRadioYes.value = 'yes';
    
        folderRadioNo = document.createElement('input');
        folderRadioNo.type = 'radio';
        folderRadioNo.name = 'createFolder';
        folderRadioNo.value = 'no';
        folderRadioNo.checked = true;

        // フォルダ名入力欄
        folderNameInput = document.createElement('textarea');
        folderNameInput.className = 'textarea-element';
        folderNameInput.value = title; // titleを初期値として使用
        folderNameInput.disabled = true;
    
        // ZIPファイル名入力欄
        zipFileNameInput = document.createElement('textarea');
        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);    
        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);
        document.body.appendChild(toggleButton);
    }

    // ページ番号が正しいか確認する関数
    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 = `Processing(${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);