ImageDownloaderLib

Image downloader for manga download scripts.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/451810/1398192/ImageDownloaderLib.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.  
  25. // elements
  26. let startNumInputElement = null;
  27. let endNumInputElement = null;
  28. let downloadButtonElement = null;
  29. let panelElement = null;
  30.  
  31. // svg icons
  32. 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>`;
  33. 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>`;
  34.  
  35. // initialization
  36. function init({
  37. maxImageAmount,
  38. getImagePromises,
  39. title = `package_${Date.now()}`,
  40. imageSuffix = 'jpg',
  41. zipOptions = {},
  42. positionOptions = {}
  43. }) {
  44. // assign value
  45. maxNum = maxImageAmount;
  46.  
  47. // setup UI
  48. setupUI(positionOptions);
  49.  
  50. // setup update notification
  51. setupUpdateNotification();
  52.  
  53. // add click event listener to download button
  54. downloadButtonElement.onclick = function () {
  55. if (!isOKToDownload()) return;
  56.  
  57. this.disabled = true;
  58. this.textContent = "Processing";
  59. this.style.backgroundColor = '#aaa';
  60. this.style.cursor = 'not-allowed';
  61. download(getImagePromises, title, imageSuffix, zipOptions);
  62. }
  63. }
  64.  
  65. // setup UI
  66. function setupUI(positionOptions) {
  67. // common input element style
  68. const inputElementStyle = `
  69. box-sizing: content-box;
  70. padding: 1px 2px;
  71. width: 40%;
  72. height: 26px;
  73.  
  74. border: 1px solid #aaa;
  75. border-radius: 4px;
  76.  
  77. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  78. text-align: center;
  79. `;
  80.  
  81. // create start number input element
  82. startNumInputElement = document.createElement('input');
  83. startNumInputElement.id = 'ImageDownloader-StartNumInput';
  84. startNumInputElement.style = inputElementStyle;
  85. startNumInputElement.type = 'text';
  86. startNumInputElement.value = 1;
  87.  
  88. // create end number input element
  89. endNumInputElement = document.createElement('input');
  90. endNumInputElement.id = 'ImageDownloader-EndNumInput';
  91. endNumInputElement.style = inputElementStyle;
  92. endNumInputElement.type = 'text';
  93. endNumInputElement.value = maxNum;
  94.  
  95. // prevent keyboard input from being blocked
  96. startNumInputElement.onkeydown = (e) => e.stopPropagation();
  97. endNumInputElement.onkeydown = (e) => e.stopPropagation();
  98.  
  99. // create 'to' span element
  100. const toSpanElement = document.createElement('span');
  101. toSpanElement.id = 'ImageDownloader-ToSpan';
  102. toSpanElement.textContent = 'to';
  103. toSpanElement.style = `
  104. margin: 0 6px;
  105. color: black;
  106. line-height: 1;
  107. word-break: keep-all;
  108. user-select: none;
  109. `;
  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. width: 128px;
  118. height: 48px;
  119.  
  120. display: flex;
  121. justify-content: center;
  122. align-items: center;
  123.  
  124. font-size: 14px;
  125. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  126. color: #fff;
  127. line-height: 1.2;
  128.  
  129. background-color: #0984e3;
  130. border: none;
  131. border-radius: 4px;
  132. cursor: pointer;
  133. `;
  134.  
  135. // create range input container element
  136. const rangeInputContainerElement = document.createElement('div');
  137. rangeInputContainerElement.id = 'ImageDownloader-RangeInputContainer';
  138. rangeInputContainerElement.style = `
  139. display: flex;
  140. justify-content: center;
  141. align-items: baseline;
  142. `;
  143.  
  144. // create panel element
  145. panelElement = document.createElement('div');
  146. panelElement.id = 'ImageDownloader-Panel';
  147. panelElement.style = `
  148. position: fixed;
  149. top: 72px;
  150. left: 72px;
  151. z-index: 999999999;
  152.  
  153. box-sizing: border-box;
  154. padding: 8px;
  155. width: 146px;
  156. height: 106px;
  157.  
  158. display: flex;
  159. flex-direction: column;
  160. justify-content: center;
  161. align-items: baseline;
  162.  
  163. font-size: 14px;
  164. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  165. letter-spacing: normal;
  166.  
  167. background-color: #f1f1f1;
  168. border: 1px solid #aaa;
  169. border-radius: 4px;
  170. `;
  171.  
  172. // modify panel position according to 'positionOptions'
  173. for (const [key, value] of Object.entries(positionOptions)) {
  174. if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
  175. panelElement.style[key] = value;
  176. }
  177. }
  178.  
  179. // assemble and then insert into document
  180. rangeInputContainerElement.appendChild(startNumInputElement);
  181. rangeInputContainerElement.appendChild(toSpanElement);
  182. rangeInputContainerElement.appendChild(endNumInputElement);
  183. panelElement.appendChild(rangeInputContainerElement);
  184. panelElement.appendChild(downloadButtonElement);
  185. document.body.appendChild(panelElement);
  186. }
  187.  
  188. // setup update notification
  189. async function setupUpdateNotification() {
  190. if (typeof GM_info === 'undefined' || typeof GM_xmlhttpRequest === 'undefined') return;
  191.  
  192. // get local version
  193. const localVersion = Number(GM_info.script.version);
  194.  
  195. // get latest version
  196. const scriptID = (GM_info.script.homepageURL || GM_info.script.homepage).match(/scripts\/(?<id>\d+)-/)?.groups?.id;
  197. const scriptURL = `https://update.gf.qytechs.cn/scripts/${scriptID}/raw.js`;
  198. const latestVersionString = await new Promise(resolve => {
  199. GM_xmlhttpRequest({
  200. method: 'GET',
  201. url: scriptURL,
  202. responseType: 'text',
  203. onload: res => resolve(res.response.match(/@version\s+(?<version>[0-9\.]+)/)?.groups?.version)
  204. });
  205. });
  206. const latestVersion = Number(latestVersionString);
  207.  
  208. if (Number.isNaN(localVersion) || Number.isNaN(latestVersion)) return;
  209. if (latestVersion <= localVersion) return;
  210.  
  211. // show update notification
  212. const updateLinkElement = document.createElement('a');
  213. updateLinkElement.id = 'ImageDownloader-UpdateLink';
  214. updateLinkElement.href = scriptURL.replace('raw.js', 'raw.user.js');
  215. updateLinkElement.innerHTML = `Update to V${latestVersionString}${externalLinkSVG}`;
  216. updateLinkElement.style = `
  217. position: absolute;
  218. bottom: -38px;
  219. left: -1px;
  220.  
  221. display: flex;
  222. justify-content: space-around;
  223. align-items: center;
  224.  
  225. box-sizing: border-box;
  226. padding: 8px;
  227. width: 146px;
  228. height: 32px;
  229.  
  230. font-size: 14px;
  231. font-family: 'Consolas', 'Monaco', 'Microsoft YaHei';
  232. text-decoration: none;
  233. color: white;
  234.  
  235. background-color: #32CD32;
  236. border-radius: 4px;
  237. `;
  238. updateLinkElement.onclick = () => setTimeout(() => {
  239. updateLinkElement.removeAttribute('href');
  240. updateLinkElement.innerHTML = `Please Reload${reloadSVG}`;
  241. updateLinkElement.style.cursor = 'default';
  242. }, 1000);
  243.  
  244. panelElement.appendChild(updateLinkElement);
  245. }
  246.  
  247. // check validity of page nums from input
  248. function isOKToDownload() {
  249. const startNum = Number(startNumInputElement.value);
  250. const endNum = Number(endNumInputElement.value);
  251.  
  252. if (Number.isNaN(startNum) || Number.isNaN(endNum)) { alert("请正确输入数值\nPlease enter page number correctly."); return false; }
  253. if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) { alert("请正确输入数值\nPlease enter page number correctly."); return false; }
  254. if (startNum < 1 || endNum < 1) { alert("页码的值不能小于1\nPage number should not smaller than 1."); return false; }
  255. if (startNum > maxNum || endNum > maxNum) { alert(`页码的值不能大于${maxNum}\nPage number should not bigger than ${maxNum}.`); return false; }
  256. if (startNum > endNum) { alert("起始页码的值不能大于终止页码的值\nNumber of start should not bigger than number of end."); return false; }
  257.  
  258. return true;
  259. }
  260.  
  261. // start downloading
  262. async function download(getImagePromises, title, imageSuffix, zipOptions) {
  263. const startNum = Number(startNumInputElement.value);
  264. const endNum = Number(endNumInputElement.value);
  265. promiseCount = endNum - startNum + 1;
  266.  
  267. // start downloading images, max amount of concurrent requests is limited to 4
  268. let images = [];
  269. for (let num = startNum; num <= endNum; num += 4) {
  270. const from = num;
  271. const to = Math.min(num + 3, endNum);
  272. try {
  273. const result = await Promise.all(getImagePromises(from, to));
  274. images = images.concat(result);
  275. } catch (error) {
  276. return; // cancel downloading
  277. }
  278. }
  279.  
  280. // configure file structure of zip archive
  281. JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
  282. const zip = new JSZip();
  283. const zipTitle = title.replaceAll(/\/|\\|\:|\*|\?|\"|\<|\>|\|/g, ''); // remove some characters
  284. const folder = zip.folder(zipTitle);
  285. for (const [index, image] of images.entries()) {
  286. const filename = `${String(index + 1).padStart(images.length >= 100 ? String(images.length).length : 2, '0')}.${imageSuffix}`;
  287. folder.file(filename, image, zipOptions);
  288. }
  289.  
  290. // start zipping & show progress
  291. const zipProgressHandler = (metadata) => { downloadButtonElement.innerHTML = `Zipping<br>(${metadata.percent.toFixed()}%)`; }
  292. const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);
  293.  
  294. // open 'Save As' window to save
  295. saveAs(content, `${zipTitle}.zip`);
  296.  
  297. // all completed
  298. downloadButtonElement.textContent = "Completed";
  299. }
  300.  
  301. // handle promise fulfilled
  302. function fulfillHandler(res) {
  303. if (!isErrorOccurred) {
  304. fulfillCount++;
  305. downloadButtonElement.innerHTML = `Processing<br>(${fulfillCount}/${promiseCount})`;
  306. }
  307.  
  308. return res;
  309. }
  310.  
  311. // handle promise rejected
  312. function rejectHandler(err) {
  313. isErrorOccurred = true;
  314. console.error(err);
  315.  
  316. downloadButtonElement.textContent = 'Error Occurred';
  317. downloadButtonElement.style.backgroundColor = 'red';
  318.  
  319. return Promise.reject(err);
  320. }
  321.  
  322. return { init, fulfillHandler, rejectHandler };
  323. })(window);

QingJ © 2025

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