- /*
- * 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/jszip@3.7.1/dist/jszip.min.js
-
- * FileSaver
- * Github: https://github.com/eligrey/FileSaver.js
- * CDN: https://unpkg.com/file-saver@2.0.5/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 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: #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.gf.qytechs.cn/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);