/*
* 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 worker = null;
// elements
let startNumInputElement = null;
let endNumInputElement = null;
let downloadButtonElement = null;
let panelElement = 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()}`,
imageSuffix = 'jpg',
zipOptions = {},
positionOptions = {}
}) {
// assign value
maxNum = maxImageAmount;
// setup UI
setupUI(positionOptions);
// 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';
// Create Web Worker for background processing
const workerCode = `
self.onmessage = async function(e) {
const { startNum, endNum, getImagePromises, title, imageSuffix, zipOptions } = e.data;
const JSZip = (${JSZip.toString()})();
const images = [];
// Download images in chunks
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.push(...result);
postMessage({ type: 'progress', current: num, total: endNum });
} catch (error) {
postMessage({ type: 'error', error });
return;
}
}
// Create zip file
JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
const zip = new JSZip();
let newtitle = title.replace(/(\?|\~|\/|\:)/gi, function ($0, $1) {
return {
'?':'?',
'~':'~',
'/':'/',
':':':',
}[$1];
});
const zipTitle = newtitle.replaceAll(/\\|\*|\"|\<|\>|\|/g, '_');
const folder = zip.folder(zipTitle);
images.forEach((image, index) => {
const filename = \`\${String(index + 1).padStart(4, '0')}.\${imageSuffix}\`;
folder.file(filename, image, zipOptions);
});
// Generate zip and send back to main thread
const content = await zip.generateAsync({ type: "blob" }, (metadata) => {
postMessage({ type: 'zipping', percent: metadata.percent });
});
postMessage({ type: 'complete', content, zipTitle });
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function(e) {
const data = e.data;
switch(data.type) {
case 'progress':
const progress = Math.round((data.current / data.total) * 100);
downloadButtonElement.innerHTML = 'Downloading<br>(' + progress + '%)';
break;
case 'zipping':
downloadButtonElement.innerHTML = 'Zipping<br>(' + data.percent.toFixed() + '%)';
break;
case 'error':
console.error(data.error);
downloadButtonElement.textContent = 'Error Occurred';
downloadButtonElement.style.backgroundColor = 'red';
break;
case 'complete':
saveAs(data.content, data.zipTitle + '.zip');
downloadButtonElement.textContent = "Completed";
setTimeout(function() { window.close(); }, 3000);
worker.terminate();
break;
}
};
worker.postMessage({
startNum: Number(startNumInputElement.value),
endNum: Number(endNumInputElement.value),
getImagePromises: getImagePromises,
title: title,
imageSuffix: imageSuffix,
zipOptions: zipOptions
});
}
}
// setup UI
function setupUI(positionOptions) {
// common input element style
const inputElementStyle = `
box-sizing: content-box;
padding: 1px 2px;
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 = function(e) { e.stopPropagation(); };
endNumInputElement.onkeydown = function(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;
width: 128px;
height: 48px;
display: flex;
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;
`;
// 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 panel element
panelElement = document.createElement('div');
panelElement.id = 'ImageDownloader-Panel';
panelElement.style = `
position: fixed;
top: 72px;
left: 72px;
z-index: 999999999;
box-sizing: border-box;
padding: 8px;
width: 146px;
height: 106px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: baseline;
font-size: 14px;
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;
}
}
// assemble and then insert into document
rangeInputContainerElement.appendChild(startNumInputElement);
rangeInputContainerElement.appendChild(toSpanElement);
rangeInputContainerElement.appendChild(endNumInputElement);
panelElement.appendChild(rangeInputContainerElement);
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\/(\d+)-/)?.groups?.id;
const scriptURL = 'https://update.gf.qytechs.cn/scripts/' + scriptID + '/raw.js';
const latestVersionString = await new Promise(function(resolve) {
GM_xmlhttpRequest({
method: 'GET',
url: scriptURL,
responseType: 'text',
onload: function(res) {
const match = res.response.match(/@version\s+([0-9\.]+)/);
resolve(match ? match[1] : null);
}
});
});
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 = function() {
setTimeout(function() {
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\nPage number should not smaller than 1.");
return false;
}
if (startNum > maxNum || endNum > maxNum) {
alert("页码的值不能大于" + 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;
}
// handle promise fulfilled
function fulfillHandler(res) {
if (!isErrorOccurred) {
fulfillCount++;
downloadButtonElement.innerHTML = 'Processing<br>(' + 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);