LibImgDown

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

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

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/528949/1552286/LibImgDown.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

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