LibImgDown

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

As of 2025-03-25. See the latest version.

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.greatest.deepsurf.us/scripts/528949/1559807/LibImgDown.js

  1. /*
  2. * Dependencies:
  3.  
  4. * GM_info(optional)
  5. * Docs: https://violentmonkey.github.io/api/gm/#gm_info
  6.  
  7. * GM_xmlhttpRequest(optional)
  8. * Docs: https://violentmonkey.github.io/api/gm/#gm_xmlhttprequest
  9.  
  10. * JSZIP
  11. * Github: https://github.com/Stuk/jszip
  12. * CDN: https://unpkg.com/jszip@3.7.1/dist/jszip.min.js
  13.  
  14. * FileSaver
  15. * Github: https://github.com/eligrey/FileSaver.js
  16. * CDN: https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js
  17. */
  18. ;
  19. const ImageDownloader = (({ JSZip, saveAs }) => {
  20. let maxNum = 0;
  21. let promiseCount = 0;
  22. let fulfillCount = 0;
  23. let isErrorOccurred = false;
  24. let createFolder = false;
  25. let folderName = "images";
  26. let zipFileName = "download.zip";
  27. let zip = null; // ZIPオブジェクトの初期化
  28. let imageDataArray = []; //imageDataArrayの初期化
  29. // elements
  30. let startNumInputElement = null;
  31. let endNumInputElement = null;
  32. let downloadButtonElement = null;
  33. let panelElement = null;
  34. let folderRadioYes = null;
  35. let folderRadioNo = null;
  36. let folderNameInput = null;
  37. let zipFileNameInput = null;
  38.  
  39. // 初期化関数
  40. function init({
  41. maxImageAmount,
  42. getImagePromises,
  43. title = `package_${Date.now()}`,
  44. WidthText = 0,
  45. HeightText = 0,
  46. imageSuffix = 'jpg',
  47. zipOptions = {},
  48. positionOptions = {}
  49. }) {
  50. // 値を割り当てる
  51. maxNum = maxImageAmount;
  52. // UIをセットアップする
  53. setupUI(positionOptions, title, WidthText, HeightText);
  54. // ダウンロードボタンにクリックイベントリスナーを追加
  55. downloadButtonElement.onclick = function () {
  56. if (!isOKToDownload()) return;
  57.  
  58. this.disabled = true;
  59. this.textContent = "処理中"; // Processing → 処理中
  60. this.style.backgroundColor = '#aaa';
  61. this.style.cursor = 'not-allowed';
  62.  
  63. download(getImagePromises, title, imageSuffix, zipOptions);
  64. };
  65. }
  66.  
  67. // スタイルを定義
  68. const style = document.createElement('style');
  69. style.textContent = `
  70. .input-element {
  71. box-sizing: content-box;
  72. padding: 1px 1px;
  73. width: 34px;
  74. height: 26px;
  75. border: 1px solid #aaa;
  76. border-radius: 4px;
  77. font-family: 'Consolas', 'Monaco';
  78. font-size: 14px;
  79. text-align: center;
  80. }
  81. .button-element {
  82. margin-top: 8px;
  83. margin-left: auto;
  84. width: 128px;
  85. height: 48px;
  86. padding: 5px 5px;
  87. display: block;
  88. justify-content: center;
  89. align-items: center;
  90. font-size: 14px;
  91. font-family: 'BIZ UDPゴシック', 'Arial';
  92. color: #fff;
  93. line-height: 1.2;
  94. background-color: #0984e3;
  95. border: 3px solidrgb(0, 0, 0);
  96. border-radius: 4px;
  97. cursor: pointer;
  98. }
  99. .toggle-button {
  100. position: fixed;
  101. top: 45px;
  102. left: 5px;
  103. z-index: 999999999;
  104. padding: 2px 5px;
  105. font-size: 14px;
  106. font-weight: bold;
  107. font-family: 'Monaco', 'Microsoft YaHei';
  108. color: #fff;
  109. background-color: #000000;
  110. border: 1px solid #aaa;
  111. border-radius: 4px;
  112. cursor: pointer;
  113. }
  114. .panel-element {
  115. position: fixed;
  116. top: 80px;
  117. left: 5px;
  118. z-index: 999999999;
  119. box-sizing: border-box;
  120. padding: 0px;
  121. width: auto;
  122. min-width: 400px;
  123. max-width: 600px;
  124. height: auto;
  125. display: none;
  126. flex-direction: column;
  127. justify-content: center;
  128. align-items: baseline;
  129. font-size: 14px;
  130. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  131. letter-spacing: normal;
  132. background-color: #f1f1f1;
  133. border: 1px solid #aaa;
  134. border-radius: 4px;
  135. }
  136. .range-container, .radio-container {
  137. display: flex;
  138. justify-content: center;
  139. align-items: baseline;
  140. }
  141. .textarea-element {
  142. box-sizing: content-box;
  143. padding: 1px 0px;
  144. width: 99%;
  145. min-height: 45px;
  146. max-height: 200px;
  147. border: 1px solid #aaa;
  148. border-radius: 1px;
  149. font-size: 11px;
  150. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  151. text-align: left;
  152. resize: vertical;
  153. height: auto;
  154. }
  155. .to-span {
  156. margin: 0 6px;
  157. color: black;
  158. line-height: 1;
  159. word-break: keep-all;
  160. user-select: none;
  161. }
  162. `;
  163. document.head.appendChild(style);
  164.  
  165. // UIセットアップ関数
  166. function setupUI(positionOptions, title, WidthText, HeightText) {
  167. title = sanitizeFileName(title);
  168.  
  169. // トグルボタンの作成
  170. const toggleButton = document.createElement('button');
  171. toggleButton.id = 'ImageDownloader-ToggleButton';
  172. toggleButton.className = 'toggle-button';
  173. toggleButton.textContent = 'UI OPEN';
  174. document.body.appendChild(toggleButton);
  175. let isUIVisible = false; // 初期状態を非表示に設定
  176. function toggleUI() {
  177. if (isUIVisible) {
  178. panelElement.style.display = 'none';
  179. toggleButton.textContent = 'UI OPEN';
  180. } else {
  181. panelElement.style.display = 'flex';
  182. toggleButton.textContent = 'UI CLOSE';
  183. }
  184. isUIVisible = !isUIVisible;
  185. }
  186. toggleButton.addEventListener('click', toggleUI);
  187.  
  188. // パネル要素の作成
  189. panelElement = document.createElement('div');
  190. panelElement.id = 'ImageDownloader-Panel';
  191. panelElement.className = 'panel-element';
  192. // 「positionOptions」に従ってパネルの位置を変更する。
  193. for (const [key, value] of Object.entries(positionOptions)) {
  194. if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
  195. panelElement.style[key] = value;
  196. }
  197. }
  198.  
  199. // 開始番号入力欄の作成
  200. startNumInputElement = document.createElement('input');
  201. startNumInputElement.id = 'ImageDownloader-StartNumInput';
  202. startNumInputElement.className = 'input-element';
  203. startNumInputElement.type = 'text';
  204. startNumInputElement.value = 1;
  205.  
  206. // 終了番号入力欄の作成
  207. endNumInputElement = document.createElement('input');
  208. endNumInputElement.id = 'ImageDownloader-EndNumInput';
  209. endNumInputElement.className = 'input-element';
  210. endNumInputElement.type = 'text';
  211. endNumInputElement.value = maxNum;
  212.  
  213. // キーボード入力がブロックされないようにする
  214. startNumInputElement.onkeydown = (e) => e.stopPropagation();
  215. endNumInputElement.onkeydown = (e) => e.stopPropagation();
  216.  
  217. // 「to」スパン要素の作成
  218. const toSpanElement = document.createElement('span');
  219. toSpanElement.id = 'ImageDownloader-ToSpan';
  220. toSpanElement.className = 'to-span';
  221. toSpanElement.textContent = 'から'; // to → から
  222.  
  223. // ダウンロードボタン要素の作成
  224. downloadButtonElement = document.createElement('button');
  225. downloadButtonElement.id = 'ImageDownloader-DownloadButton';
  226. downloadButtonElement.className = 'button-element';
  227. downloadButtonElement.textContent = 'ダウンロード'; // Download → ダウンロード
  228.  
  229. // 範囲入力コンテナ要素の作成
  230. const rangeInputContainerElement = document.createElement('div');
  231. rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
  232. rangeInputContainerElement.className = 'range-container';
  233.  
  234. // ラジオボタンコンテナ要素の作成
  235. const rangeInputRadioElement = document.createElement('div');
  236. rangeInputRadioElement.id = 'ImageDownloader-RadioChecker';
  237. rangeInputRadioElement.className = 'radio-container';
  238.  
  239. // ラジオボタンを作成(フォルダ選択用'YES')
  240. folderRadioYes = document.createElement('input');
  241. folderRadioYes.type = 'radio';
  242. folderRadioYes.name = 'createFolder';
  243. folderRadioYes.value = 'yes';
  244. folderRadioYes.id = 'createFolderYes';
  245.  
  246. // ラジオボタンを作成(フォルダ選択用'No')
  247. folderRadioNo = document.createElement('input');
  248. folderRadioNo.type = 'radio';
  249. folderRadioNo.name = 'createFolder';
  250. folderRadioNo.value = 'no';
  251. folderRadioNo.id = 'createFolderNo';
  252. folderRadioNo.checked = true;
  253.  
  254. // フォルダ名入力欄の作成
  255. folderNameInput = document.createElement('textarea');
  256. folderNameInput.id = 'folderNameInput';
  257. folderNameInput.className = 'textarea-element';
  258. folderNameInput.value = title; // 初期値としてタイトルを使用
  259. folderNameInput.disabled = true;
  260.  
  261. // ZIPファイル名入力欄の作成
  262. zipFileNameInput = document.createElement('textarea');
  263. zipFileNameInput.id = 'zipFileNameInput';
  264. zipFileNameInput.className = 'textarea-element';
  265. zipFileNameInput.value = `${title}.zip`; // titleを使用してZIPファイル名を設定
  266. // ラジオボタンのイベントリスナーを追加
  267. folderRadioYes.addEventListener('change', () => {
  268. createFolder = true;
  269. folderNameInput.disabled = false; // フォルダ名入力欄を有効化
  270. });
  271. folderRadioNo.addEventListener('change', () => {
  272. createFolder = false;
  273. folderNameInput.disabled = true; // フォルダ名入力欄を無効化
  274. });
  275.  
  276. // 組み立ててドキュメントに挿入
  277. rangeInputContainerElement.appendChild(startNumInputElement);
  278. rangeInputContainerElement.appendChild(toSpanElement);
  279. rangeInputContainerElement.appendChild(endNumInputElement);
  280. panelElement.appendChild(rangeInputContainerElement);
  281. rangeInputRadioElement.appendChild(document.createTextNode('フォルダ作成:'));
  282. rangeInputRadioElement.appendChild(folderRadioYes);
  283. rangeInputRadioElement.appendChild(document.createTextNode('する '));
  284. rangeInputRadioElement.appendChild(folderRadioNo);
  285. rangeInputRadioElement.appendChild(document.createTextNode('しない'));
  286. panelElement.appendChild(rangeInputRadioElement);
  287. panelElement.appendChild(document.createTextNode('フォルダ名: '));
  288. panelElement.appendChild(folderNameInput);
  289. panelElement.appendChild(document.createTextNode('ZIPファイル名: '));
  290. panelElement.appendChild(zipFileNameInput);
  291. // サイズ情報の追加(条件付き)
  292. if (WidthText > 0 && HeightText > 0) {
  293. panelElement.appendChild(document.createTextNode(` サイズ: ${WidthText} x ${HeightText}`));
  294. }
  295. panelElement.appendChild(downloadButtonElement);
  296. document.body.appendChild(panelElement);
  297. }
  298.  
  299. // ページ番号が正しいか確認する関数
  300. function isOKToDownload() {
  301. const startNum = Number(startNumInputElement.value);
  302. const endNum = Number(endNumInputElement.value);
  303.  
  304. if (Number.isNaN(startNum) || Number.isNaN(endNum)) {
  305. alert("正しい値を入力してください。\nPlease enter page numbers correctly.");
  306. return false;
  307. }
  308. if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) {
  309. alert("ページ番号は整数である必要があります。\nPage numbers must be integers.");
  310. return false;
  311. }
  312. if (startNum < 1 || endNum < 1) {
  313. alert("ページ番号は1以上である必要があります。\nPage numbers must be greater than or equal to 1.");
  314. return false;
  315. }
  316. if (startNum > maxNum || endNum > maxNum) {
  317. alert(`ページ番号は最大値(${maxNum})以下である必要があります。\nPage numbers must not exceed ${maxNum}.`);
  318. return false;
  319. }
  320. if (startNum > endNum) {
  321. alert("開始ページ番号は終了ページ番号以下である必要があります。\nStart page number must not exceed end page number.");
  322. return false;
  323. }
  324.  
  325. return true; // 全ての条件が満たされている場合、trueを返す
  326. }
  327.  
  328.  
  329. // ダウンロード処理の開始
  330. async function download(getImagePromises, title, imageSuffix, zipOptions) {
  331. const startNum = Number(startNumInputElement.value);
  332. const endNum = Number(endNumInputElement.value);
  333. promiseCount = endNum - startNum + 1;
  334. // 画像のダウンロードを開始、同時リクエスト数の上限は4
  335. let images = [];
  336. for (let num = startNum; num <= endNum; num += 4) {
  337. const from = num;
  338. const to = Math.min(num + 3, endNum);
  339. try {
  340. const result = await Promise.all(getImagePromises(from, to));
  341. images = images.concat(result);
  342. } catch (error) {
  343. return; // cancel downloading
  344. }
  345. }
  346.  
  347. // ZIPアーカイブのファイル構造を設定
  348. JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
  349. zip = new JSZip();
  350. const { folderName, zipFileName } = sanitizeInputs(folderNameInput, zipFileNameInput);
  351. if (createFolder) {
  352. const folder = zip.folder(folderName);
  353. for (const [index, image] of images.entries()) {
  354. const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
  355. folder.file(filename, image, zipOptions);
  356. }
  357. } else {
  358. for (const [index, image] of images.entries()) {
  359. const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
  360. zip.file(filename, image, zipOptions);
  361. }
  362. }
  363.  
  364. // ZIP化を開始し、進捗状況を表示
  365. const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `ZIP書庫作成中(${metadata.percent.toFixed()}%)`; };
  366. const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);
  367. // 「名前を付けて保存」ウィンドウを開く
  368. saveAs(content, zipFileName);
  369. // 全て完了
  370. downloadButtonElement.textContent = "完了しました"; // Completed → 完了しました
  371. downloadButtonElement.disabled = false;
  372. downloadButtonElement.style.backgroundColor = '#0984e3';
  373. downloadButtonElement.style.cursor = 'pointer';
  374. }
  375.  
  376. // ファイル名整形用の関数
  377. function sanitizeFileName(str) {
  378. return str.trim()
  379. // 全角英数字を半角に変換
  380. .replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0))
  381. // 連続する空白(全角含む)を半角スペース1つに統一
  382. .replace(/[\s\u3000]+/g, ' ')
  383. // 「!?」または「?!」を「⁉」に置換
  384. .replace(/[!?][!?]/g, '⁉')
  385. // 特定の全角記号を対応する半角記号に変換
  386. .replace(/[!#$%&’,.()+-=@^_{}]/g, s => {
  387. const from = '!#$%&’,.()+-=@^_{}';
  388. const to = "!#$%&',.()+-=@^_{}";
  389. return to[from.indexOf(s)];
  390. })
  391. // ファイル名に使えない文字をハイフンに置換
  392. .replace(/[\\/:*?"<>|]/g, '-');
  393. }
  394.  
  395. // folderNameとzipFileNameの整形処理関数
  396. function sanitizeInputs(folderNameInput, zipFileNameInput) {
  397. const folderName = sanitizeFileName(folderNameInput.value);
  398. const zipFileName = sanitizeFileName(zipFileNameInput.value);
  399. return { folderName, zipFileName };
  400. }
  401.  
  402. // プロミスが成功した場合の処理
  403. function fulfillHandler(res) {
  404. if (!isErrorOccurred) {
  405. fulfillCount++;
  406. downloadButtonElement.innerHTML = `処理中(${fulfillCount}/${promiseCount})`;
  407. }
  408. return res;
  409. }
  410.  
  411. // プロミスが失敗した場合の処理
  412. function rejectHandler(err) {
  413. isErrorOccurred = true;
  414. console.error(err);
  415. downloadButtonElement.textContent = 'エラーが発生しました'; // Error Occurred → エラーが発生しました
  416. downloadButtonElement.style.backgroundColor = 'red';
  417. return Promise.reject(err);
  418. }
  419.  
  420. return { init, fulfillHandler, rejectHandler };
  421. })(window);