LibImgDown

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

Version vom 25.03.2025. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/528949/1559807/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);
        };
    }

// スタイルを定義
const style = document.createElement('style');
style.textContent = `
    .input-element {
        box-sizing: content-box;
        padding: 1px 1px;
        width: 34px;
        height: 26px;
        border: 1px solid #aaa;
        border-radius: 4px;
        font-family: 'Consolas', 'Monaco';
        font-size: 14px;
        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: 'BIZ UDPゴシック', 'Arial';
        color: #fff;
        line-height: 1.2;
        background-color: #0984e3;
        border: 3px solidrgb(0, 0, 0);
        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: 14px;
        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: 1px 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);

    // トグルボタンの作成
    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);

    // パネル要素の作成
    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;
        }
    }

    // 開始番号入力欄の作成
    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 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';

    // ラジオボタンを作成(フォルダ選択用'YES')
    folderRadioYes = document.createElement('input');
    folderRadioYes.type = 'radio';
    folderRadioYes.name = 'createFolder';
    folderRadioYes.value = 'yes';
    folderRadioYes.id = 'createFolderYes';

     // ラジオボタンを作成(フォルダ選択用'No')
    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);
    // サイズ情報の追加(条件付き)
    if (WidthText > 0 && HeightText > 0) {
        panelElement.appendChild(document.createTextNode(` サイズ: ${WidthText} x ${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);