LibImgDown

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

Verze ze dne 25. 03. 2025. Zobrazit nejnovější verzi.

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