WEBのダウンロードライブラリ
Version vom
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/1558024/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) { 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); } // 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(); 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); } 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);