This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @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);