LibImgDown

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

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址