LibImgDown

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

目前为 2025-03-25 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/528949/1559663/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: 0px 0px;
  73. width: 26px;
  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: 0px 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. startNumInputElement = document.createElement('input');
  170. startNumInputElement.id = 'ImageDownloader-StartNumInput';
  171. startNumInputElement.className = 'input-element';
  172. startNumInputElement.type = 'text';
  173. startNumInputElement.value = 1;
  174. // 終了番号入力欄の作成
  175. endNumInputElement = document.createElement('input');
  176. endNumInputElement.id = 'ImageDownloader-EndNumInput';
  177. endNumInputElement.className = 'input-element';
  178. endNumInputElement.type = 'text';
  179. endNumInputElement.value = maxNum;
  180. // キーボード入力がブロックされないようにする
  181. startNumInputElement.onkeydown = (e) => e.stopPropagation();
  182. endNumInputElement.onkeydown = (e) => e.stopPropagation();
  183. // 「to」スパン要素の作成
  184. const toSpanElement = document.createElement('span');
  185. toSpanElement.id = 'ImageDownloader-ToSpan';
  186. toSpanElement.className = 'to-span';
  187. toSpanElement.textContent = 'から'; // to → から
  188. // ダウンロードボタン要素の作成
  189. downloadButtonElement = document.createElement('button');
  190. downloadButtonElement.id = 'ImageDownloader-DownloadButton';
  191. downloadButtonElement.className = 'button-element';
  192. downloadButtonElement.textContent = 'ダウンロード'; // Download → ダウンロード
  193. // トグルボタンの作成
  194. const toggleButton = document.createElement('button');
  195. toggleButton.id = 'ImageDownloader-ToggleButton';
  196. toggleButton.className = 'toggle-button';
  197. toggleButton.textContent = 'UI OPEN';
  198. document.body.appendChild(toggleButton);
  199. let isUIVisible = false; // 初期状態を非表示に設定
  200. function toggleUI() {
  201. if (isUIVisible) {
  202. panelElement.style.display = 'none';
  203. toggleButton.textContent = 'UI OPEN';
  204. } else {
  205. panelElement.style.display = 'flex';
  206. toggleButton.textContent = 'UI CLOSE';
  207. }
  208. isUIVisible = !isUIVisible;
  209. }
  210. toggleButton.addEventListener('click', toggleUI);
  211. // 範囲入力コンテナ要素の作成
  212. const rangeInputContainerElement = document.createElement('div');
  213. rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
  214. rangeInputContainerElement.className = 'range-container';
  215. // ラジオボタンコンテナ要素の作成
  216. const rangeInputRadioElement = document.createElement('div');
  217. rangeInputRadioElement.id = 'ImageDownloader-RadioChecker';
  218. rangeInputRadioElement.className = 'radio-container';
  219. // パネル要素の作成
  220. panelElement = document.createElement('div');
  221. panelElement.id = 'ImageDownloader-Panel';
  222. panelElement.className = 'panel-element';
  223. // 「positionOptions」に従ってパネルの位置を変更する。
  224. for (const [key, value] of Object.entries(positionOptions)) {
  225. if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
  226. panelElement.style[key] = value;
  227. }
  228. }
  229.  
  230. // フォルダラジオボタンを作成
  231. folderRadioYes = document.createElement('input');
  232. folderRadioYes.type = 'radio';
  233. folderRadioYes.name = 'createFolder';
  234. folderRadioYes.value = 'yes';
  235. folderRadioYes.id = 'createFolderYes';
  236.  
  237. folderRadioNo = document.createElement('input');
  238. folderRadioNo.type = 'radio';
  239. folderRadioNo.name = 'createFolder';
  240. folderRadioNo.value = 'no';
  241. folderRadioNo.id = 'createFolderNo';
  242. folderRadioNo.checked = true;
  243.  
  244. // フォルダ名入力欄の作成
  245. folderNameInput = document.createElement('textarea');
  246. folderNameInput.id = 'folderNameInput';
  247. folderNameInput.className = 'textarea-element';
  248. folderNameInput.value = title; // 初期値としてタイトルを使用
  249. folderNameInput.disabled = true;
  250. // ZIPファイル名入力欄の作成
  251. zipFileNameInput = document.createElement('textarea');
  252. zipFileNameInput.id = 'zipFileNameInput';
  253. zipFileNameInput.className = 'textarea-element';
  254. zipFileNameInput.value = `${title}.zip`; // titleを使用してZIPファイル名を設定
  255. // ラジオボタンのイベントリスナーを追加
  256. folderRadioYes.addEventListener('change', () => {
  257. createFolder = true;
  258. folderNameInput.disabled = false; // フォルダ名入力欄を有効化
  259. });
  260. folderRadioNo.addEventListener('change', () => {
  261. createFolder = false;
  262. folderNameInput.disabled = true; // フォルダ名入力欄を無効化
  263. });
  264. // 組み立ててドキュメントに挿入
  265. rangeInputContainerElement.appendChild(startNumInputElement);
  266. rangeInputContainerElement.appendChild(toSpanElement);
  267. rangeInputContainerElement.appendChild(endNumInputElement);
  268. panelElement.appendChild(rangeInputContainerElement);
  269. rangeInputRadioElement.appendChild(document.createTextNode('フォルダ作成:'));
  270. rangeInputRadioElement.appendChild(folderRadioYes);
  271. rangeInputRadioElement.appendChild(document.createTextNode('する '));
  272. rangeInputRadioElement.appendChild(folderRadioNo);
  273. rangeInputRadioElement.appendChild(document.createTextNode('しない'));
  274. panelElement.appendChild(rangeInputRadioElement);
  275. panelElement.appendChild(document.createTextNode('フォルダ名: '));
  276. panelElement.appendChild(folderNameInput);
  277. panelElement.appendChild(document.createTextNode('ZIPファイル名: '));
  278. panelElement.appendChild(zipFileNameInput);
  279. panelElement.appendChild(document.createTextNode(` サイズ: ${WidthText} x `));
  280. panelElement.appendChild(document.createTextNode(`${HeightText}`));
  281. panelElement.appendChild(downloadButtonElement);
  282. document.body.appendChild(panelElement);
  283. }
  284.  
  285. // ページ番号が正しいか確認する関数
  286. function isOKToDownload() {
  287. const startNum = Number(startNumInputElement.value);
  288. const endNum = Number(endNumInputElement.value);
  289.  
  290. if (Number.isNaN(startNum) || Number.isNaN(endNum)) {
  291. alert("正しい値を入力してください。\nPlease enter page numbers correctly.");
  292. return false;
  293. }
  294. if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) {
  295. alert("ページ番号は整数である必要があります。\nPage numbers must be integers.");
  296. return false;
  297. }
  298. if (startNum < 1 || endNum < 1) {
  299. alert("ページ番号は1以上である必要があります。\nPage numbers must be greater than or equal to 1.");
  300. return false;
  301. }
  302. if (startNum > maxNum || endNum > maxNum) {
  303. alert(`ページ番号は最大値(${maxNum})以下である必要があります。\nPage numbers must not exceed ${maxNum}.`);
  304. return false;
  305. }
  306. if (startNum > endNum) {
  307. alert("開始ページ番号は終了ページ番号以下である必要があります。\nStart page number must not exceed end page number.");
  308. return false;
  309. }
  310.  
  311. return true; // 全ての条件が満たされている場合、trueを返す
  312. }
  313.  
  314.  
  315. // ダウンロード処理の開始
  316. async function download(getImagePromises, title, imageSuffix, zipOptions) {
  317. const startNum = Number(startNumInputElement.value);
  318. const endNum = Number(endNumInputElement.value);
  319. promiseCount = endNum - startNum + 1;
  320. // 画像のダウンロードを開始、同時リクエスト数の上限は4
  321. let images = [];
  322. for (let num = startNum; num <= endNum; num += 4) {
  323. const from = num;
  324. const to = Math.min(num + 3, endNum);
  325. try {
  326. const result = await Promise.all(getImagePromises(from, to));
  327. images = images.concat(result);
  328. } catch (error) {
  329. return; // cancel downloading
  330. }
  331. }
  332.  
  333. // ZIPアーカイブのファイル構造を設定
  334. JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
  335. zip = new JSZip();
  336. const { folderName, zipFileName } = sanitizeInputs(folderNameInput, zipFileNameInput);
  337. if (createFolder) {
  338. const folder = zip.folder(folderName);
  339. for (const [index, image] of images.entries()) {
  340. const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
  341. folder.file(filename, image, zipOptions);
  342. }
  343. } else {
  344. for (const [index, image] of images.entries()) {
  345. const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
  346. zip.file(filename, image, zipOptions);
  347. }
  348. }
  349.  
  350. // ZIP化を開始し、進捗状況を表示
  351. const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `ZIP書庫作成中(${metadata.percent.toFixed()}%)`; };
  352. const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);
  353. // 「名前を付けて保存」ウィンドウを開く
  354. saveAs(content, zipFileName);
  355. // 全て完了
  356. downloadButtonElement.textContent = "完了しました"; // Completed → 完了しました
  357. downloadButtonElement.disabled = false;
  358. downloadButtonElement.style.backgroundColor = '#0984e3';
  359. downloadButtonElement.style.cursor = 'pointer';
  360. }
  361.  
  362. // ファイル名整形用の関数
  363. function sanitizeFileName(str) {
  364. return str.trim()
  365. // 全角英数字を半角に変換
  366. .replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0))
  367. // 連続する空白(全角含む)を半角スペース1つに統一
  368. .replace(/[\s\u3000]+/g, ' ')
  369. // 「!?」または「?!」を「⁉」に置換
  370. .replace(/[!?][!?]/g, '⁉')
  371. // 特定の全角記号を対応する半角記号に変換
  372. .replace(/[!#$%&’,.()+-=@^_{}]/g, s => {
  373. const from = '!#$%&’,.()+-=@^_{}';
  374. const to = "!#$%&',.()+-=@^_{}";
  375. return to[from.indexOf(s)];
  376. })
  377. // ファイル名に使えない文字をハイフンに置換
  378. .replace(/[\\/:*?"<>|]/g, '-');
  379. }
  380.  
  381. // folderNameとzipFileNameの整形処理関数
  382. function sanitizeInputs(folderNameInput, zipFileNameInput) {
  383. const folderName = sanitizeFileName(folderNameInput.value);
  384. const zipFileName = sanitizeFileName(zipFileNameInput.value);
  385. return { folderName, zipFileName };
  386. }
  387.  
  388. // プロミスが成功した場合の処理
  389. function fulfillHandler(res) {
  390. if (!isErrorOccurred) {
  391. fulfillCount++;
  392. downloadButtonElement.innerHTML = `処理中(${fulfillCount}/${promiseCount})`;
  393. }
  394. return res;
  395. }
  396.  
  397. // プロミスが失敗した場合の処理
  398. function rejectHandler(err) {
  399. isErrorOccurred = true;
  400. console.error(err);
  401. downloadButtonElement.textContent = 'エラーが発生しました'; // Error Occurred → エラーが発生しました
  402. downloadButtonElement.style.backgroundColor = 'red';
  403. return Promise.reject(err);
  404. }
  405.  
  406. return { init, fulfillHandler, rejectHandler };
  407. })(window);