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.greasyfork.org/scripts/528949/1559309/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;

    // initialization
    function init({
        maxImageAmount,
        getImagePromises,
        title = `package_${Date.now()}`,
        WidthText = 0,
        HeightText = 0,
        imageSuffix = 'jpg',
        zipOptions = {},
        positionOptions = {}
    }) {
        // assign value
        maxNum = maxImageAmount;
        // setup UI
        setupUI(positionOptions, title, WidthText, HeightText);
        // 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, WidthText, HeightText) {
        title = sanitizeFileName(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;
            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;
        `;
        // 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.createTextNode(` サイズ: ${WidthText} x `));
        panelElement.appendChild(document.createTextNode(`${HeightText}`));
        panelElement.appendChild(downloadButtonElement);
        document.body.appendChild(panelElement);
    }

    // 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より小さくすることはできません1\nPage number should not smaller than 1."); return false; }
        if (startNum > maxNum || endNum > maxNum) { alert(`ページ番号の値を1より大きくすることはできません${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);
        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);
            }
        }

        // 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";
        // ボタンを再度押せるようにする
    }

    // ファイル名整形用の関数
    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 };
    }

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