LibImgDown

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

目前为 2025-03-25 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/528949/1559525/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);
        // 共通の入力要素スタイル
        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;
        `;
        // 開始番号入力欄の作成
        startNumInputElement = document.createElement('input');
        startNumInputElement.id = 'ImageDownloader-StartNumInput';
        startNumInputElement.style = inputElementStyle;
        startNumInputElement.type = 'text';
        startNumInputElement.value = 1;
        // 終了番号入力欄の作成
        endNumInputElement = document.createElement('input');
        endNumInputElement.id = 'ImageDownloader-EndNumInput';
        endNumInputElement.style = inputElementStyle;
        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.textContent = 'から'; // to → から
        toSpanElement.style = `
            margin: 0 6px;
            color: black;
            line-height: 1;
            word-break: keep-all;
            user-select: none;
        `;
        // ダウンロードボタン要素の作成
        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);
        // 範囲入力コンテナ要素の作成
        const rangeInputContainerElement = document.createElement('div');
        rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
        rangeInputContainerElement.style = `
            display: flex;
            justify-content: center;
            align-items: baseline;
        `;
        // ラジオボタンコンテナ要素の作成
        const rangeInputRadioElement = document.createElement('div');
        rangeInputRadioElement.id = 'ImageDownloader-RadioChecker';
        rangeInputRadioElement.style = `
            display: flex;
            justify-content: center;
            align-items: baseline;
        `;
        // パネル要素の作成
        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;
        `;
        // 「positionOptions」に従ってパネルの位置を変更する。
        for (const [key, value] of Object.entries(positionOptions)) {
            if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
                panelElement.style[key] = value;
            }
        }

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

        const folderRadioNo = document.createElement('input');
        folderRadioNo.type = 'radio';
        folderRadioNo.name = 'createFolder';
        folderRadioNo.value = 'no';
        folderRadioNo.id = 'createFolderNo';
        folderRadioNo.checked = true;

        // フォルダ名入力欄の作成
        const folderNameInput = document.createElement('textarea');
        folderNameInput.id = 'folderNameInput';
        folderNameInput.value = 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ファイル名入力欄の作成
        const 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;
        `;
        // ラジオボタンのイベントリスナーを追加
        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);
        panelElement.appendChild(document.createTextNode(` サイズ: ${WidthText} x `));
        panelElement.appendChild(document.createTextNode(`${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 = `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);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址