Greasy Fork is available in English.

LibImgDown

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

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/528949/1548933/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;

    // svg icons
    const externalLinkSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16"><path fill-rule="evenodd" d="M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z"></path></svg>`;
    const reloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>`;

    // 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) {
        // 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 CLOSE';
        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: #0984e3;
            border: 1px solid #aaa;
            border-radius: 4px;
            cursor: pointer;
        `;
        document.body.appendChild(toggleButton);
        let isUIVisible = true;
        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: flex;
            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);
    }

    // setup update notification
    async function setupUpdateNotification() {
        if (typeof GM_info === 'undefined' || typeof GM_xmlhttpRequest === 'undefined') return;
        // get local version
        const localVersion = Number(GM_info.script.version);
        // get latest version
        //const scriptID = (GM_info.script.homepageURL || GM_info.script.homepage).match(/scripts\/(?<id>\d+)-/)?.groups?.id;
        let scriptID = null;
        const homepageURL = GM_info.script.homepageURL || GM_info.script.homepage;
        if (homepageURL) {
            const match = homepageURL.match(/scripts\/(?<id>\d+)-/);
            if (match && match.groups && match.groups.id) {
                scriptID = match.groups.id;
            }
        }
        const scriptURL = `https://update.greasyfork.org/scripts/${scriptID}/raw.js`;
        const latestVersionString = await new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: scriptURL,
                responseType: 'text',
                onload: res => resolve(res.response.match(/@version\s+(?<version>[0-9\.]+)/)?.groups?.version)
            });
        });
        const latestVersion = Number(latestVersionString);
        if (Number.isNaN(localVersion) || Number.isNaN(latestVersion)) return;
        if (latestVersion <= localVersion) return;
        // show update notification
        const updateLinkElement = document.createElement('a');
        updateLinkElement.id = 'ImageDownloader-UpdateLink';
        updateLinkElement.href = scriptURL.replace('raw.js', 'raw.user.js');
        updateLinkElement.innerHTML = `Update to V${latestVersionString}${externalLinkSVG}`;
        updateLinkElement.style = `
            position: absolute;
            bottom: -38px;
            left: -1px;
            display: flex;
            justify-content: space-around;
            align-items: center;
            box-sizing: border-box;
            padding: 8px;
            width: 146px;
            height: 32px;
            font-size: 14px;
            font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
            text-decoration: none;
            color: white;
            background-color: #32CD32;
            border-radius: 4px;
        `;
        updateLinkElement.onclick = () => setTimeout(() => {
            updateLinkElement.removeAttribute('href');
            updateLinkElement.innerHTML = `Please Reload${reloadSVG}`;
            updateLinkElement.style.cursor = 'default';
        }, 1000);
        panelElement.appendChild(updateLinkElement);
    }

    // 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();
        folderName = folderNameInput.value;
        zipFileName = zipFileNameInput.value;
        folderName = folderName.trim()
        folderName = folderName.replace(/[A-Za-z0-9]/g, function(s) {
            return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
        });
        folderName = folderName.replace(/ /g, ' ');
        folderName = folderName.replace(/[!?][!?]/g, '⁉');
        folderName = folderName.replace(/[!#$%&()+*]/g, function(s) {
            return '!#$%&()+*'['!#$%&()+*'.indexOf(s)];
        });
        folderName = folderName.replace(/[\\/:*?"<>|]/g, '-');
        zipFileName = zipFileName.trim()
        zipFileName = zipFileName.replace(/[A-Za-z0-9]/g, function(s) {
            return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
        });
        zipFileName = zipFileName.replace(/ /g, ' ');
        zipFileName = zipFileName.replace(/[!?][!?]/g, '⁉');
        zipFileName = zipFileName.replace(/[!#$%&()+*]/g, function(s) {
            return '!#$%&()+*'['!#$%&()+*'.indexOf(s)];
        });
        zipFileName = zipFileName.replace(/[\\/:*?"<>|]/g, '-');
        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";
        // ボタンを再度押せるようにする
        downloadButtonElement.disabled = false;
        downloadButtonElement.style.backgroundColor = '#0984e3';
        downloadButtonElement.style.cursor = 'pointer';
    }

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

    function reset() {
        maxNum = 0;
        promiseCount = 0;
        fulfillCount = 0;
        isErrorOccurred = false;
        zip = null; // ZIPオブジェクトの初期化
        imageDataArray = []; //imageDataArrayの初期化
        // UI要素のリセット
        if (startNumInputElement) startNumInputElement.value = 1;
        if (endNumInputElement) endNumInputElement.value = 1;
        if (downloadButtonElement) {
            downloadButtonElement.disabled = false;
            downloadButtonElement.textContent = "Download";
            downloadButtonElement.style.backgroundColor = '#0984e3';
            downloadButtonElement.style.cursor = 'pointer';
        }
        // パネルの削除(再作成のため)
        if (panelElement && panelElement.parentNode) {
            panelElement.parentNode.removeChild(panelElement);
        }
    }

    return { init, fulfillHandler, rejectHandler, reset };
})(window);