ImageDownloaderLib

Image downloader for manga download scripts.

当前为 2022-12-18 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/451810/1129512/ImageDownloaderLib.js

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