LibImgDown

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

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

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

QingJ © 2025

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