LibImgDown

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

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

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/528949/1558021/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. // svg icons
  40. 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>`;
  41. 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>`;
  42.  
  43. // initialization
  44. function init({
  45. maxImageAmount,
  46. getImagePromises,
  47. title = `package_${Date.now()}`,
  48. WidthText = 0,
  49. HeightText = 0,
  50. imageSuffix = 'jpg',
  51. zipOptions = {},
  52. positionOptions = {}
  53. }) {
  54. // assign value
  55. maxNum = maxImageAmount;
  56. // setup UI
  57. setupUI(positionOptions, title, WidthText, HeightText);
  58. // setup update notification
  59. setupUpdateNotification();
  60. // add click event listener to download button
  61. downloadButtonElement.onclick = function () {
  62. if (!isOKToDownload()) return;
  63. this.disabled = true;
  64. this.textContent = "Processing";
  65. this.style.backgroundColor = '#aaa';
  66. this.style.cursor = 'not-allowed';
  67. download(getImagePromises, title, imageSuffix, zipOptions);
  68. };
  69. }
  70.  
  71. // setup UI
  72. function setupUI(positionOptions, title, WidthText, HeightText) {
  73. title = sanitizeFileName(title);
  74. // common input element style
  75. const inputElementStyle = `
  76. box-sizing: content-box;
  77. padding: 0px 0px;
  78. width: 40%;
  79. height: 26px;
  80. border: 1px solid #aaa;
  81. border-radius: 4px;
  82. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  83. text-align: center;
  84. `;
  85. // create start number input element
  86. startNumInputElement = document.createElement('input');
  87. startNumInputElement.id = 'ImageDownloader-StartNumInput';
  88. startNumInputElement.style = inputElementStyle;
  89. startNumInputElement.type = 'text';
  90. startNumInputElement.value = 1;
  91. // create end number input element
  92. endNumInputElement = document.createElement('input');
  93. endNumInputElement.id = 'ImageDownloader-EndNumInput';
  94. endNumInputElement.style = inputElementStyle;
  95. endNumInputElement.type = 'text';
  96. endNumInputElement.value = maxNum;
  97. // prevent keyboard input from being blocked
  98. startNumInputElement.onkeydown = (e) => e.stopPropagation();
  99. endNumInputElement.onkeydown = (e) => e.stopPropagation();
  100. // create 'to' span element
  101. const toSpanElement = document.createElement('span');
  102. toSpanElement.id = 'ImageDownloader-ToSpan';
  103. toSpanElement.textContent = 'to';
  104. toSpanElement.style = `
  105. margin: 0 6px;
  106. color: black;
  107. line-height: 1;
  108. word-break: keep-all;
  109. user-select: none;
  110. `;
  111. // create download button element
  112. downloadButtonElement = document.createElement('button');
  113. downloadButtonElement.id = 'ImageDownloader-DownloadButton';
  114. downloadButtonElement.textContent = 'Download';
  115. downloadButtonElement.style = `
  116. margin-top: 8px;
  117. margin-left: auto; // 追加
  118. width: 128px;
  119. height: 48px;
  120. padding: 5px 5px;
  121. display: block;
  122. justify-content: center;
  123. align-items: center;
  124. font-size: 14px;
  125. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  126. color: #fff;
  127. line-height: 1.2;
  128. background-color: #0984e3;
  129. border: none;
  130. border-radius: 4px;
  131. cursor: pointer;
  132. `;
  133. const toggleButton = document.createElement('button');
  134. toggleButton.id = 'ImageDownloader-ToggleButton';
  135. toggleButton.textContent = 'UI CLOSE';
  136. toggleButton.style = `
  137. position: fixed;
  138. top: 45px;
  139. left: 5px;
  140. z-index: 999999999;
  141. padding: 2px 5px;
  142. font-size: 14px;
  143. font-weight: 'bold';
  144. font-family: 'Monaco', 'Microsoft YaHei';
  145. color: #fff;
  146. background-color: #000000;
  147. border: 1px solid #aaa;
  148. border-radius: 4px;
  149. cursor: pointer;
  150. `;
  151. document.body.appendChild(toggleButton);
  152. let isUIVisible = false; // 初期状態を非表示に設定
  153. function toggleUI() {
  154. if (isUIVisible) {
  155. panelElement.style.display = 'none';
  156. toggleButton.textContent = 'UI OPEN';
  157. } else {
  158. panelElement.style.display = 'flex';
  159. toggleButton.textContent = 'UI CLOSE';
  160. }
  161. isUIVisible = !isUIVisible;
  162. }
  163. toggleButton.addEventListener('click', toggleUI)
  164. // create range input container element
  165. const rangeInputContainerElement = document.createElement('div');
  166. rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
  167. rangeInputContainerElement.style = `
  168. display: flex;
  169. justify-content: center;
  170. align-items: baseline;
  171. `;
  172. // create range input container element
  173. const rangeInputRadioElement = document.createElement('div');
  174. rangeInputRadioElement.id = 'ImageDownloader-RadioChecker';
  175. rangeInputRadioElement.style = `
  176. display: flex;
  177. justify-content: center;
  178. align-items: baseline;
  179. `;
  180. // create panel element
  181. panelElement = document.createElement('div');
  182. panelElement.id = 'ImageDownloader-Panel';
  183. panelElement.style = `
  184. position: fixed;
  185. top: 80px;
  186. left: 5px;
  187. z-index: 999999999;
  188. box-sizing: border-box;
  189. padding: 0px;
  190. width: auto;
  191. min-width: 200px;
  192. max-width: 300px;
  193. height: auto;
  194. display: none;
  195. flex-direction: column;
  196. justify-content: center;
  197. align-items: baseline;
  198. font-size: 12px;
  199. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  200. letter-spacing: normal;
  201. background-color: #f1f1f1;
  202. border: 1px solid #aaa;
  203. border-radius: 4px;
  204. `;
  205. // modify panel position according to 'positionOptions'
  206. for (const [key, value] of Object.entries(positionOptions)) {
  207. if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
  208. panelElement.style[key] = value;
  209. }
  210. }
  211.  
  212. // create folder radio buttons
  213. folderRadioYes = document.createElement('input');
  214. folderRadioYes.type = 'radio';
  215. folderRadioYes.name = 'createFolder';
  216. folderRadioYes.value = 'yes';
  217. folderRadioYes.id = 'createFolderYes';
  218. folderRadioNo = document.createElement('input');
  219. folderRadioNo.type = 'radio';
  220. folderRadioNo.name = 'createFolder';
  221. folderRadioNo.value = 'no';
  222. folderRadioNo.id = 'createFolderNo';
  223. folderRadioNo.checked = true;
  224. // フォルダ名入力欄の作成
  225. folderNameInput = document.createElement('textarea');
  226. folderNameInput.id = 'folderNameInput';
  227. folderNameInput.value = title; // titleを初期値として使用
  228. folderNameInput.disabled = true;
  229. folderNameInput.style = `
  230. ${inputElementStyle}
  231. resize: vertical;
  232. height: auto;
  233. width: 99%;
  234. min-height: 45px;
  235. max-height: 200px;
  236. padding: 0px 0px;
  237. border: 1px solid #aaa;
  238. border-radius: 1px;
  239. font-size: 11px;
  240. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  241. text-align: left;
  242. `;
  243. // ZIPファイル名入力欄の作成
  244. zipFileNameInput = document.createElement('textarea');
  245. zipFileNameInput.id = 'zipFileNameInput';
  246. zipFileNameInput.value = `${title}.zip`; // titleを使用してZIPファイル名を設定
  247. zipFileNameInput.style = `
  248. ${inputElementStyle}
  249. resize: vertical;
  250. height: auto;
  251. width: 99%;
  252. min-height: 45px;
  253. max-height: 200px;
  254. padding: 0px 0px;
  255. border: 1px solid #aaa;
  256. border-radius: 1px;
  257. font-size: 11px;
  258. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  259. text-align: left;
  260. `;
  261. // add event listeners for radio buttons
  262. folderRadioYes.addEventListener('change', () => {
  263. createFolder = true;
  264. folderNameInput.disabled = false;
  265. });
  266. folderRadioNo.addEventListener('change', () => {
  267. createFolder = false;
  268. folderNameInput.disabled = true;
  269. });
  270. // assemble and then insert into document
  271. rangeInputContainerElement.appendChild(startNumInputElement);
  272. rangeInputContainerElement.appendChild(toSpanElement);
  273. rangeInputContainerElement.appendChild(endNumInputElement);
  274. panelElement.appendChild(rangeInputContainerElement);
  275. rangeInputRadioElement.appendChild(document.createTextNode('フォルダ:'));
  276. rangeInputRadioElement.appendChild(folderRadioYes);
  277. rangeInputRadioElement.appendChild(document.createTextNode('作成 '));
  278. rangeInputRadioElement.appendChild(folderRadioNo);
  279. rangeInputRadioElement.appendChild(document.createTextNode('不要'));
  280. panelElement.appendChild(rangeInputRadioElement);
  281. panelElement.appendChild(document.createTextNode('フォルダ名: '));
  282. panelElement.appendChild(folderNameInput);
  283. panelElement.appendChild(document.createElement('br'));
  284. panelElement.appendChild(document.createTextNode('ZIPファイル名: '));
  285. panelElement.appendChild(zipFileNameInput);
  286. panelElement.appendChild(document.createTextNode(` サイズ: ${WidthText} x `));
  287. panelElement.appendChild(document.createTextNode(`${HeightText}`));
  288. panelElement.appendChild(downloadButtonElement);
  289. document.body.appendChild(panelElement);
  290. }
  291.  
  292. // setup update notification
  293. async function setupUpdateNotification() {
  294. if (typeof GM_info === 'undefined' || typeof GM_xmlhttpRequest === 'undefined') return;
  295. // get local version
  296. const localVersion = Number(GM_info.script.version);
  297. // get latest version
  298. //const scriptID = (GM_info.script.homepageURL || GM_info.script.homepage).match(/scripts\/(?<id>\d+)-/)?.groups?.id;
  299. let scriptID = null;
  300. const homepageURL = GM_info.script.homepageURL || GM_info.script.homepage;
  301. if (homepageURL) {
  302. const match = homepageURL.match(/scripts\/(?<id>\d+)-/);
  303. if (match && match.groups && match.groups.id) {
  304. scriptID = match.groups.id;
  305. }
  306. }
  307. const scriptURL = `https://update.gf.qytechs.cn/scripts/${scriptID}/raw.js`;
  308. const latestVersionString = await new Promise(resolve => {
  309. GM_xmlhttpRequest({
  310. method: 'GET',
  311. url: scriptURL,
  312. responseType: 'text',
  313. onload: res => resolve(res.response.match(/@version\s+(?<version>[0-9\.]+)/)?.groups?.version)
  314. });
  315. });
  316. const latestVersion = Number(latestVersionString);
  317. if (Number.isNaN(localVersion) || Number.isNaN(latestVersion)) return;
  318. if (latestVersion <= localVersion) return;
  319. // show update notification
  320. const updateLinkElement = document.createElement('a');
  321. updateLinkElement.id = 'ImageDownloader-UpdateLink';
  322. updateLinkElement.href = scriptURL.replace('raw.js', 'raw.user.js');
  323. updateLinkElement.innerHTML = `Update to V${latestVersionString}${externalLinkSVG}`;
  324. updateLinkElement.style = `
  325. position: absolute;
  326. bottom: -38px;
  327. left: -1px;
  328. display: flex;
  329. justify-content: space-around;
  330. align-items: center;
  331. box-sizing: border-box;
  332. padding: 8px;
  333. width: 146px;
  334. height: 32px;
  335. font-size: 14px;
  336. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  337. text-decoration: none;
  338. color: white;
  339. background-color: #32CD32;
  340. border-radius: 4px;
  341. `;
  342. updateLinkElement.onclick = () => setTimeout(() => {
  343. updateLinkElement.removeAttribute('href');
  344. updateLinkElement.innerHTML = `Please Reload${reloadSVG}`;
  345. updateLinkElement.style.cursor = 'default';
  346. }, 1000);
  347. panelElement.appendChild(updateLinkElement);
  348. }
  349.  
  350. // check validity of page nums from input
  351. function isOKToDownload() {
  352. const startNum = Number(startNumInputElement.value);
  353. const endNum = Number(endNumInputElement.value);
  354. if (Number.isNaN(startNum) || Number.isNaN(endNum)) { alert("正しい値を入力して\nPlease enter page number correctly."); return false; }
  355. if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) { alert("正しい値を入力して\nPlease enter page number correctly."); return false; }
  356. if (startNum < 1 || endNum < 1) { alert("ページ番号の値を1より小さくすることはできません1\nPage number should not smaller than 1."); return false; }
  357. if (startNum > maxNum || endNum > maxNum) { alert(`ページ番号の値を1より大きくすることはできません${maxNum}\nPage number should not bigger than ${maxNum}.`); return false; }
  358. if (startNum > endNum) { alert("開始ページ番号の値を終了ページ番号の値より大きくすることはできません\nNumber of start should not bigger than number of end."); return false; }
  359. return true;
  360. }
  361.  
  362. // start downloading
  363. async function download(getImagePromises, title, imageSuffix, zipOptions) {
  364. const startNum = Number(startNumInputElement.value);
  365. const endNum = Number(endNumInputElement.value);
  366. promiseCount = endNum - startNum + 1;
  367. // start downloading images, max amount of concurrent requests is limited to 4
  368. let images = [];
  369. for (let num = startNum; num <= endNum; num += 4) {
  370. const from = num;
  371. const to = Math.min(num + 3, endNum);
  372. try {
  373. const result = await Promise.all(getImagePromises(from, to));
  374. images = images.concat(result);
  375. } catch (error) {
  376. return; // cancel downloading
  377. }
  378. }
  379.  
  380. // configure file structure of zip archive
  381. JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
  382. zip = new JSZip();
  383. const { folderName, zipFileName } = sanitizeInputs(folderNameInput, zipFileNameInput);
  384. if (createFolder) {
  385. const folder = zip.folder(folderName);
  386. for (const [index, image] of images.entries()) {
  387. const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
  388. folder.file(filename, image, zipOptions);
  389. }
  390. } else {
  391. for (const [index, image] of images.entries()) {
  392. const filename = `${String(index + 1).padStart(3, '0')}.${imageSuffix}`;
  393. zip.file(filename, image, zipOptions);
  394. }
  395. }
  396.  
  397. // start zipping & show progress
  398. const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `Zipping(${metadata.percent.toFixed()}%)`; };
  399. const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);
  400. // open 'Save As' window to save
  401. saveAs(content, zipFileName);
  402. // 全て完了
  403. downloadButtonElement.textContent = "Completed";
  404. // ボタンを再度押せるようにする
  405. }
  406.  
  407. // ファイル名整形用の関数
  408. function sanitizeFileName(str) {
  409. return str.trim()
  410. // 全角英数字を半角に変換
  411. .replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0))
  412. // 連続する空白(全角含む)を半角スペース1つに統一
  413. .replace(/[\s\u3000]+/g, ' ')
  414. // 「!?」または「?!」を「⁉」に置換
  415. .replace(/[!?][!?]/g, '⁉')
  416. // 特定の全角記号を対応する半角記号に変換
  417. .replace(/[!#$%&’,.()+-=@^_{}]/g, s => {
  418. const from = '!#$%&’,.()+-=@^_{}';
  419. const to = "!#$%&',.()+-=@^_{}";
  420. return to[from.indexOf(s)];
  421. })
  422. // ファイル名に使えない文字をハイフンに置換
  423. .replace(/[\\/:*?"<>|]/g, '-');
  424. }
  425.  
  426. // folderNameとzipFileNameの整形処理関数
  427. function sanitizeInputs(folderNameInput, zipFileNameInput) {
  428. const folderName = sanitizeFileName(folderNameInput.value);
  429. const zipFileName = sanitizeFileName(zipFileNameInput.value);
  430. return { folderName, zipFileName };
  431. }
  432.  
  433. // handle promise fulfilled
  434. function fulfillHandler(res) {
  435. if (!isErrorOccurred) {
  436. fulfillCount++;
  437. downloadButtonElement.innerHTML = `Processing(${fulfillCount}/${promiseCount})`;
  438. }
  439. return res;
  440. }
  441.  
  442. // handle promise rejected
  443. function rejectHandler(err) {
  444. isErrorOccurred = true;
  445. console.error(err);
  446. downloadButtonElement.textContent = 'Error Occurred';
  447. downloadButtonElement.style.backgroundColor = 'red';
  448. return Promise.reject(err);
  449. }
  450.  
  451. function reset() {
  452. maxNum = 0;
  453. promiseCount = 0;
  454. fulfillCount = 0;
  455. isErrorOccurred = false;
  456. zip = null; // ZIPオブジェクトの初期化
  457. imageDataArray = []; //imageDataArrayの初期化
  458. // UI要素のリセット
  459. if (startNumInputElement) startNumInputElement.value = 1;
  460. if (endNumInputElement) endNumInputElement.value = 1;
  461. if (downloadButtonElement) {
  462. downloadButtonElement.disabled = false;
  463. downloadButtonElement.textContent = "Download";
  464. downloadButtonElement.style.backgroundColor = '#0984e3';
  465. downloadButtonElement.style.cursor = 'pointer';
  466. }
  467. // パネルの削除(再作成のため)
  468. if (panelElement && panelElement.parentNode) {
  469. panelElement.parentNode.removeChild(panelElement);
  470. }
  471. }
  472.  
  473. return { init, fulfillHandler, rejectHandler };
  474. })(window);

QingJ © 2025

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