Pixiv Downloader

一键下载Pixiv各页面原图。支持多图下载,动图下载,按作品标签下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webp | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。

Versión del día 2/9/2023. Echa un vistazo a la versión más reciente.

  1. // ==UserScript==
  2. // @name Pixiv Downloader
  3. // @namespace https://greasyfork.org/zh-CN/scripts/432150
  4. // @version 0.9.2
  5. // @description:en Download the original images of Pixiv pages with one click. Supports:multiple illustrations, ugoira(animation), and batch downloads of artists' work. Ugoira support format conversion: Gif | Apng | Webp | Webm. The downloaded images will be saved in a separate folder named after the artist (you need to adjust the tampermonkey "Download" setting to "Browser API"). A record of downloaded images is kept.
  6. // @description 一键下载Pixiv各页面原图。支持多图下载,动图下载,按作品标签下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webp | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。
  7. // @description:zh-TW 一鍵下載Pixiv各頁面原圖。支持多圖下載,動圖下載,按作品標籤下載,畫師作品批次下載。動圖支持格式轉換:Gif | Apng | Webp | Webm。下載的圖片將保存到以畫師名命名的單獨文件夾(需要調整tampermonkey“下載”設置為“瀏覽器API”)。保留已下載圖片的紀錄。
  8. // @author ruaruarua
  9. // @match https://www.pixiv.net/*
  10. // @icon https://www.pixiv.net/favicon.ico
  11. // @noframes
  12. // @grant unsafeWindow
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM_download
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_info
  18. // @grant GM_registerMenuCommand
  19. // @connect i.pximg.net
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  21. // @require https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js
  22. // @require https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js
  23. // @require https://greasyfork.org/scripts/455256-toanimatedwebp/code/toAnimatedWebp.js?version=1120088
  24. // ==/UserScript==
  25. (function (workerChunk, GIF, JSZip, dayjs) {
  26. 'use strict';
  27.  
  28. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  29.  
  30. var workerChunk__default = /*#__PURE__*/_interopDefaultLegacy(workerChunk);
  31. var GIF__default = /*#__PURE__*/_interopDefaultLegacy(GIF);
  32. var JSZip__default = /*#__PURE__*/_interopDefaultLegacy(JSZip);
  33. var dayjs__default = /*#__PURE__*/_interopDefaultLegacy(dayjs);
  34.  
  35. var e=[],t$1=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t$1[u]={}),a=t$1[u]&&t$1[u][s]?t$1[u][s]:t$1[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}}
  36.  
  37. var css$4 = "@property --pdl-progress {\n syntax: '<percentage>';\n inherits: true;\n initial-value: 0%;\n}\n\n@keyframes pdl_loading {\n 100% {\n transform: translate(-50%, -50%) rotate(360deg);\n }\n}\n\n:root {\n --pdl-btn-top: 100;\n --pdl-btn-left: 0;\n --pdl-btn-self-bookmark-top: 75;\n --pdl-btn-self-bookmark-left: 100;\n}\n\n.pdl-btn {\n position: relative;\n border-radius: 4px;\n background: no-repeat center/85%;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%233C3C3C' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E\");\n color: #01b468;\n display: inline-block;\n font-size: 13px;\n font-weight: bold;\n height: 32px;\n line-height: 32px;\n margin: 0;\n overflow: hidden;\n padding: 0;\n border: none;\n text-decoration: none !important;\n text-align: center;\n text-overflow: ellipsis;\n user-select: none;\n white-space: nowrap;\n width: 32px;\n z-index: 10;\n cursor: pointer;\n}\n\n.pdl-btn-main {\n margin: 0 0 0 10px;\n}\n\n.pdl-btn-sub {\n position: absolute;\n background-color: rgba(255, 255, 255, 0.5);\n top: calc((100% - 32px) * var(--pdl-btn-top) / 100);\n left: calc((100% - 32px) * var(--pdl-btn-left) / 100);\n}\n\n.pdl-btn-sub.artworks {\n position: sticky;\n top: 40px;\n left: 0px;\n}\n\n.pdl-btn-sub.presentation {\n position: fixed;\n top: 50px;\n right: 20px;\n left: auto;\n border-radius: 8px;\n}\n\n.pdl-btn-sub.self-bookmark {\n top: calc((100% - 32px) * var(--pdl-btn-self-bookmark-top) / 100);\n left: calc((100% - 32px) * var(--pdl-btn-self-bookmark-left) / 100);\n}\n\n.pdl-error {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23EA0000' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E\") !important;\n}\n\n.pdl-complete {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%2301B468' d='M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z'%3E%3C/path%3E %3C/svg%3E\") !important;\n}\n\n.pdl-progress {\n background-image: none !important;\n cursor: default !important;\n}\n\n.pdl-progress:after {\n content: '';\n display: inline-block;\n position: absolute;\n top: 50%;\n left: 50%;\n width: 27px;\n height: 27px;\n transform: translate(-50%, -50%);\n -webkit-mask: radial-gradient(transparent, transparent 54%, #000 57%, #000);\n mask: radial-gradient(transparent, transparent 54%, #000 57%, #000);\n border-radius: 50%;\n}\n\n.pdl-progress:not(:empty):after {\n background: conic-gradient(#01b468 0, #01b468 var(--pdl-progress), transparent var(--pdl-progress), transparent);\n transition: --pdl-progress 0.2s ease;\n}\n\n.pdl-progress:empty:after {\n background: conic-gradient(#01b468 0, #01b468 25%, #01b46833 25%, #01b46833);\n animation: 1.5s infinite linear pdl_loading;\n}\n\n.pdl-btn.pdl-tag {\n height: auto;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n left: -1px;\n background-color: rgba(0, 0, 0, 0.12);\n transition: background-image 0.5s;\n}\n\n.pdl-btn.pdl-tag.pdl-tag-hide,\n.pdl-btn.pdl-modal-tag.pdl-tag-hide {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3C/svg%3E\");\n pointer-events: none;\n}\n\n.pdl-btn.pdl-modal-tag {\n position: absolute;\n right: 65px;\n top: 6px;\n background-origin: content-box;\n border-radius: 4px;\n padding: 5px;\n width: 42px;\n height: 50px;\n background-color: rgba(0, 0, 0, 0.04);\n transition: 0.25s background-color;\n}\n\n.pdl-btn.pdl-modal-tag:not(.pdl-tag-hide):hover {\n background-color: rgba(0, 0, 0, 0.12);\n}\n\n.pdl-wrap-artworks {\n position: absolute;\n right: 8px;\n top: 0px;\n bottom: 0px;\n margin-top: 40px;\n}\n";
  38. n(css$4,{});
  39.  
  40. var css$3 = ".pdl-modal * {\n font-family: 'win-bug-omega, system-ui, -apple-system, \"Segoe UI\", Roboto, Ubuntu, Cantarell, \"Noto Sans\", \"Hiragino Kaku Gothic ProN\", Meiryo, sans-serif';\n line-height: 1.15;\n}\n\n.pdl-modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n z-index: 99;\n background-color: rgba(0, 0, 0, 0.32);\n user-select: none;\n}\n\n.pdl-dialog {\n position: relative;\n background-color: #fff;\n border-radius: 24px;\n margin: auto;\n padding: 20px 40px 30px 40px;\n width: 600px;\n font-size: 16px;\n}\n\n.pdl-dialog-header > h3 {\n font-weight: bold;\n font-size: 1.17em;\n margin: 1em 0;\n}\n\n.pdl-dialog-close {\n position: absolute;\n top: 10px;\n right: 10px;\n margin: 0;\n padding: 0;\n width: 25px;\n height: 25px;\n border: none;\n cursor: pointer;\n border-radius: 50%;\n background-color: transparent;\n transform: rotate(45deg);\n transition: 0.25s background-color;\n background: linear-gradient(rgb(125, 125, 125) 0%, rgb(125, 125, 125) 100%) center/18px 2px no-repeat,\n linear-gradient(rgb(125, 125, 125) 0%, rgb(125, 125, 125) 100%) center/2px 18px no-repeat;\n}\n\n.pdl-dialog-close:hover {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.pdl-dialog-content hr {\n height: 0 !important;\n margin: 0;\n border: none;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.pdl-dialog-content hr.sub {\n margin-inline-start: 1.5em;\n}\n\n.pdl-dialog-content hr.vertical {\n height: 1.15em !important;\n border: none;\n border-left: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n/* button */\n.pdl-dialog-button {\n font-size: 16px;\n background-color: #fff;\n border: 1px solid rgb(125, 125, 125);\n border-radius: 5px;\n padding: 0.5em 1.5em;\n cursor: pointer;\n transition: 0.2s opacity;\n line-height: 1.15;\n}\n.pdl-dialog-button:hover {\n opacity: 0.7;\n}\n\n.pdl-dialog-button.primary {\n color: #fff;\n background-color: #0096fa;\n border-color: #0096fa;\n}\n\n.pdl-dialog-button.icon {\n padding: 0.5em 0.8em;\n}\n\n.pdl-dialog-button[disabled] {\n cursor: not-allowed !important;\n color: #c0c4cc;\n background-color: #fff;\n border-color: #e4e7ed;\n opacity: 1 !important;\n}\n\n.pdl-dialog-button[disabled]:hover {\n opacity: 1 !important;\n}\n\n.pdl-dialog-button.primary[disabled] {\n color: #fff;\n background-color: #a0cfff;\n border-color: #a0cfff;\n}\n\n/* tabs */\n.pdl-tabs-nav {\n display: flex;\n align-items: center;\n border-bottom: 1px solid #dcdfe6;\n position: relative;\n}\n\n.pdl-tabs-nav .pdl-tab-item {\n padding: 0px 16px;\n line-height: 2.5;\n cursor: pointer;\n transition: 0.2s color;\n}\n\n.pdl-tabs-nav .pdl-tab-item:nth-child(2) {\n padding-left: 0px;\n}\n\n.pdl-tabs-nav .pdl-tab-item:last-child {\n padding-right: 0px;\n}\n\n.pdl-tab-item:hover,\n.pdl-tab-item.active {\n color: #0096fa;\n}\n\n.pdl-tab-item.active {\n font-weight: 700;\n}\n\n.pdl-tabs-content {\n padding: 16px;\n min-height: 200px;\n}\n\n.pdl-tabs-nav .pdl-tabs__active-bar {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 2px;\n background-color: #0096fa;\n z-index: 1;\n transition: width 0.2s, transform 0.2s;\n}\n\n/* filename */\n\n#pdl-setting-filename a {\n color: #0096fa;\n text-decoration: underline;\n}\n#pdl-setting-filename .tags-option label,\n#pdl-setting-filename .tags-option input {\n cursor: pointer;\n}\n\n#pdl-setting-filename .tags-content {\n flex: 1;\n display: flex;\n gap: 20px;\n}\n\n#pdl-setting-filename .pdl-input-wrap,\n#pdl-setting-filename .tags-option {\n display: flex;\n align-items: center;\n margin: 12px 0;\n gap: 12px;\n}\n\n#pdl-setting-filename .pdl-input-wrap label,\n#pdl-setting-filename .tags-option .tags-title {\n cursor: default;\n font-weight: 700;\n width: 7em;\n}\n\n#pdl-setting-filename .pdl-input-wrap input[type='text'] {\n height: auto;\n padding: 0.5em;\n font-size: 16px;\n line-height: 1.5;\n flex: 1;\n border: 1px solid #333;\n}\n\n#pdl-setting-filename .pdl-input-wrap input[type='text']:focus {\n background-color: #fff !important;\n}\n\n#pdl-setting-filename .pdl-input-wrap button {\n line-height: 1.5;\n}\n\n#pdl-setting-filename .pdl-options {\n display: flex;\n align-items: center;\n justify-content: space-between;\n cursor: pointer;\n padding: 0.6em 0;\n}\n\n#pdl-setting-filename .pdl-options:hover {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n#pdl-setting-filename .pdl-unavailable {\n pointer-events: none !important;\n opacity: 0.5 !important;\n cursor: not-allowed !important;\n}\n\n/* ugoria */\n#pdl-setting-ugoria p.option-header {\n font-weight: 700;\n}\n\n#pdl-setting-ugoria #pdl-ugoria-format-wrap {\n display: flex;\n justify-content: space-between;\n flex-wrap: nowrap;\n margin: 1.5em 1em;\n}\n\n#pdl-ugoria-format-wrap .pdl-ugoria-format-item,\n#pdl-ugoria-format-wrap .pdl-ugoria-format-item label,\n#pdl-ugoria-format-wrap .pdl-ugoria-format-item input {\n cursor: pointer;\n}\n\n#pdl-ugoria-format-wrap .pdl-ugoria-format-item label {\n padding-left: 4px;\n}\n\n/* history */\n#pdl-setting-history div {\n text-align: center;\n margin: 1em 0;\n}\n\n#pdl-setting-history .btn-history {\n width: 80%;\n}\n\n/* others setting */\n#pdl-setting-others .pdl-options {\n display: flex;\n align-items: center;\n gap: 20px;\n cursor: pointer;\n transition: 0.2s background-color;\n padding: 1em 0.5em;\n border-radius: 4px;\n}\n\n#pdl-setting-others .pdl-options:hover {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n#pdl-setting-others .pdl-options.sub-option {\n padding: 0.5em;\n padding-inline-start: 2em;\n}\n\n/* donate */\n#pdl-setting-donate {\n text-align: center;\n}\n\n#pdl-setting-donate p {\n margin: 0.5em 0;\n}\n\n/* upgrade msg */\n.pdl-changelog h4 {\n margin: 1em 0;\n font-weight: bold !important;\n}\n\n.pdl-changelog ul {\n padding-inline-start: 40px !important;\n}\n\n.pdl-changelog li {\n line-height: 2;\n list-style-type: disc !important;\n}\n\n.pdl-changelog p {\n margin: 0.5em 0;\n}\n\n/* adjust button position */\n.pdl-adjust-button {\n display: flex;\n justify-content: space-between;\n gap: 32px;\n}\n\n.pdl-adjust-content {\n flex: 1;\n}\n\n.pdl-adjust-content .pdl-adjust-item {\n margin-bottom: 1em;\n}\n\n.pdl-adjust-content .pdl-adjust-item .pdl-adjust-title {\n font-weight: 700;\n margin-bottom: 0.8em;\n}\n\n.pdl-adjust-content .pdl-adjust-item .pdl-adjust-select {\n display: flex;\n align-items: center;\n margin: 0.6em 0;\n padding: 0 0.4em;\n gap: 20px;\n}\n\n.pdl-adjust-select input[type='range'] {\n flex: 1;\n}\n\n.pdl-adjust-preview {\n align-self: center;\n}\n\n.pdl-adjust-preview .pdl-thumbnail-sample {\n position: relative;\n width: 184px;\n height: 184px;\n background-color: rgba(0, 150, 250, 0.15);\n border-radius: 4px;\n}\n";
  41. n(css$3,{});
  42.  
  43. var css$2 = ".pdl-dlbar-status_bar {\n flex-grow: 1;\n height: 46px;\n line-height: 46px;\n padding-right: 8px;\n text-align: right;\n font-weight: bold;\n font-size: 16px;\n color: rgb(133, 133, 133);\n cursor: default;\n white-space: nowrap;\n}\n\n.pdl-btn-all.pdl-btn-all,\n.pdl-stop.pdl-stop {\n background-color: transparent;\n border: none;\n padding: 0 10px;\n}\n\n.pdl-btn-all.pdl-btn-all:hover,\n.pdl-stop.pdl-stop:hover {\n color: rgb(31, 31, 31);\n}\n\n.pdl-btn-all::before,\n.pdl-stop::before {\n content: '';\n height: 24px;\n width: 24px;\n transition: background-image 0.2s ease 0s;\n background: no-repeat center/85%;\n}\n\n.pdl-btn-all::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n.pdl-stop::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n.pdl-btn-all:hover::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n.pdl-stop:hover::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n.pdl-hide {\n display: none !important;\n}\n\n.pdl-filter-wrap {\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n margin: 4px 0;\n font-weight: bold;\n font-size: 14px;\n line-height: 14px;\n color: rgb(133, 133, 133);\n transition: color 0.2s ease 0s;\n}\n\n.pdl-filter-wrap.unavailable {\n pointer-events: none !important;\n opacity: 0.5 !important;\n}\n\n.pdl-filter-wrap .pdl-filter:hover {\n color: rgb(31, 31, 31);\n}\n\n.pdl-filter-wrap label {\n padding-left: 8px;\n cursor: pointer;\n}\n\n.pdl-checkbox.pdl-checkbox {\n vertical-align: top;\n appearance: none;\n position: relative;\n box-sizing: border-box;\n width: 28px;\n border: 2px solid transparent;\n cursor: pointer;\n border-radius: 14px;\n height: 14px;\n background-color: rgba(133, 133, 133);\n transition: background-color 0.2s ease 0s, box-shadow 0.2s ease 0s;\n}\n\n.pdl-checkbox:hover {\n background-color: rgba(31, 31, 31);\n}\n\n.pdl-checkbox::after {\n content: '';\n position: absolute;\n display: block;\n top: 0px;\n left: 0px;\n width: 10px;\n height: 10px;\n transform: translateX(0px);\n background-color: rgb(255, 255, 255);\n border-radius: 10px;\n transition: transform 0.2s ease 0s;\n}\n\n.pdl-checkbox:checked {\n background-color: rgb(0, 150, 250);\n}\n\n.pdl-checkbox:checked::after {\n transform: translateX(14px);\n}\n\n.pdl-dlbar {\n display: flex;\n flex-grow: 1;\n}\n\n.pdl-dlbar-follow_latest {\n padding: 0 8px;\n}\n";
  44. n(css$2,{});
  45.  
  46. var css$1 = "[data-theme='dark'] .pdl-btn-all::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n[data-theme='dark'] .pdl-btn-main,\n[data-theme='dark'] .pdl-btn-all:hover::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23D6D6D6' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n[data-theme='dark'] .pdl-stop::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n[data-theme='dark'] .pdl-stop:hover::before {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23D6D6D6' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E\");\n}\n\n[data-theme='dark'] .pdl-checkbox:not(:checked):hover {\n background-color: rgba(155, 155, 155);\n}\n\n[data-theme='dark'] .pdl-btn.pdl-tag {\n background-color: rgba(255, 255, 255, 0.4);\n}\n\n[data-theme='dark'] .pdl-btn.pdl-modal-tag {\n background-color: rgba(255, 255, 255, 0.4);\n}\n\n[data-theme='dark'] .pdl-btn.pdl-modal-tag:hover {\n background-color: rgba(255, 255, 255, 0.6);\n}\n\n[data-theme='dark'] .pdl-wrap .pdl-filter:hover,\n[data-theme='dark'] .pdl-stop.pdl-stop:hover,\n[data-theme='dark'] .pdl-btn-all.pdl-btn-all:hover {\n color: rgb(214, 214, 214);\n}\n\n/* modal */\n[data-theme='dark'] .pdl-dialog {\n background-color: rgb(31, 31, 31);\n}\n\n[data-theme='dark'] .pdl-dialog-footer button {\n background-color: rgb(245, 245, 245);\n}\n\n[data-theme='dark'] .pdl-dialog-content hr {\n border-top: 1px solid rgba(255, 255, 255, 0.3);\n}\n\n/* others setting */\n[data-theme='dark'] #pdl-setting-others .pdl-options:hover {\n background-color: rgba(255, 255, 255, 0.1);\n}\n";
  47. n(css$1,{});
  48.  
  49. const regexp = {
  50. preloadData: /"meta-preload-data" content='(.*?)'>/,
  51. globalData: /"meta-global-data" content='(.*?)'>/,
  52. artworksPage: /artworks\/(\d+)$/,
  53. userPage: /users\/(\d+)/,
  54. bookmarkPage: /users\/(\d+)\/bookmarks\/artworks/,
  55. userPageTags: /users\/\d+\/(artworks|illustrations|manga|bookmarks(?!artworks))/,
  56. ppSearchPage: /\/tags\/.*\/(artworks|illustrations|manga)/,
  57. suscribePage: /bookmark_new_illust/,
  58. activityHref: /illust_id=(\d+)/,
  59. originSrcPageNum: /(?<=_p)\d+/,
  60. followLatest: /\/bookmark_new_illust(?:_r18)?\.php/
  61. };
  62. const depsUrls = {
  63. gifWorker: 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js',
  64. pako: 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.4/pako.min.js',
  65. upng: 'https://cdnjs.cloudflare.com/ajax/libs/upng-js/2.1.0/UPNG.min.js'
  66. };
  67. const creditCode = `<img style="display: block; margin: 1em auto; width: 200px"
  68. src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAADzWSURBVHhe7Z0HnBRF9sdrMyxZoiCegKCCOZ3xzKcCgp56CoqKohj+6pmznBGMGM+cQDALBgwneuZ4nieGU5KKIoqAIrCEDf1/3zddQ29vd09P2oD++BQzW93TXV316uWqLvhZUCowv8P8+OOP5uOPPzY9evQwvXr1MoWFhe6RYDiOY2bPnm2+/vprs+WWW5p27dq5R37bWCEwNTU1K6WDfvNYvny5M2LECKd58+bOjjvu6Hz77bfukXB88803zuabb+60aNHCueqqq5xVq1a5R+JB+t79tmZBnms5U7IqQWdrHhYvXmymTZvm/hWNlStXmpkzZxohMOVCcK9UKCkpMT/88INZtmyZqa6uNgUFBe6RaHzxxRdm6dKl7l9rJKqieX0Txi+//GKGDh1qhKOYs846y1RVRc+fsrIy0759eyWOyspK2Ll7JBxrr722ef31182ECRPMyJEjTXFxsXskHOeee67p27ev+b//+79Y92iqWGMJCy4yb9481YNeeOGFlBwINbNbt26qV8G94g567969lYDXWmsttyYatIM2iWqLGuLWrnlYYwmrdevWyoHATz/9ZL777jv9HgYIqmPHjqaoqEg5FmIUAsg1xo4dax588EFz/fXXm/Lycrd2zUOTIiy4CDqNKMluTThatmxpNt54YxVPiMXvv/8+JaF07dpVCYvrw1nywVHatm1rDjvsMLU64+hk6G60Jc4zNyY0GcJCPI0ePdoMGjTIiAWmXCUKDNr+++9vNthgA7PeeuupmEs1kB06dFDCgqDgcg0tqrj/zTffbAYPHmzGjRuXUk9sVJDGL5OZ3OghHMfZZpttYDmO6DXOl19+6R4JhwyEI7PdmTNnjn5PhX/9619Oq1at9B4nn3yyuiDiQvrR/ZY7VFRUOMJFtT177723s2TJEvdI44b0xZImw7FQjvv166cc5auvvjJTp05VMREFzu3UqZPp3r27fk8FxJQ9LxXH4t633Xab2XrrrVVfSsVBM0GzZs3MlVdeaf70pz+Z4cOHm+bNm7tHmgCaCscC77//vtOmTRudwZtuuqkjg+8eiQYz/7PPPnNk8N2aYMyePdvp3LmzXn+PPfZwfv31V/dIXcyfP9/Zbrvt9Ny+ffsqV4xC3DYEAU4lhOz+1fjRpDiWtNdstdVWZt9999W/P/30UzNlyhT9ngrXXHON2XHHHdWHFAUh2mRYZtGiRZEcCwcnliOAy6XyYV133XXahlGjRrk18dGiRYuU4aVGh8bAscTaiz0jZ8yYoSEUabrqWmLxuUfCIaJEzxcCcObNm+fW1gXtsHrcH/7wh0iO+MUXXzg9evTQc//4xz86QojukbrgulZXEmPCrQ0HoaFrr73WkYngiBUcW3+jD9PRC/OFRsGx3nvvPXPUUUeZ++67L5aesv7665tjjjlGvwuRmXvuuUe/R0EGXj/Ri15++WX9HgS87zvttJN+h3NFcSFcHzhhgYjPSP1HjAJ1kwARsfoZhaefflq565gxY8xTTz0Vyy1BKOqGG25QVwb6Z4OjoTnWgQce6EjHafD3zTffdGuj8c477ziiaMfiFkBEpiNEo+eLEhzKHeEMIt6cCy+8UC3EKC767rvvOq1bt9ZrCqG7tcE47rjj9DzKs88+69YGg/tvttlmei5t5j5xMHPmTGejjTbSvtxnn30y0uVyhUbBsbB40FGYccOGDdMwTCrgm8IaYyb/73//M//+97/dI8HgfOl09XSjR4VxAOrx2F922WVm1113jeQUePJFIdfv1sMfBvxjOGy32GILs+2227q1wXj44YfNl19+qd+HDBlixEjR76lAG0jd6dmzp+qhcazgvKKhORbWkrDv5Iw++OCDI60xi0cffVR1LazEl156ya0NBj6s//znP8ot0tFZoiDErHqTEIxytygsXLjQefHFF9UqTKVL3nXXXcq9hRidV155Ja22LliwQP176HQNCThWAYQlM7NBg1b//e9/zX777adcoFWrVkaIxuy99956LIxr4IV+5plnVNfBw17fPh70tY8++kitVThFrjgEz/PEE08ohxswYECsjInGBumTpQ3OsZiRzOKLLrrIKSkpUa5Fot3PP//snpFbYOkxo9PhBL8jPcCxGlzHgiPhozn99NONKPJah2ed2GBcEGT+8MMPk7E0eTb99IPcKeJuf//732OnxdQnPvnkkzUnAbChOZYX0qnO3/72N+eBBx5QX04criImv3PEEUeolYjfB+4X9jtSj+WRndLSUmfatGlubV288MILzvHHH+98+umnbk1d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"
  69. />`;
  70.  
  71. const langZh = {
  72. downloadBar: {
  73. button: {
  74. stop: '停止',
  75. works: '作品',
  76. bookmarks: '收藏',
  77. bookmarks_public: '公开',
  78. bookmarks_private: '不公开',
  79. all_one_page: '全部(单页)',
  80. all: '全部(批量)',
  81. r18_one_page: 'R-18(单页)',
  82. r18: 'R-18(批量)'
  83. },
  84. filter: {
  85. exclude_downloaded: '排除已下载图片',
  86. illusts: '插画',
  87. manga: '漫画',
  88. ugoria: '动图'
  89. }
  90. },
  91. modals: {
  92. upgradeMsg: {
  93. feedback: '有问题or想建议?这里反馈'
  94. },
  95. setting: {
  96. filename: {
  97. tab_title: '文件名',
  98. input: {
  99. folder_label: '文件夹名:',
  100. folder_placeholder: '我不想保存到子文件夹',
  101. folder_placeholder_vm: 'Violentmonkey不支持',
  102. folder_placeholder_need_api: '需要Browser Api',
  103. filename_label: '文件名:',
  104. filename_placeholder: '你的名字?',
  105. tag_label: '标签语言:',
  106. fsa_label: '使用FileSystemAccess API',
  107. fsa_placeholder: '根文件夹名',
  108. filename_conflict_label: '文件名重复时:',
  109. filename_conflict_option_uniquify: '重命名',
  110. filename_conflict_option_overwrite: '覆盖',
  111. filename_conflict_option_prompt: '提示'
  112. },
  113. tips: {
  114. filename_pattern: '{artist}:作者, {artistID}:作者ID, {title}:作品标题, {id}:作品ID, {page}:页码, {tags}:作品标签,{date} / {date(占位符)}: 创建时间',
  115. empty_folder: '如果不想保存到画师目录,文件夹名留空即可。',
  116. tag_translation: '请注意:标签翻译不一定是你选择的语言,部分<a href="https://crowdin.com/project/pixiv-tags" target="_blank">无对应语言翻译的标签</a>仍可能是其他语言。'
  117. },
  118. button: {
  119. fsa_change_dir: '更改'
  120. }
  121. },
  122. ugoria: {
  123. tab_title: '动图',
  124. format_label: '动图格式:'
  125. },
  126. history: {
  127. tab_title: '历史记录',
  128. button: {
  129. import: '导入记录(替换)',
  130. merge: '导入记录(合并)',
  131. export: '导出记录',
  132. clear: '清除记录'
  133. },
  134. tips: {
  135. clear: '真的要清除历史记录吗?'
  136. }
  137. },
  138. button_pos: {
  139. tab_title: '按钮',
  140. self_bookmark_title: '预览图(我的收藏)',
  141. preview_title: '预览图',
  142. input: {
  143. horizon_label: '水平:',
  144. vertical_label: '垂直:'
  145. }
  146. },
  147. others: {
  148. tab_title: '其它',
  149. input: {
  150. bundle_illusts: '将多页插图打包为.zip压缩包',
  151. bundle_manga: '将多页漫画作品打包为.zip压缩包',
  152. add_bookmark: '下载单个作品时收藏作品',
  153. add_bookmark_with_tags: '收藏时添加作品标签',
  154. add_bookmark_private_r18: '将R-18作品收藏到不公开类别',
  155. show_popup_button: '显示设置按钮'
  156. }
  157. },
  158. feedback: {
  159. tab_title: '反馈 / 赞赏',
  160. tips: {
  161. feedback: '有问题or想建议?这里反馈'
  162. }
  163. }
  164. }
  165. },
  166. gm_menu: {
  167. setting: '设置'
  168. }
  169. };
  170. const langEn = {
  171. downloadBar: {
  172. button: {
  173. stop: 'Stop',
  174. works: 'Works',
  175. bookmarks: 'Bookmarks',
  176. bookmarks_public: 'Public',
  177. bookmarks_private: 'Private',
  178. all_one_page: 'All (one page)',
  179. all: 'All',
  180. r18_one_page: 'R-18 (one page)',
  181. r18: 'R-18'
  182. },
  183. filter: {
  184. exclude_downloaded: 'Exclude downloaded',
  185. illusts: 'Illustrations',
  186. manga: 'Manga',
  187. ugoria: 'Ugoria'
  188. }
  189. },
  190. modals: {
  191. upgradeMsg: {
  192. feedback: 'Feedback'
  193. },
  194. setting: {
  195. filename: {
  196. tab_title: 'Filename',
  197. input: {
  198. folder_label: 'FileName:',
  199. folder_placeholder: "I don't need subfolder",
  200. folder_placeholder_vm: "VM doesn't support",
  201. folder_placeholder_need_api: 'Need Browser Api',
  202. filename_label: 'FileName:',
  203. filename_placeholder: 'Your Name?',
  204. tag_label: 'Tags language:',
  205. fsa_label: 'FileSystemAccess API',
  206. fsa_placeholder: 'Root directory',
  207. filename_conflict_label: 'Conflict Action: ',
  208. filename_conflict_option_uniquify: 'Uniquify',
  209. filename_conflict_option_overwrite: 'Overwrite',
  210. filename_conflict_option_prompt: 'Prompt'
  211. },
  212. tips: {
  213. filename_pattern: '{artist}, {artistID}, {title}, {id}, {page}, {tags}, {date} / {date(format)}',
  214. empty_folder: "If you don't need a subfolder, just leave the folder name blank",
  215. tag_translation: 'Note: Tags language may not be the language you selected, <a href="https://crowdin.com/project/pixiv-tags" target="_blank">some tags without translations</a> may still be in other languages.'
  216. },
  217. button: {
  218. fsa_change_dir: 'Change'
  219. }
  220. },
  221. ugoria: {
  222. tab_title: 'Ugoria',
  223. format_label: 'Ugoria Format:'
  224. },
  225. history: {
  226. tab_title: 'History',
  227. button: {
  228. import: 'Import (Replace)',
  229. merge: 'Import (Merge)',
  230. export: 'Export',
  231. clear: 'Clear'
  232. },
  233. tips: {
  234. clear: 'Do you really want to clear history?'
  235. }
  236. },
  237. button_pos: {
  238. tab_title: 'Button',
  239. self_bookmark_title: 'Thumbnail(My bookmarks)',
  240. preview_title: 'Thumbnail',
  241. input: {
  242. horizon_label: 'X:',
  243. vertical_label: 'Y:'
  244. }
  245. },
  246. others: {
  247. tab_title: 'Others',
  248. input: {
  249. bundle_illusts: 'Pack multi-page illustrations into a .zip archive',
  250. bundle_manga: 'Pack manga into a .zip archive',
  251. add_bookmark: 'Bookmark work when downloading a single work',
  252. add_bookmark_with_tags: 'Add works tags',
  253. add_bookmark_private_r18: 'Bookmark R-18 works to private category',
  254. show_popup_button: 'Show setting button'
  255. }
  256. },
  257. feedback: {
  258. tab_title: 'Feedback',
  259. tips: {
  260. feedback: 'Feedback'
  261. }
  262. }
  263. }
  264. },
  265. gm_menu: {
  266. setting: '设置'
  267. }
  268. };
  269. const messages = {
  270. 'zh-cn': langZh,
  271. 'zh-tw': langZh,
  272. zh: langZh,
  273. en: langEn
  274. };
  275. const curLang = document.documentElement.getAttribute('lang')?.toLowerCase() || 'en';
  276. const defaultLang = 'en';
  277. function t(key) {
  278. const lang = (curLang in messages ? curLang : defaultLang);
  279. const paths = key.split('.');
  280. let last = messages[lang];
  281. for (let i = 0; i < paths.length; i++) {
  282. const value = last[paths[i]];
  283. if (value === undefined || value === null)
  284. return null;
  285. last = value;
  286. }
  287. return last;
  288. }
  289. unsafeWindow.t = t;
  290.  
  291. function createHistory() {
  292. let records = (function getHistory() {
  293. const storage = localStorage.pixivDownloader || '[]';
  294. return new Set(JSON.parse(storage));
  295. })();
  296. function readHistoryFile(file, cb) {
  297. if (file.type === 'text/plain') {
  298. const reader = new FileReader();
  299. reader.readAsText(file);
  300. reader.onload = (readEvt) => {
  301. const text = readEvt.target?.result;
  302. try {
  303. if (typeof text !== 'string')
  304. throw new Error('Invalid file');
  305. const history = JSON.parse(text);
  306. if (!(history instanceof Array))
  307. throw new Error('Invalid file');
  308. cb(history);
  309. location.reload();
  310. }
  311. catch (error) {
  312. alert(error.message);
  313. }
  314. };
  315. }
  316. else {
  317. alert('Invalid file');
  318. }
  319. }
  320. return {
  321. add(pixivId) {
  322. if (records.has(pixivId))
  323. return;
  324. records.add(pixivId);
  325. localStorage.setItem(`pdlTemp-${pixivId}`, '');
  326. },
  327. has(pixivId) {
  328. return records.has(pixivId);
  329. },
  330. getAll() {
  331. return [...records];
  332. },
  333. updateHistory() {
  334. const validKeys = Object.keys(localStorage).filter((key) => /(?<=^pdlTemp-)\d+$/.test(key));
  335. if (!validKeys.length)
  336. return;
  337. validKeys.forEach((key) => {
  338. const [id] = /(?<=^pdlTemp-)\d+$/.exec(key);
  339. records.add(id);
  340. localStorage.removeItem(key);
  341. });
  342. this.saveHistory();
  343. },
  344. saveHistory(historyArr) {
  345. if (historyArr) {
  346. if (historyArr.length && !historyArr.every((id) => typeof id === 'string')) {
  347. throw new Error('Invalid id type');
  348. }
  349. this.updateHistory();
  350. localStorage.pixivDownloader = JSON.stringify(historyArr);
  351. }
  352. else {
  353. localStorage.pixivDownloader = JSON.stringify([...records]);
  354. }
  355. },
  356. clearHistory() {
  357. const isConfirm = confirm(t('modals.setting.history.tips.clear'));
  358. if (!isConfirm)
  359. return;
  360. this.updateHistory();
  361. records = new Set();
  362. localStorage.pixivDownloader = '[]';
  363. location.reload();
  364. },
  365. replace(file) {
  366. readHistoryFile(file, this.saveHistory.bind(this));
  367. },
  368. merge(file) {
  369. readHistoryFile(file, (historyArr) => {
  370. if (!historyArr.length)
  371. throw new Error('No id found');
  372. if (!historyArr.every((id) => typeof id === 'string')) {
  373. throw new Error('Invalid id type');
  374. }
  375. historyArr.forEach((id) => records.add(id));
  376. this.saveHistory();
  377. });
  378. }
  379. };
  380. }
  381. const pixivHistory = createHistory();
  382.  
  383. function getSelfId() {
  384. return document.querySelector('#qualtrics_user-id')?.textContent ?? '';
  385. }
  386.  
  387. function getIllustId(node) {
  388. const isLinkToArtworksPage = regexp.artworksPage.exec(node.href);
  389. if (isLinkToArtworksPage) {
  390. if (node.getAttribute('data-gtm-value') ||
  391. node.classList.contains('gtm-illust-recommend-node-node') ||
  392. node.classList.contains('gtm-discover-user-recommend-node') ||
  393. node.classList.contains('work')) {
  394. return isLinkToArtworksPage[1];
  395. }
  396. }
  397. else {
  398. const isActivityThumb = regexp.activityHref.exec(node.href);
  399. if (isActivityThumb && node.classList.contains('work')) {
  400. return isActivityThumb[1];
  401. }
  402. }
  403. return '';
  404. }
  405.  
  406. function getLogger() {
  407. const methods = ['info', 'warn', 'error'];
  408. const style = ['color: green;', 'color: orange;', 'color: red;'];
  409. const logLevel = 2 ;
  410. const namePrefix = '[Pixiv Downlaoder] ';
  411. function log(level, args) {
  412. if (logLevel <= level)
  413. console[methods[level]]('%c[Pixiv Downloader]', style[level], ...args);
  414. }
  415. return {
  416. info(...args) {
  417. log(0 , args);
  418. },
  419. warn(...args) {
  420. log(1 , args);
  421. },
  422. error(...args) {
  423. log(2 , args);
  424. },
  425. time(label) {
  426. console.time(namePrefix + label);
  427. },
  428. timeLog(label) {
  429. console.timeLog(namePrefix + label);
  430. },
  431. timeEnd(label) {
  432. console.timeEnd(namePrefix + label);
  433. }
  434. };
  435. }
  436. const logger = getLogger();
  437.  
  438. var IllustType;
  439. (function (IllustType) {
  440. IllustType[IllustType["illusts"] = 0] = "illusts";
  441. IllustType[IllustType["manga"] = 1] = "manga";
  442. IllustType[IllustType["ugoira"] = 2] = "ugoira";
  443. })(IllustType || (IllustType = {}));
  444. var BookmarkRestrict;
  445. (function (BookmarkRestrict) {
  446. BookmarkRestrict[BookmarkRestrict["public"] = 0] = "public";
  447. BookmarkRestrict[BookmarkRestrict["private"] = 1] = "private";
  448. })(BookmarkRestrict || (BookmarkRestrict = {}));
  449.  
  450. function sleep(delay) {
  451. return new Promise((resolve) => {
  452. setTimeout(resolve, delay);
  453. });
  454. }
  455. function wakeableSleep(delay) {
  456. let wake = () => void {};
  457. const sleep = new Promise((r) => {
  458. setTimeout(r, delay);
  459. wake = r;
  460. });
  461. return {
  462. wake,
  463. sleep
  464. };
  465. }
  466. function replaceInvalidChar(str) {
  467. if (typeof str !== 'string')
  468. throw new TypeError('expect string but got ' + typeof str);
  469. if (!str)
  470. return '';
  471. return str
  472. .replace(/\p{C}/gu, '')
  473. .replace(/\\/g, '\')
  474. .replace(/\//g, '/')
  475. .replace(/:/g, ':')
  476. .replace(/\*/g, '*')
  477. .replace(/\?/g, '?')
  478. .replace(/\|/g, '|')
  479. .replace(/"/g, '"')
  480. .replace(/</g, '﹤')
  481. .replace(/>/g, '﹥')
  482. .trim()
  483. .replace(/^\.|\.$/g, '.');
  484. }
  485. function unescapeHtml(str) {
  486. if (typeof str !== 'string')
  487. throw new TypeError('expect string but got ' + typeof str);
  488. if (!str)
  489. return '';
  490. const el = document.createElement('p');
  491. el.innerHTML = str;
  492. return el.innerText;
  493. }
  494. function stringToFragment(string) {
  495. const renderer = document.createElement('template');
  496. renderer.innerHTML = string;
  497. return renderer.content;
  498. }
  499.  
  500. function loadConfig() {
  501. const defaultConfig = Object.freeze({
  502. version: "0.9.2",
  503. ugoriaFormat: 'zip',
  504. folderPattern: 'pixiv/{artist}',
  505. filenamePattern: '{artist}_{title}_{id}_p{page}',
  506. tagLang: 'ja',
  507. showMsg: true,
  508. filterExcludeDownloaded: false,
  509. filterIllusts: true,
  510. filterManga: true,
  511. filterUgoria: true,
  512. bundleIllusts: false,
  513. bundleManga: false,
  514. addBookmark: false,
  515. addBookmarkWithTags: false,
  516. privateR18: false,
  517. useFileSystemAccess: false,
  518. fileSystemFilenameConflictAction: 'uniquify',
  519. showPopupButton: true
  520. });
  521. const config = (() => {
  522. if (!localStorage.pdlSetting)
  523. return {};
  524. let config;
  525. try {
  526. config = JSON.parse(localStorage.pdlSetting);
  527. }
  528. catch (error) {
  529. console.log(error);
  530. return {};
  531. }
  532. if (config.version !== defaultConfig.version) {
  533. config.version = defaultConfig.version;
  534. config.showMsg = true;
  535. }
  536. return config;
  537. })();
  538. return {
  539. get(key) {
  540. return config[key] ?? defaultConfig[key];
  541. },
  542. set(key, value) {
  543. if (config[key] !== value) {
  544. config[key] = value;
  545. localStorage.pdlSetting = JSON.stringify(config);
  546. logger.info('Config set:', key, value);
  547. }
  548. }
  549. };
  550. }
  551. const config = loadConfig();
  552.  
  553. const handleWorker = `
  554. let webpApi = {};
  555. Module.onRuntimeInitialized = () => {
  556. webpApi = {
  557. init: Module.cwrap('init', '', ['number', 'number', 'number']),
  558. createBuffer: Module.cwrap('createBuffer', 'number', ['number']),
  559. addFrame: Module.cwrap('addFrame', 'number', ['number', 'number', 'number']),
  560. generate: Module.cwrap('generate', 'number', []),
  561. freeResult: Module.cwrap('freeResult', '', []),
  562. getResultPointer: Module.cwrap('getResultPointer', 'number', []),
  563. getResultSize: Module.cwrap('getResultSize', 'number', []),
  564. };
  565.  
  566. postMessage('ok');
  567. };
  568.  
  569. onmessage = (evt) => {
  570. const { data, delays, lossless = 1, quality = 75, method = 4} = evt.data;
  571.  
  572. webpApi.init(lossless, quality, method);
  573. data.forEach((u8a, idx) => {
  574. const pointer = webpApi.createBuffer(u8a.length);
  575. Module.HEAPU8.set(u8a, pointer);
  576. webpApi.addFrame(pointer, u8a.length, delays[idx]);
  577. postMessage(idx);
  578. });
  579.  
  580. webpApi.generate();
  581. const resultPointer = webpApi.getResultPointer();
  582. const resultSize = webpApi.getResultSize();
  583. const result = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
  584. postMessage(result);
  585. webpApi.freeResult();
  586. };`;
  587.  
  588. class RequestError extends Error {
  589. response;
  590. constructor(message, response) {
  591. super(message);
  592. this.name = 'RequestError';
  593. this.response = response;
  594. }
  595. }
  596. class CancelError extends Error {
  597. constructor() {
  598. super('User aborted');
  599. this.name = 'CancelError';
  600. }
  601. }
  602. class JsonDataError extends Error {
  603. constructor(msg) {
  604. super(msg);
  605. this.name = 'JsonDataError';
  606. }
  607. }
  608.  
  609. function createService() {
  610. async function _requestJson(url, init) {
  611. logger.info('fetch url:', url);
  612. const res = await fetch(url, init);
  613. if (!res.ok)
  614. throw new RequestError('Request ' + url + ' failed with status code ' + res.status, res);
  615. const data = await res.json();
  616. if (data.error)
  617. throw new JsonDataError(data.message);
  618. return data.body;
  619. }
  620. async function _getDeps(url) {
  621. return fetch(url).then((res) => {
  622. if (res.ok)
  623. return res.text();
  624. throw new RequestError(`Fetch dependency ${url} failed with status code ${res.status}`, res);
  625. });
  626. }
  627. return {
  628. async getJson(url) {
  629. let json;
  630. let retry = 0;
  631. const MAX_RETRY = 3;
  632. do {
  633. try {
  634. json = await _requestJson(url);
  635. }
  636. catch (error) {
  637. logger.error(error);
  638. retry++;
  639. if (retry === MAX_RETRY)
  640. throw error;
  641. sleep(3000);
  642. }
  643. } while (!json);
  644. return json;
  645. },
  646. async getArtworkHtml(illustId) {
  647. logger.info('Fetch illust:', illustId);
  648. let params = '';
  649. const tagLang = config.get('tagLang');
  650. if (tagLang !== 'ja')
  651. params = '?lang=' + tagLang;
  652. const res = await fetch('https://www.pixiv.net/artworks/' + illustId + params);
  653. if (!res.ok)
  654. throw new RequestError('Request failed with status code ' + res.status, res);
  655. return await res.text();
  656. },
  657. async getGifWS() {
  658. let gifWS;
  659. if (!(gifWS = await GM_getValue('gifWS'))) {
  660. gifWS = await _getDeps(depsUrls.gifWorker);
  661. GM_setValue('gifWS', gifWS);
  662. }
  663. return gifWS;
  664. },
  665. async getApngWS() {
  666. let apngWS;
  667. if (!(apngWS = await GM_getValue('apngWS'))) {
  668. const [pako, upng] = await Promise.all([_getDeps(depsUrls.pako), _getDeps(depsUrls.upng)]);
  669. const upngScript = upng.replace('window.UPNG', 'UPNG').replace('window.pako', 'pako');
  670. const workerEvt = `onmessage = (evt) => {
  671. const {data, width, height, delay } = evt.data;
  672. const png = UPNG.encode(data, width, height, 0, delay, {loop: 0});
  673. if (!png) console.error('Convert Apng failed.');
  674. postMessage(png);
  675. };`;
  676. apngWS = workerEvt + pako + upngScript;
  677. GM_setValue('apngWS', apngWS);
  678. }
  679. return apngWS;
  680. },
  681. getWebpWS() {
  682. return workerChunk__default["default"] + handleWorker;
  683. },
  684. addBookmark(illustId, token, tags = [], restrict = BookmarkRestrict.public) {
  685. return _requestJson('/ajax/illusts/bookmarks/add', {
  686. method: 'POST',
  687. headers: {
  688. accept: 'application/json',
  689. 'content-type': 'application/json; charset=utf-8',
  690. 'x-csrf-token': token
  691. },
  692. body: JSON.stringify({
  693. illust_id: illustId,
  694. restrict,
  695. comment: '',
  696. tags
  697. })
  698. });
  699. },
  700. getFollowLatestWorks(page, mode = 'all') {
  701. return _requestJson(`/ajax/follow_latest/illust?p=${page}&mode=${mode}&lang=jp`);
  702. },
  703. getUserAllProfile(userId) {
  704. return _requestJson('/ajax/user/' + userId + '/profile/all');
  705. },
  706. getUgoriaMeta(illustId) {
  707. return _requestJson('/ajax/illust/' + illustId + '/ugoira_meta');
  708. },
  709. getArtworkDetail(illustId) {
  710. let params = '';
  711. const tagLang = config.get('tagLang');
  712. if (tagLang !== 'ja')
  713. params = '?lang=' + tagLang;
  714. return _requestJson('/ajax/illust/' + illustId + params);
  715. }
  716. };
  717. }
  718. const api = createService();
  719.  
  720. function addBookmark(pdlBtn, illustId, token, tags) {
  721. if (!config.get('addBookmark'))
  722. return;
  723. api
  724. .addBookmark(illustId, token, config.get('addBookmarkWithTags') ? tags : [], config.get('privateR18') && tags.includes('R-18') ? BookmarkRestrict.private : BookmarkRestrict.public)
  725. .then(() => {
  726. const bookmarkBtnRef = findBookmarkBtn(pdlBtn);
  727. if (!bookmarkBtnRef)
  728. return;
  729. switch (bookmarkBtnRef.kind) {
  730. case "main" : {
  731. const pathBorder = bookmarkBtnRef.button.querySelector('svg g path');
  732. pathBorder && (pathBorder.style.color = 'rgb(255, 64, 96)');
  733. break;
  734. }
  735. case "sub" : {
  736. const pathBorder = bookmarkBtnRef.button.querySelector('path');
  737. pathBorder && (pathBorder.style.color = 'rgb(255, 64, 96)');
  738. break;
  739. }
  740. case "rank" : {
  741. bookmarkBtnRef.button.style.backgroundColor = 'rgb(255, 64, 96)';
  742. break;
  743. }
  744. }
  745. })
  746. .catch((reason) => {
  747. logger.error(reason.message);
  748. });
  749. }
  750. function findBookmarkBtn(pdlBtn) {
  751. const bookmarkBtnRef = {};
  752. if (pdlBtn.classList.contains('pdl-btn-sub')) {
  753. const btn = pdlBtn.parentElement?.nextElementSibling?.querySelector('button[type="button"]');
  754. if (btn) {
  755. bookmarkBtnRef.kind = "sub" ;
  756. bookmarkBtnRef.button = btn;
  757. }
  758. else {
  759. const btn = pdlBtn.parentElement?.querySelector('div._one-click-bookmark');
  760. if (btn) {
  761. bookmarkBtnRef.kind = "rank" ;
  762. bookmarkBtnRef.button = btn;
  763. }
  764. }
  765. }
  766. else if (pdlBtn.classList.contains('pdl-btn-main')) {
  767. const btn = pdlBtn.parentElement?.parentElement?.querySelector('button.gtm-main-bookmark');
  768. if (btn) {
  769. bookmarkBtnRef.kind = "main" ;
  770. bookmarkBtnRef.button = btn;
  771. }
  772. }
  773. else {
  774. return logger.error(new Error('Can not find bookmark button.'));
  775. }
  776. return bookmarkBtnRef;
  777. }
  778.  
  779. const env = {
  780. isFirefox() {
  781. return navigator.userAgent.includes('Firefox');
  782. },
  783. isViolentmonkey() {
  784. return GM_info.scriptHandler === 'Violentmonkey';
  785. },
  786. isTampermonkey() {
  787. return GM_info.scriptHandler === 'Tampermonkey';
  788. },
  789. isBlobDlAvaliable() {
  790. return !this.isFirefox() || (this.isFirefox() && this.isTampermonkey() && parseFloat(GM_info.version ?? '') < 4.18);
  791. },
  792. isSupportSubpath() {
  793. return this.isBrowserDownloadMode();
  794. },
  795. isBrowserDownloadMode() {
  796. return GM_info.downloadMode === 'browser';
  797. },
  798. isConflictActionEnable() {
  799. return this.isTampermonkey() && parseFloat(GM_info.version ?? '') >= 4.18 && this.isBrowserDownloadMode();
  800. },
  801. isConflictActionPromptEnable() {
  802. return !this.isFirefox() && this.isConflictActionEnable();
  803. },
  804. isFileSystemAccessAvaliable() {
  805. return (typeof unsafeWindow.showDirectoryPicker === 'function' && typeof unsafeWindow.showSaveFilePicker === 'function');
  806. }
  807. };
  808.  
  809. function createCompressor() {
  810. const zip = new JSZip__default["default"]();
  811. return {
  812. add(id, name, data) {
  813. zip.folder(id)?.file(name, data);
  814. },
  815. bundle(id) {
  816. const folder = zip.folder(id);
  817. if (!folder)
  818. throw new TypeError('no such folder:' + id);
  819. return folder.generateAsync({ type: 'blob' });
  820. },
  821. remove(ids) {
  822. if (typeof ids === 'string') {
  823. zip.remove(ids);
  824. }
  825. else {
  826. const dirs = zip.filter((_, file) => file.dir).map((dir) => dir.name);
  827. const dirsToDel = ids.filter((id) => dirs.some((dir) => dir.includes(id)));
  828. dirsToDel.forEach((dir) => zip.remove(dir));
  829. logger.info('Compressor: Remove', zip);
  830. }
  831. },
  832. fileCount(id) {
  833. let count = 0;
  834. zip.folder(id)?.forEach(() => count++);
  835. return count;
  836. },
  837. async unzip(data) {
  838. const id = Math.random().toString(36);
  839. let folder = zip.folder(id);
  840. if (!folder)
  841. throw TypeError('Can not get new root folder');
  842. const filesPromises = [];
  843. folder = await folder.loadAsync(data);
  844. folder.forEach((_, file) => {
  845. filesPromises.push(file.async('blob'));
  846. });
  847. const files = await Promise.all(filesPromises);
  848. zip.remove(id);
  849. return files;
  850. }
  851. };
  852. }
  853. const compressor = createCompressor();
  854.  
  855. function createConverter() {
  856. const freeApngWorkers = [];
  857. const freeWebpWorkers = [];
  858. const MAX_CONVERT = 2;
  859. let isStop = false;
  860. let queue = [];
  861. let active = [];
  862. const cachedQueue = {
  863. gif: [],
  864. png: []
  865. };
  866. const depsUrl = {
  867. gif: '',
  868. png: '',
  869. webp: URL.createObjectURL(new Blob([api.getWebpWS()], { type: 'text/javascript' }))
  870. };
  871. let LoadStatus;
  872. (function (LoadStatus) {
  873. LoadStatus[LoadStatus["unloaded"] = 0] = "unloaded";
  874. LoadStatus[LoadStatus["loading"] = 1] = "loading";
  875. LoadStatus[LoadStatus["loaded"] = 2] = "loaded";
  876. })(LoadStatus || (LoadStatus = {}));
  877. const depsStatus = {
  878. gif: {
  879. loaded: LoadStatus.unloaded,
  880. load() {
  881. logger.info('开始加载gif依赖');
  882. this.loaded = LoadStatus.loading;
  883. return api.getGifWS().then((str) => {
  884. depsUrl.gif = URL.createObjectURL(new Blob([str], { type: 'text/javascript' }));
  885. this.loaded = LoadStatus.loaded;
  886. logger.info('加载gif依赖完成');
  887. });
  888. }
  889. },
  890. png: {
  891. loaded: LoadStatus.unloaded,
  892. load() {
  893. logger.info('开始加载png依赖');
  894. this.loaded = LoadStatus.loading;
  895. return api.getApngWS().then((str) => {
  896. depsUrl.png = URL.createObjectURL(new Blob([str], { type: 'text/javascript' }));
  897. this.loaded = LoadStatus.loaded;
  898. logger.info('加载png依赖完成');
  899. });
  900. }
  901. }
  902. };
  903. const convertTo = {
  904. webp: (frames, convertMeta) => {
  905. return new Promise((resolve, reject) => {
  906. let worker;
  907. let reuse = false;
  908. logger.time(convertMeta.id);
  909. if (freeWebpWorkers.length) {
  910. logger.info('Reuse webp workers.');
  911. worker = freeWebpWorkers.shift();
  912. reuse = true;
  913. }
  914. else {
  915. worker = new Worker(depsUrl.webp);
  916. }
  917. convertMeta.abort = () => {
  918. logger.timeEnd(convertMeta.id);
  919. logger.warn('Convert stop manually.' + convertMeta.id);
  920. reject(new CancelError());
  921. convertMeta.isAborted = true;
  922. worker.terminate();
  923. };
  924. const workerLoad = new Promise((onLoaded) => {
  925. if (reuse)
  926. return onLoaded();
  927. worker.onmessage = (evt) => {
  928. if (evt.data === 'ok') {
  929. logger.info('Webp worker loaded.');
  930. onLoaded();
  931. }
  932. };
  933. });
  934. const delays = convertMeta.source.framesInfo.map((frameInfo) => {
  935. return Number(frameInfo.delay);
  936. });
  937. const data = [];
  938. let completed = 0;
  939. frames.forEach((frame, idx) => {
  940. const canvas = document.createElement('canvas');
  941. const width = (canvas.width = frame.naturalWidth);
  942. const height = (canvas.height = frame.naturalHeight);
  943. const context = canvas.getContext('2d');
  944. if (!context)
  945. return;
  946. context.drawImage(frame, 0, 0, width, height);
  947. data.push(new Promise((onFulfilled, onRejected) => {
  948. canvas.toBlob((blob) => {
  949. if (!blob)
  950. return onRejected(new TypeError('Convert failed when invoke canvas.toBlob() ' + idx));
  951. blob.arrayBuffer().then((buffer) => {
  952. const u8a = new Uint8Array(buffer);
  953. onFulfilled(u8a);
  954. convertMeta.onProgress?.((++completed / frames.length) * 0.5, 'webp');
  955. });
  956. }, 'image/webp', 1);
  957. }));
  958. });
  959. workerLoad
  960. .then(() => Promise.all(data))
  961. .then((u8arrs) => {
  962. if (convertMeta.isAborted)
  963. return;
  964. logger.timeLog(convertMeta.id);
  965. worker.onmessage = (evt) => {
  966. const data = evt.data;
  967. if (typeof data !== 'object') {
  968. convertMeta.onProgress?.(0.5 + (evt.data / frames.length) * 0.5, 'webp');
  969. }
  970. else {
  971. logger.timeEnd(convertMeta.id);
  972. freeWebpWorkers.push(worker);
  973. resolve(new Blob([evt.data], { type: 'image/webp' }));
  974. }
  975. };
  976. worker.postMessage({ data: u8arrs, delays });
  977. }, (reason) => {
  978. logger.timeLog(convertMeta.id);
  979. reject(reason);
  980. });
  981. });
  982. },
  983. gif: (frames, convertMeta) => {
  984. return new Promise((resolve, reject) => {
  985. const gif = new GIF__default["default"]({
  986. workers: 2,
  987. quality: 10,
  988. workerScript: depsUrl.gif
  989. });
  990. convertMeta.abort = () => {
  991. gif.abort();
  992. };
  993. logger.info('Start convert:', convertMeta.id);
  994. logger.time(convertMeta.id);
  995. frames.forEach((frame, i) => {
  996. gif.addFrame(frame, {
  997. delay: convertMeta.source.framesInfo[i].delay
  998. });
  999. });
  1000. gif.on('progress', (progress) => {
  1001. if (typeof convertMeta.onProgress === 'function')
  1002. convertMeta.onProgress(progress, 'gif');
  1003. });
  1004. gif.on('finished', (gifBlob) => {
  1005. logger.timeEnd(convertMeta.id);
  1006. resolve(gifBlob);
  1007. });
  1008. gif.on('abort', () => {
  1009. logger.timeEnd(convertMeta.id);
  1010. logger.warn('Convert stop manually. ' + convertMeta.id);
  1011. convertMeta.isAborted = true;
  1012. reject(new CancelError());
  1013. });
  1014. gif.render();
  1015. });
  1016. },
  1017. png: (frames, convertMeta) => {
  1018. return new Promise((resolve, reject) => {
  1019. const canvas = document.createElement('canvas');
  1020. const width = (canvas.width = frames[0].naturalWidth);
  1021. const height = (canvas.height = frames[0].naturalHeight);
  1022. const context = canvas.getContext('2d', { willReadFrequently: true });
  1023. if (!context)
  1024. return reject(new TypeError('Can not get canvas context'));
  1025. const data = [];
  1026. const delay = convertMeta.source.framesInfo.map((frameInfo) => {
  1027. return Number(frameInfo.delay);
  1028. });
  1029. logger.info('Start convert:', convertMeta.id);
  1030. logger.time(convertMeta.id);
  1031. for (const frame of frames) {
  1032. if (convertMeta.isAborted) {
  1033. logger.timeEnd(convertMeta.id);
  1034. logger.warn('Convert stop manually. ' + convertMeta.id);
  1035. return reject(new CancelError());
  1036. }
  1037. context.clearRect(0, 0, width, height);
  1038. context.drawImage(frame, 0, 0, width, height);
  1039. data.push(context.getImageData(0, 0, width, height).data);
  1040. }
  1041. logger.timeLog(convertMeta.id);
  1042. let worker;
  1043. if (freeApngWorkers.length) {
  1044. worker = freeApngWorkers.shift();
  1045. logger.info('Reuse apng workers.');
  1046. }
  1047. else {
  1048. worker = new Worker(depsUrl.png);
  1049. }
  1050. convertMeta.abort = () => {
  1051. logger.timeEnd(convertMeta.id);
  1052. logger.warn('Convert stop manually. ' + convertMeta.id);
  1053. reject(new CancelError());
  1054. convertMeta.isAborted = true;
  1055. worker.terminate();
  1056. };
  1057. worker.onmessage = function (e) {
  1058. freeApngWorkers.push(worker);
  1059. logger.timeEnd(convertMeta.id);
  1060. if (!e.data) {
  1061. return reject(new TypeError('Failed to get png data. ' + convertMeta.id));
  1062. }
  1063. const pngBlob = new Blob([e.data], { type: 'image/png' });
  1064. resolve(pngBlob);
  1065. };
  1066. const cfg = { data, width, height, delay };
  1067. worker.postMessage(cfg);
  1068. });
  1069. },
  1070. webm: (frames, convertMeta) => {
  1071. return new Promise((resolve, reject) => {
  1072. const canvas = document.createElement('canvas');
  1073. const width = (canvas.width = frames[0].naturalWidth);
  1074. const height = (canvas.height = frames[0].naturalHeight);
  1075. const context = canvas.getContext('2d');
  1076. if (!context)
  1077. return reject(new TypeError('Can not get canvas context'));
  1078. const stream = canvas.captureStream();
  1079. const recorder = new MediaRecorder(stream, {
  1080. mimeType: 'video/webm',
  1081. videoBitsPerSecond: 80000000
  1082. });
  1083. const delay = convertMeta.source.framesInfo.map((frame) => {
  1084. return Number(frame.delay);
  1085. });
  1086. const data = [];
  1087. let frame = 0;
  1088. const displayFrame = () => {
  1089. context.clearRect(0, 0, width, height);
  1090. context.drawImage(frames[frame], 0, 0);
  1091. if (convertMeta.isAborted) {
  1092. return recorder.stop();
  1093. }
  1094. setTimeout(() => {
  1095. if (typeof convertMeta.onProgress === 'function')
  1096. convertMeta.onProgress((frame + 1) / frames.length, 'webm');
  1097. if (frame === frames.length - 1) {
  1098. return recorder.stop();
  1099. }
  1100. else {
  1101. frame++;
  1102. }
  1103. displayFrame();
  1104. }, delay[frame]);
  1105. };
  1106. recorder.ondataavailable = (event) => {
  1107. if (event.data && event.data.size) {
  1108. data.push(event.data);
  1109. }
  1110. };
  1111. recorder.onstop = () => {
  1112. if (convertMeta.isAborted) {
  1113. logger.warn('Convert stop manually.' + convertMeta.id);
  1114. return reject(new CancelError());
  1115. }
  1116. resolve(new Blob(data, { type: 'video/webm' }));
  1117. };
  1118. displayFrame();
  1119. recorder.start();
  1120. });
  1121. }
  1122. };
  1123. const convert = (convertMeta) => {
  1124. const { id, source, resolve, reject } = convertMeta;
  1125. let frames;
  1126. active.push(convertMeta);
  1127. if (typeof convertMeta.onProgress === 'function')
  1128. convertMeta.onProgress(0, 'zip');
  1129. compressor
  1130. .unzip(source.data)
  1131. .then((files) => {
  1132. const imagePromises = files.map((blob) => {
  1133. return new Promise((resolve) => {
  1134. const image = new Image();
  1135. image.onload = () => {
  1136. resolve(image);
  1137. };
  1138. image.src = URL.createObjectURL(blob);
  1139. });
  1140. });
  1141. return Promise.all(imagePromises);
  1142. })
  1143. .then((imgEles) => {
  1144. frames = imgEles;
  1145. if (convertMeta.isAborted) {
  1146. logger.warn('Convert stop manually.' + id);
  1147. throw new CancelError();
  1148. }
  1149. return convertTo[source.format](frames, convertMeta);
  1150. })
  1151. .then(resolve, reject)
  1152. .finally(() => {
  1153. frames.forEach((frame) => URL.revokeObjectURL(frame.src));
  1154. active.splice(active.indexOf(convertMeta), 1);
  1155. if (queue.length)
  1156. convert(queue.shift());
  1157. });
  1158. };
  1159. return {
  1160. add: (convertSource, handler) => {
  1161. logger.info('Converter add', convertSource.id);
  1162. return new Promise((resolve, reject) => {
  1163. const meta = {
  1164. id: convertSource.id,
  1165. isAborted: false,
  1166. source: convertSource,
  1167. onProgress: handler?.onProgress,
  1168. resolve,
  1169. reject,
  1170. abort() {
  1171. this.isAborted = true;
  1172. }
  1173. };
  1174. const format = convertSource.format;
  1175. if ((format === 'gif' || format === 'png') && depsStatus[format].loaded !== LoadStatus.loaded) {
  1176. switch (depsStatus[format].loaded) {
  1177. case LoadStatus.unloaded:
  1178. cachedQueue[format].push(meta);
  1179. depsStatus[format].load().then(() => {
  1180. logger.info(`添加${cachedQueue[format].length}个任务`);
  1181. queue.push(...cachedQueue[format]);
  1182. cachedQueue[format].length = 0;
  1183. while (active.length < MAX_CONVERT && queue.length && !isStop) {
  1184. convert(queue.shift());
  1185. }
  1186. }, (reason) => {
  1187. cachedQueue[format].forEach((meta) => {
  1188. meta.reject(reason);
  1189. });
  1190. depsStatus[format].loaded = LoadStatus.unloaded;
  1191. cachedQueue[format].length = 0;
  1192. });
  1193. break;
  1194. case LoadStatus.loading:
  1195. cachedQueue[format].push(meta);
  1196. break;
  1197. default:
  1198. throw new RangeError('Invalid deps status.');
  1199. }
  1200. }
  1201. else {
  1202. queue.push(meta);
  1203. while (active.length < MAX_CONVERT && queue.length && !isStop) {
  1204. convert(queue.shift());
  1205. }
  1206. }
  1207. });
  1208. },
  1209. del: (taskIds) => {
  1210. if (!taskIds.length)
  1211. return;
  1212. logger.info('Converter del, active:', active.map((meta) => meta.id), 'queue:', queue.map((meta) => meta.id));
  1213. isStop = true;
  1214. active = active.filter((convertMeta) => {
  1215. if (taskIds.includes(convertMeta.id)) {
  1216. convertMeta.abort();
  1217. }
  1218. else {
  1219. return true;
  1220. }
  1221. });
  1222. queue = queue.filter((convertMeta) => !taskIds.includes(convertMeta.id));
  1223. isStop = false;
  1224. while (active.length < MAX_CONVERT && queue.length) {
  1225. convert(queue.shift());
  1226. }
  1227. }
  1228. };
  1229. }
  1230. const converter = createConverter();
  1231.  
  1232. const updateDirHandleChannel = new BroadcastChannel('update_dir_channel');
  1233. updateDirHandleChannel.onmessage = (evt) => {
  1234. const data = evt.data;
  1235. switch (data.kind) {
  1236. case 1 :
  1237. dirHandleStatus = 1 ;
  1238. logger.info('正在选择目录');
  1239. break;
  1240. case 0 :
  1241. logger.warn('取消更新dirHandle');
  1242. if (dirHandle) {
  1243. dirHandleStatus = 2 ;
  1244. processCachedSave();
  1245. }
  1246. else {
  1247. dirHandleStatus = 0 ;
  1248. rejectCachedSave();
  1249. }
  1250. break;
  1251. case 2 :
  1252. dirHandleStatus = 2 ;
  1253. dirHandle = data.handle;
  1254. logger.info('更新dirHandle', dirHandle);
  1255. processCachedSave();
  1256. break;
  1257. case 'request':
  1258. if (dirHandle) {
  1259. updateDirHandleChannel.postMessage({
  1260. kind: 'response',
  1261. handle: dirHandle
  1262. });
  1263. logger.info('响应请求dirHandle');
  1264. }
  1265. break;
  1266. case 'response':
  1267. if (!dirHandle) {
  1268. if (dirHandleStatus === 0 )
  1269. dirHandleStatus = 2 ;
  1270. dirHandle = data.handle;
  1271. logger.info('首次获取dirHandle', dirHandle);
  1272. }
  1273. break;
  1274. default:
  1275. throw new Error('Invalid data kind.');
  1276. }
  1277. };
  1278. updateDirHandleChannel.postMessage({ kind: 'request' });
  1279. async function getDirHandleRecursive(dirs) {
  1280. let handler = dirHandle;
  1281. if (typeof dirs === 'string') {
  1282. if (dirs.indexOf('/') === -1)
  1283. return await handler.getDirectoryHandle(dirs, { create: true });
  1284. dirs = dirs.split('/').filter((dir) => !!dir);
  1285. }
  1286. for await (const dir of dirs) {
  1287. handler = await handler.getDirectoryHandle(dir, { create: true });
  1288. }
  1289. return handler;
  1290. }
  1291. const duplicateFilenameCached = {};
  1292. async function getFilenameHandle(dirHandle, filename) {
  1293. const conflictAction = config.get('fileSystemFilenameConflictAction');
  1294. if (conflictAction === 'overwrite')
  1295. return await dirHandle.getFileHandle(filename, { create: true });
  1296. if (!(filename in duplicateFilenameCached)) {
  1297. duplicateFilenameCached[filename] = [];
  1298. try {
  1299. await dirHandle.getFileHandle(filename);
  1300. logger.warn('存在同名文件', filename);
  1301. }
  1302. catch (error) {
  1303. return await dirHandle.getFileHandle(filename, { create: true });
  1304. }
  1305. }
  1306. const extIndex = filename.lastIndexOf('.');
  1307. const ext = filename.slice(extIndex + 1);
  1308. const name = filename.slice(0, extIndex);
  1309. if (conflictAction === 'prompt') {
  1310. return await unsafeWindow.showSaveFilePicker({
  1311. suggestedName: filename,
  1312. types: [{ description: 'Image file', accept: { ['image/' + ext]: ['.' + ext] } }]
  1313. });
  1314. }
  1315. else {
  1316. for (let suffix = 1; suffix < 1000; suffix++) {
  1317. const newName = `${name} (${suffix}).${ext}`;
  1318. try {
  1319. await dirHandle.getFileHandle(newName);
  1320. }
  1321. catch (error) {
  1322. if (duplicateFilenameCached[filename].includes(newName)) {
  1323. continue;
  1324. }
  1325. else {
  1326. duplicateFilenameCached[filename].push(newName);
  1327. }
  1328. logger.info('使用文件名:', newName);
  1329. return await dirHandle.getFileHandle(newName, { create: true });
  1330. }
  1331. }
  1332. throw new RangeError('Oops, you have too many duplicate files.');
  1333. }
  1334. }
  1335. function clearFilenameCached(duplicateName, actualName) {
  1336. if (!(duplicateName in duplicateFilenameCached))
  1337. return;
  1338. const usedNameArr = duplicateFilenameCached[duplicateName];
  1339. logger.info('清理重名文件名', usedNameArr, actualName);
  1340. if (usedNameArr.length === 0) {
  1341. delete duplicateFilenameCached[duplicateName];
  1342. return;
  1343. }
  1344. const index = usedNameArr.indexOf(actualName);
  1345. if (index === -1)
  1346. return;
  1347. usedNameArr.splice(index, 1);
  1348. if (usedNameArr.length === 0)
  1349. delete duplicateFilenameCached[duplicateName];
  1350. }
  1351. async function updateDirHandle() {
  1352. try {
  1353. dirHandleStatus = 1 ;
  1354. updateDirHandleChannel.postMessage({ kind: 1 });
  1355. dirHandle = await unsafeWindow.showDirectoryPicker({ id: 'pdl', mode: 'readwrite' });
  1356. logger.info('更新dirHandle', dirHandle);
  1357. dirHandleStatus = 2 ;
  1358. updateDirHandleChannel.postMessage({
  1359. kind: 2 ,
  1360. handle: dirHandle
  1361. });
  1362. processCachedSave();
  1363. return true;
  1364. }
  1365. catch (error) {
  1366. logger.warn(error);
  1367. updateDirHandleChannel.postMessage({ kind: 0 });
  1368. if (dirHandle) {
  1369. dirHandleStatus = 2 ;
  1370. processCachedSave();
  1371. }
  1372. else {
  1373. dirHandleStatus = 0 ;
  1374. rejectCachedSave();
  1375. }
  1376. return false;
  1377. }
  1378. }
  1379. let dirHandleStatus = 0 ;
  1380. let dirHandle;
  1381. const cachedSaveProcess = [];
  1382. async function saveWithFileSystemAccess(blob, downloadMeta) {
  1383. try {
  1384. if (downloadMeta.state === 0 )
  1385. return;
  1386. if (dirHandleStatus === 1 ) {
  1387. cachedSaveProcess.push([blob, downloadMeta]);
  1388. return;
  1389. }
  1390. if (dirHandleStatus === 0 ) {
  1391. const isSuccess = await updateDirHandle();
  1392. if (!isSuccess)
  1393. throw new TypeError('Failed to get dir handle.');
  1394. }
  1395. let currenDirHandle;
  1396. let filename;
  1397. const path = downloadMeta.source.path;
  1398. const index = path.lastIndexOf('/');
  1399. if (index === -1) {
  1400. filename = path;
  1401. currenDirHandle = dirHandle;
  1402. }
  1403. else {
  1404. filename = path.slice(index + 1);
  1405. currenDirHandle = await getDirHandleRecursive(path.slice(0, index));
  1406. }
  1407. const fileHandle = await getFilenameHandle(currenDirHandle, filename);
  1408. const writableStream = await fileHandle.createWritable();
  1409. await writableStream.write(blob);
  1410. await writableStream.close();
  1411. clearFilenameCached(filename, fileHandle.name);
  1412. downloadMeta.resolve(downloadMeta.taskId);
  1413. logger.info('Download complete:', downloadMeta.source.path);
  1414. }
  1415. catch (error) {
  1416. downloadMeta.reject(error);
  1417. logger.error(error);
  1418. }
  1419. downloadMeta.state = 2 ;
  1420. }
  1421. function processCachedSave() {
  1422. cachedSaveProcess.forEach((args) => saveWithFileSystemAccess(...args));
  1423. logger.info(`执行${cachedSaveProcess.length}个缓存任务`);
  1424. cachedSaveProcess.length = 0;
  1425. }
  1426. function rejectCachedSave() {
  1427. cachedSaveProcess.forEach(([, downloadMeta]) => downloadMeta.reject(new CancelError()));
  1428. logger.info(`取消${cachedSaveProcess.length}个缓存任务`);
  1429. cachedSaveProcess.length = 0;
  1430. }
  1431. function getCurrentDirName() {
  1432. return dirHandle?.name ?? '';
  1433. }
  1434. function isShouldGetDirHandle() {
  1435. return isUseFileSystemAccess() && dirHandleStatus === 0 ;
  1436. }
  1437. function isUseFileSystemAccess() {
  1438. return env.isFileSystemAccessAvaliable() && config.get('useFileSystemAccess');
  1439. }
  1440.  
  1441. const _saveWithoutSubpath = (blob, downloadMeta) => {
  1442. const dlEle = document.createElement('a');
  1443. dlEle.href = URL.createObjectURL(blob);
  1444. dlEle.download = downloadMeta.source.path;
  1445. dlEle.click();
  1446. URL.revokeObjectURL(dlEle.href);
  1447. downloadMeta.state = 2 ;
  1448. downloadMeta.resolve(downloadMeta.taskId);
  1449. };
  1450. const _saveWithSubpath = (blob, downloadMeta) => {
  1451. const imgUrl = URL.createObjectURL(blob);
  1452. const request = {
  1453. url: imgUrl,
  1454. name: downloadMeta.source.path,
  1455. onerror: (error) => {
  1456. if (downloadMeta.state !== 0 ) {
  1457. downloadMeta.reject(new Error(`Download error when saving ${downloadMeta.source.path} because ${error.error} ${error.details ?? ''} `));
  1458. }
  1459. URL.revokeObjectURL(imgUrl);
  1460. },
  1461. onload: () => {
  1462. if (typeof downloadMeta.onLoad === 'function')
  1463. downloadMeta.onLoad();
  1464. URL.revokeObjectURL(imgUrl);
  1465. downloadMeta.state = 2 ;
  1466. downloadMeta.resolve(downloadMeta.taskId);
  1467. logger.info('Download complete:', downloadMeta.source.path);
  1468. }
  1469. };
  1470. downloadMeta.abort = GM_download(request).abort;
  1471. };
  1472. function createDownloader() {
  1473. const MAX_DOWNLOAD = 5;
  1474. const MAX_RETRY = 3;
  1475. const INTERVAL = 500;
  1476. const TIMEOUT = 20000;
  1477. let isStop = false;
  1478. let queue = [];
  1479. let active = [];
  1480. let save;
  1481. if (env.isBlobDlAvaliable() && env.isSupportSubpath()) {
  1482. save = _saveWithSubpath;
  1483. }
  1484. else {
  1485. logger.warn('Download function not full support:', GM_info.scriptHandler, GM_info.version);
  1486. save = _saveWithoutSubpath;
  1487. }
  1488. const download = (downloadMeta) => {
  1489. const { taskId, source } = downloadMeta;
  1490. logger.info('Start download:', source.path);
  1491. active.push(downloadMeta);
  1492. let fileSaveFn = save;
  1493. const isUseFSA = isUseFileSystemAccess();
  1494. if (isUseFSA) {
  1495. fileSaveFn = saveWithFileSystemAccess;
  1496. }
  1497. let abortObj;
  1498. const errorHandler = errorHandlerFactory(downloadMeta);
  1499. if ((!env.isBlobDlAvaliable() || (env.isViolentmonkey() && !isUseFSA)) && !('kind' in source)) {
  1500. abortObj = GM_download({
  1501. url: source.src,
  1502. name: source.path,
  1503. headers: {
  1504. referer: 'https://www.pixiv.net'
  1505. },
  1506. ontimeout: errorHandler,
  1507. onerror: errorHandler,
  1508. onload: async () => {
  1509. logger.info('Download complete:', taskId, source.path);
  1510. if (typeof downloadMeta.onLoad === 'function')
  1511. downloadMeta.onLoad();
  1512. downloadMeta.resolve(taskId);
  1513. await sleep(INTERVAL);
  1514. active.splice(active.indexOf(downloadMeta), 1);
  1515. if (queue.length && !isStop)
  1516. download(queue.shift());
  1517. }
  1518. });
  1519. }
  1520. else {
  1521. abortObj = GM_xmlhttpRequest({
  1522. url: source.src,
  1523. timeout: TIMEOUT,
  1524. method: 'GET',
  1525. headers: {
  1526. referer: 'https://www.pixiv.net'
  1527. },
  1528. responseType: 'blob',
  1529. ontimeout: errorHandler,
  1530. onerror: errorHandler,
  1531. onprogress: (e) => {
  1532. if (e.lengthComputable && typeof downloadMeta.onProgress === 'function') {
  1533. downloadMeta.onProgress(e.loaded / e.total);
  1534. }
  1535. },
  1536. onload: async (e) => {
  1537. logger.info('Xhr complete:', source.path);
  1538. if (downloadMeta.state === 0 )
  1539. return logger.warn('Download was canceled.', taskId, source.path);
  1540. if (!('kind' in source)) {
  1541. fileSaveFn(e.response, downloadMeta);
  1542. }
  1543. else if (source.kind === 'convert') {
  1544. const convertSource = {
  1545. id: taskId,
  1546. data: e.response,
  1547. format: source.format,
  1548. framesInfo: source.ugoiraMeta?.frames
  1549. };
  1550. converter.add(convertSource, { onProgress: downloadMeta.onProgress }).then((blob) => {
  1551. fileSaveFn(blob, downloadMeta);
  1552. }, downloadMeta.reject);
  1553. }
  1554. else if (source.kind === 'bundle') {
  1555. compressor.add(taskId, source.filename, e.response);
  1556. if (compressor.fileCount(taskId) === source.pageCount) {
  1557. compressor.bundle(taskId).then((blob) => {
  1558. fileSaveFn(blob, downloadMeta);
  1559. compressor.remove(taskId);
  1560. });
  1561. }
  1562. else {
  1563. downloadMeta.resolve(taskId);
  1564. if (typeof downloadMeta.onLoad === 'function')
  1565. downloadMeta.onLoad();
  1566. }
  1567. }
  1568. await sleep(INTERVAL);
  1569. active.splice(active.indexOf(downloadMeta), 1);
  1570. if (queue.length && !isStop)
  1571. download(queue.shift());
  1572. }
  1573. });
  1574. }
  1575. downloadMeta.abort = abortObj.abort;
  1576. };
  1577. function errorHandlerFactory(downloadMeta) {
  1578. return function (error) {
  1579. const { taskId, source, state } = downloadMeta;
  1580. if (state === 0 )
  1581. return;
  1582. if (error) {
  1583. logger.error('Download ' + taskId + ' error', error.error ? ' with reason: ' + error.error : '', 'details' in error ? error.details : error);
  1584. if ('status' in error && error.status === 429) {
  1585. downloadMeta.reject(new RequestError('Too many request', error));
  1586. active.splice(active.indexOf(downloadMeta), 1);
  1587. return;
  1588. }
  1589. }
  1590. else {
  1591. logger.warn('Download timeout:', source.src);
  1592. }
  1593. downloadMeta.retry++;
  1594. if (downloadMeta.retry > MAX_RETRY) {
  1595. downloadMeta.reject(new Error('Download failed: ' + source.src));
  1596. active.splice(active.indexOf(downloadMeta), 1);
  1597. if (queue.length && !isStop)
  1598. download(queue.shift());
  1599. }
  1600. else {
  1601. logger.info('Download retry:', downloadMeta.retry, source.src);
  1602. download(downloadMeta);
  1603. }
  1604. };
  1605. }
  1606. return {
  1607. add(metas, handler = {}) {
  1608. logger.info('Downloader add:', metas);
  1609. if (metas.length < 1)
  1610. return Promise.resolve('');
  1611. const promises = [];
  1612. metas.forEach((source) => {
  1613. promises.push(new Promise((resolve, reject) => {
  1614. const downloadMeta = {
  1615. taskId: source.taskId,
  1616. source,
  1617. state: 1 ,
  1618. retry: 0,
  1619. onProgress: handler.onProgress,
  1620. onLoad: handler.onLoad,
  1621. resolve,
  1622. reject
  1623. };
  1624. queue.push(downloadMeta);
  1625. }));
  1626. });
  1627. while (active.length < MAX_DOWNLOAD && queue.length && !isStop) {
  1628. download(queue.shift());
  1629. }
  1630. return Promise.all(promises).then(([taskId]) => taskId);
  1631. },
  1632. del(taskIds) {
  1633. if (!taskIds.length)
  1634. return;
  1635. isStop = true;
  1636. logger.info('Downloader delete. active:', active.length, 'queue', queue.length);
  1637. active = active.filter((downloadMeta) => {
  1638. if (taskIds.includes(downloadMeta.taskId) && downloadMeta.state !== 2 ) {
  1639. downloadMeta.abort?.();
  1640. downloadMeta.state = 0 ;
  1641. downloadMeta.reject(new CancelError());
  1642. logger.warn('Download abort manually.', downloadMeta.source.path);
  1643. }
  1644. else {
  1645. return true;
  1646. }
  1647. });
  1648. converter.del(taskIds);
  1649. compressor.remove(taskIds);
  1650. queue = queue.filter((downloadMeta) => !taskIds.includes(downloadMeta.taskId));
  1651. isStop = false;
  1652. while (active.length < MAX_DOWNLOAD && queue.length) {
  1653. download(queue.shift());
  1654. }
  1655. }
  1656. };
  1657. }
  1658. const downloader = createDownloader();
  1659.  
  1660. const needBundle = (type) => {
  1661. return ((type === IllustType.manga && config.get('bundleManga')) ||
  1662. (type === IllustType.illusts && config.get('bundleIllusts')));
  1663. };
  1664. const getFilePath = ({ user, userId, title, tagStr, illustId, createDate, page, ext }, option = { needBundle: false, needConvert: false }) => {
  1665. let pathPattern;
  1666. const folderPattern = config.get('folderPattern');
  1667. const filenamePattern = config.get('filenamePattern');
  1668. const isUseFSA = isUseFileSystemAccess();
  1669. const shouldAddFolder = !!folderPattern &&
  1670. !option.needBundle &&
  1671. ((env.isSupportSubpath() && (!option.needConvert || env.isBlobDlAvaliable())) || isUseFSA);
  1672. if (shouldAddFolder) {
  1673. pathPattern = folderPattern + '/' + filenamePattern;
  1674. }
  1675. else {
  1676. pathPattern = filenamePattern;
  1677. }
  1678. if (option.needBundle && !filenamePattern.includes('{page}')) {
  1679. pathPattern += '_{page}';
  1680. }
  1681. function replaceDate(match, p1) {
  1682. const format = p1 || 'YYYY-MM-DD';
  1683. return dayjs__default["default"](createDate).format(format);
  1684. }
  1685. return (pathPattern
  1686. .replaceAll(/\{date\((.*?)\)\}|\{date\}/g, replaceDate)
  1687. .replaceAll('{artist}', user)
  1688. .replaceAll('{artistID}', userId)
  1689. .replaceAll('{title}', title)
  1690. .replaceAll('{tags}', tagStr)
  1691. .replaceAll('{page}', String(page))
  1692. .replaceAll('{id}', illustId) + ext);
  1693. };
  1694. const makeTagsStr = (prev, cur, index, tagsArr) => {
  1695. const tag = config.get('tagLang') === 'ja' ? cur.tag : cur.translation?.['en'] || cur.tag;
  1696. if (index < tagsArr.length - 1) {
  1697. return prev + tag + '_';
  1698. }
  1699. else {
  1700. return prev + tag;
  1701. }
  1702. };
  1703. function isValidIllustType(illustType, option) {
  1704. switch (illustType) {
  1705. case IllustType.illusts:
  1706. if (option.filterIllusts)
  1707. return true;
  1708. break;
  1709. case IllustType.manga:
  1710. if (option.filterManga)
  1711. return true;
  1712. break;
  1713. case IllustType.ugoira:
  1714. if (option.filterUgoria)
  1715. return true;
  1716. break;
  1717. default:
  1718. throw new Error('Invalid filter type');
  1719. }
  1720. return false;
  1721. }
  1722. function filterWorks(works, option) {
  1723. const obj = {
  1724. unavaliable: [],
  1725. avaliable: [],
  1726. invalid: []
  1727. };
  1728. works.forEach((work) => {
  1729. if (!work.isBookmarkable) {
  1730. obj.unavaliable.push(work.id);
  1731. }
  1732. else if (option.filterExcludeDownloaded && pixivHistory.has(work.id)) {
  1733. obj.invalid.push(work.id);
  1734. }
  1735. else if (!isValidIllustType(work.illustType, option)) {
  1736. obj.invalid.push(work.id);
  1737. }
  1738. else {
  1739. obj.avaliable.push(work.id);
  1740. }
  1741. });
  1742. return obj;
  1743. }
  1744. async function getFollowLatestGenerator(filterOption, mode, page) {
  1745. const MAX_PAGE = 34;
  1746. const MAX_ILLUSTS_PER_PAGE = 60;
  1747. let lastId;
  1748. let total;
  1749. let data;
  1750. let cache;
  1751. function findLastId(ids) {
  1752. return Math.min(...ids.map((id) => Number(id)));
  1753. }
  1754. if (page === undefined) {
  1755. data = await api.getFollowLatestWorks(1, mode);
  1756. const ids = data.page.ids;
  1757. total = ids.length;
  1758. lastId = findLastId(ids);
  1759. if (total === MAX_ILLUSTS_PER_PAGE) {
  1760. const secondPageData = await api.getFollowLatestWorks(2, mode);
  1761. const secondIds = secondPageData.page.ids;
  1762. const secondLastId = findLastId(secondIds);
  1763. if (secondLastId < lastId) {
  1764. lastId = secondLastId;
  1765. cache = secondPageData;
  1766. total += secondIds.length;
  1767. }
  1768. }
  1769. }
  1770. else {
  1771. data = await api.getFollowLatestWorks(page, mode);
  1772. total = data.page.ids.length;
  1773. }
  1774. async function* generateIds() {
  1775. yield filterWorks(data.thumbnails.illust, filterOption);
  1776. if (page === undefined) {
  1777. if (total === MAX_ILLUSTS_PER_PAGE)
  1778. return;
  1779. if (total < MAX_ILLUSTS_PER_PAGE * 2) {
  1780. yield filterWorks(cache.thumbnails.illust, filterOption);
  1781. return;
  1782. }
  1783. let currentPage = 3;
  1784. while (currentPage <= MAX_PAGE) {
  1785. const data = await api.getFollowLatestWorks(currentPage, mode);
  1786. const ids = data.page.ids;
  1787. const pageLastId = findLastId(ids);
  1788. if (pageLastId >= lastId) {
  1789. logger.info('getFollowLatestGenerator: got duplicate works');
  1790. yield filterWorks(cache.thumbnails.illust, filterOption);
  1791. break;
  1792. }
  1793. lastId = pageLastId;
  1794. total += ids.length;
  1795. yield { ...filterWorks(cache.thumbnails.illust, filterOption), total };
  1796. cache = data;
  1797. currentPage++;
  1798. await sleep(3000);
  1799. }
  1800. }
  1801. }
  1802. return {
  1803. total,
  1804. generator: generateIds()
  1805. };
  1806. }
  1807. async function getChunksGenerator(userId, category, tag, rest, filterOption) {
  1808. const OFFSET = 48;
  1809. let requestUrl;
  1810. if (category === 'bookmarks') {
  1811. requestUrl = `https://www.pixiv.net/ajax/user/${userId}/illusts/bookmarks?tag=${tag}&offset=0&limit=${OFFSET}&rest=${rest}&lang=ja`;
  1812. }
  1813. else {
  1814. requestUrl = `https://www.pixiv.net/ajax/user/${userId}/${category}/tag?tag=${tag}&offset=0&limit=${OFFSET}&lang=ja`;
  1815. }
  1816. let head = 0;
  1817. const firstPageData = await api.getJson(requestUrl);
  1818. const total = firstPageData.total;
  1819. async function* generateIds() {
  1820. yield filterWorks(firstPageData.works, filterOption);
  1821. head += OFFSET;
  1822. while (head < total) {
  1823. const data = await api.getJson(requestUrl.replace('offset=0', 'offset=' + head));
  1824. head += OFFSET;
  1825. await sleep(3000);
  1826. yield filterWorks(data.works, filterOption);
  1827. }
  1828. }
  1829. return {
  1830. total,
  1831. generator: generateIds()
  1832. };
  1833. }
  1834. async function getAllWorksGenerator(userId, filterOption) {
  1835. const profile = await api.getUserAllProfile(userId);
  1836. let illustIds = [];
  1837. let mangaIds = [];
  1838. if ((filterOption.filterIllusts || filterOption.filterUgoria) && typeof profile.illusts === 'object') {
  1839. illustIds.push(...Object.keys(profile.illusts).reverse());
  1840. }
  1841. if (filterOption.filterManga && typeof profile.manga === 'object') {
  1842. mangaIds.push(...Object.keys(profile.manga).reverse());
  1843. }
  1844. if (filterOption.filterExcludeDownloaded) {
  1845. illustIds = illustIds.filter((id) => !pixivHistory.has(id));
  1846. mangaIds = mangaIds.filter((id) => !pixivHistory.has(id));
  1847. }
  1848. async function* generateIds() {
  1849. const OFFSET = 48;
  1850. const baseUrl = 'https://www.pixiv.net/ajax/user/' + userId + '/profile/illusts';
  1851. let workCategory = 'illust';
  1852. while (illustIds.length > 0) {
  1853. let searchStr = '?';
  1854. const chunk = illustIds.splice(0, OFFSET);
  1855. searchStr +=
  1856. chunk.map((id) => 'ids[]=' + id).join('&') + `&work_category=${workCategory}&is_first_page=0&lang=ja`;
  1857. const data = await api.getJson(baseUrl + searchStr);
  1858. await sleep(3000);
  1859. yield filterWorks(Object.values(data.works).reverse(), filterOption);
  1860. }
  1861. workCategory = 'manga';
  1862. while (mangaIds.length > 0) {
  1863. let searchStr = '?';
  1864. const chunk = mangaIds.splice(0, OFFSET);
  1865. searchStr +=
  1866. chunk.map((id) => 'ids[]=' + id).join('&') + `&work_category=${workCategory}&is_first_page=0&lang=ja`;
  1867. const data = await api.getJson(baseUrl + searchStr);
  1868. await sleep(3000);
  1869. yield filterWorks(Object.values(data.works).reverse(), filterOption);
  1870. }
  1871. }
  1872. return {
  1873. total: illustIds.length + mangaIds.length,
  1874. generator: generateIds()
  1875. };
  1876. }
  1877. async function getArtworkData(illustId) {
  1878. const htmlText = await api.getArtworkHtml(illustId);
  1879. const preloadDataText = htmlText.match(regexp.preloadData);
  1880. if (!preloadDataText)
  1881. throw new Error('Fail to parse preload data.');
  1882. const preloadData = JSON.parse(preloadDataText[1]);
  1883. const illustData = preloadData.illust[illustId];
  1884. const globalDataText = htmlText.match(regexp.globalData);
  1885. if (!globalDataText)
  1886. throw new Error('Fail to parse global data.');
  1887. const globalData = JSON.parse(globalDataText[1]);
  1888. let ugoiraMeta;
  1889. if (illustData.illustType === IllustType.ugoira) {
  1890. ugoiraMeta = await api.getUgoriaMeta(illustId);
  1891. }
  1892. return {
  1893. illustData,
  1894. globalData,
  1895. ugoiraMeta
  1896. };
  1897. }
  1898. async function getAjaxArtworkData(illustId) {
  1899. const illustData = await api.getArtworkDetail(illustId);
  1900. let ugoiraMeta;
  1901. if (illustData.illustType === IllustType.ugoira) {
  1902. ugoiraMeta = await api.getUgoriaMeta(illustId);
  1903. }
  1904. return {
  1905. illustData,
  1906. ugoiraMeta
  1907. };
  1908. }
  1909. function getDownloadSource(artworkData, seletedPage) {
  1910. const { illustData, ugoiraMeta } = artworkData;
  1911. const { illustType, userName, userId, illustTitle, illustId, tags, pageCount, createDate } = illustData;
  1912. const pathInfo = {
  1913. user: replaceInvalidChar(unescapeHtml(userName)) || 'userId-' + userId,
  1914. title: replaceInvalidChar(unescapeHtml(illustTitle)) || 'illustId-' + illustId,
  1915. tagStr: replaceInvalidChar(unescapeHtml(tags.tags.reduce(makeTagsStr, ''))),
  1916. illustId,
  1917. userId,
  1918. createDate,
  1919. ext: '',
  1920. page: 0
  1921. };
  1922. const metas = [];
  1923. const taskId = illustId + '_' + Math.random().toString(36).slice(2);
  1924. if (illustType === IllustType.illusts || illustType === IllustType.manga) {
  1925. const firstImgSrc = illustData.urls.original;
  1926. const srcPrefix = firstImgSrc.slice(0, firstImgSrc.indexOf('_') + 2);
  1927. const extendName = firstImgSrc.slice(-4);
  1928. pathInfo.ext = extendName;
  1929. if (pageCount > 1 && seletedPage === undefined) {
  1930. if (needBundle(illustType)) {
  1931. const path = getFilePath({ ...pathInfo, ext: '.zip', page: pageCount });
  1932. for (let i = 0; i < pageCount; i++) {
  1933. pathInfo.page = i;
  1934. metas.push({
  1935. kind: 'bundle',
  1936. taskId,
  1937. path,
  1938. src: srcPrefix + i + extendName,
  1939. filename: getFilePath(pathInfo, { needBundle: true }),
  1940. pageCount
  1941. });
  1942. }
  1943. }
  1944. else {
  1945. for (let i = 0; i < pageCount; i++) {
  1946. pathInfo.page = i;
  1947. metas.push({
  1948. taskId,
  1949. path: getFilePath(pathInfo),
  1950. src: srcPrefix + i + extendName
  1951. });
  1952. }
  1953. }
  1954. }
  1955. else {
  1956. let src = firstImgSrc;
  1957. if (seletedPage !== undefined) {
  1958. src = srcPrefix + seletedPage + extendName;
  1959. pathInfo.page = seletedPage;
  1960. }
  1961. metas.push({
  1962. taskId,
  1963. path: getFilePath(pathInfo),
  1964. src
  1965. });
  1966. }
  1967. }
  1968. else if (illustType === IllustType.ugoira && ugoiraMeta) {
  1969. const ugoriaFormat = config.get('ugoriaFormat');
  1970. pathInfo.ext = '.' + ugoriaFormat;
  1971. if (ugoriaFormat !== 'zip') {
  1972. metas.push({
  1973. kind: 'convert',
  1974. format: ugoriaFormat,
  1975. ugoiraMeta,
  1976. taskId,
  1977. src: ugoiraMeta.originalSrc,
  1978. path: getFilePath(pathInfo, { needConvert: true })
  1979. });
  1980. }
  1981. else {
  1982. metas.push({
  1983. taskId,
  1984. src: ugoiraMeta.originalSrc,
  1985. path: getFilePath(pathInfo)
  1986. });
  1987. }
  1988. }
  1989. return metas;
  1990. }
  1991. const parser = {
  1992. getChunksGenerator,
  1993. getAllWorksGenerator,
  1994. getFollowLatestGenerator,
  1995. getArtworkData,
  1996. getDownloadSource,
  1997. getAjaxArtworkData
  1998. };
  1999.  
  2000. function handleDownload(pdlBtn, illustId) {
  2001. if (isShouldGetDirHandle())
  2002. updateDirHandle();
  2003. let pageCount;
  2004. let pageComplete = 0;
  2005. let shouldDownloadPage;
  2006. let downloading = true;
  2007. const pageAttr = pdlBtn.getAttribute('should-download');
  2008. if (pageAttr) {
  2009. shouldDownloadPage = Number(pageAttr);
  2010. }
  2011. const onProgress = (progress = 0, type = null) => {
  2012. if (pageCount > 1 || !downloading)
  2013. return;
  2014. progress = Math.floor(progress * 100);
  2015. switch (type) {
  2016. case null:
  2017. pdlBtn.style.setProperty('--pdl-progress', progress + '%');
  2018. case 'gif':
  2019. case 'webm':
  2020. case 'webp':
  2021. pdlBtn.textContent = String(progress);
  2022. break;
  2023. case 'zip':
  2024. pdlBtn.textContent = '';
  2025. break;
  2026. }
  2027. };
  2028. const onLoad = function () {
  2029. if (pageCount < 2 || !downloading)
  2030. return;
  2031. const progress = Math.floor((++pageComplete / pageCount) * 100);
  2032. pdlBtn.textContent = String(progress);
  2033. pdlBtn.style.setProperty('--pdl-progress', progress + '%');
  2034. };
  2035. pdlBtn.classList.add('pdl-progress');
  2036. parser
  2037. .getArtworkData(illustId)
  2038. .then((artworkData) => {
  2039. const { illustData, globalData } = artworkData;
  2040. const { illustId, tags, bookmarkData } = illustData;
  2041. if (!bookmarkData) {
  2042. const { token } = globalData;
  2043. const tagsArr = tags.tags.map((item) => item.tag);
  2044. addBookmark(pdlBtn, illustId, token, tagsArr);
  2045. }
  2046. return parser.getDownloadSource(artworkData, shouldDownloadPage);
  2047. })
  2048. .then((sources) => {
  2049. pageCount = sources.length;
  2050. return downloader.add(sources, { onLoad, onProgress });
  2051. })
  2052. .then(() => {
  2053. pixivHistory.add(illustId);
  2054. pdlBtn.classList.remove('pdl-error');
  2055. pdlBtn.classList.add('pdl-complete');
  2056. })
  2057. .catch((err) => {
  2058. if (err)
  2059. logger.error(err);
  2060. pdlBtn.classList.remove('pdl-complete');
  2061. pdlBtn.classList.add('pdl-error');
  2062. })
  2063. .finally(() => {
  2064. downloading = false;
  2065. pdlBtn.innerHTML = '';
  2066. pdlBtn.style.removeProperty('--pdl-progress');
  2067. pdlBtn.classList.remove('pdl-progress');
  2068. });
  2069. }
  2070.  
  2071. function createPdlBtn(attributes, textContent = '', { addEvent } = { addEvent: true }) {
  2072. const ele = document.createElement('button');
  2073. ele.textContent = textContent;
  2074. if (!attributes)
  2075. return ele;
  2076. const { attrs, classList } = attributes;
  2077. if (classList && classList.length > 0) {
  2078. for (const cla of classList) {
  2079. ele.classList.add(cla);
  2080. }
  2081. }
  2082. if (attrs) {
  2083. for (const key in attrs) {
  2084. ele.setAttribute(key, attrs[key]);
  2085. }
  2086. }
  2087. if (addEvent) {
  2088. ele.addEventListener('click', (evt) => {
  2089. evt.preventDefault();
  2090. evt.stopPropagation();
  2091. const ele = evt.currentTarget;
  2092. if (!ele.classList.contains('pdl-progress')) {
  2093. handleDownload(ele, ele.getAttribute('pdl-id'));
  2094. }
  2095. });
  2096. }
  2097. return ele;
  2098. }
  2099.  
  2100. function createThumbnailsBtn(nodes) {
  2101. let isSelfBookmark = false;
  2102. const inBookmarkPage = regexp.bookmarkPage.exec(location.pathname);
  2103. if (inBookmarkPage) {
  2104. inBookmarkPage[1] === getSelfId() && (isSelfBookmark = true);
  2105. }
  2106. nodes.forEach((e) => {
  2107. if (e.childElementCount !== 0 && !e.querySelector('.pdl-btn-sub')) {
  2108. const illustId = getIllustId(e);
  2109. if (illustId) {
  2110. const attrs = {
  2111. attrs: { 'pdl-id': illustId },
  2112. classList: ['pdl-btn', 'pdl-btn-sub']
  2113. };
  2114. if (pixivHistory.has(illustId))
  2115. attrs.classList.push('pdl-complete');
  2116. if (isSelfBookmark)
  2117. attrs.classList.push('self-bookmark');
  2118. e.appendChild(createPdlBtn(attrs));
  2119. }
  2120. }
  2121. });
  2122. }
  2123.  
  2124. function fixPixivPreviewer(nodes) {
  2125. const isPpSearchPage = regexp.ppSearchPage.test(location.pathname);
  2126. if (!isPpSearchPage)
  2127. return;
  2128. nodes.forEach((node) => {
  2129. const pdlEle = node.querySelector('.pdl-btn');
  2130. if (!pdlEle)
  2131. return false;
  2132. pdlEle.remove();
  2133. });
  2134. }
  2135.  
  2136. function getFilterOption() {
  2137. return {
  2138. filterExcludeDownloaded: config.get('filterExcludeDownloaded'),
  2139. filterIllusts: config.get('filterIllusts'),
  2140. filterManga: config.get('filterManga'),
  2141. filterUgoria: config.get('filterUgoria')
  2142. };
  2143. }
  2144. function downloadAndRetry(chunksGenerators) {
  2145. useDownloadBar(chunksGenerators).then((failed) => {
  2146. if (failed instanceof Array && failed.length) {
  2147. const gen = async function* () {
  2148. yield {
  2149. avaliable: failed,
  2150. unavaliable: [],
  2151. invalid: []
  2152. };
  2153. };
  2154. console.log('[Pixiv Downloader] Retry...');
  2155. useDownloadBar(Promise.resolve({ total: failed.length, generator: gen() }));
  2156. }
  2157. });
  2158. }
  2159. function downloadWorks(evt) {
  2160. evt.preventDefault();
  2161. evt.stopPropagation();
  2162. if (isDownloading)
  2163. return;
  2164. const btn = evt.target;
  2165. const userId = btn.getAttribute('pdl-userid');
  2166. const filterOption = getFilterOption();
  2167. if (isShouldGetDirHandle())
  2168. updateDirHandle();
  2169. const ids = parser.getAllWorksGenerator(userId, filterOption);
  2170. downloadAndRetry(ids);
  2171. }
  2172. async function downloadBookmarksOrTags(evt) {
  2173. evt.preventDefault();
  2174. evt.stopPropagation();
  2175. if (isDownloading)
  2176. return;
  2177. const btn = evt.target;
  2178. const userId = btn.getAttribute('pdl-userid');
  2179. const category = btn.getAttribute('category');
  2180. const tag = btn.getAttribute('tag') || '';
  2181. const rest = (btn.getAttribute('rest') || 'show');
  2182. if (isShouldGetDirHandle())
  2183. updateDirHandle();
  2184. const filterOption = getFilterOption();
  2185. let idsGenerators;
  2186. if (rest === 'all') {
  2187. const idsShowPromise = parser.getChunksGenerator(userId, 'bookmarks', '', 'show', filterOption);
  2188. const idsHidePromise = parser.getChunksGenerator(userId, 'bookmarks', '', 'hide', filterOption);
  2189. idsGenerators = [idsShowPromise, idsHidePromise];
  2190. }
  2191. else {
  2192. idsGenerators = parser.getChunksGenerator(userId, category, tag, rest, filterOption);
  2193. }
  2194. downloadAndRetry(idsGenerators);
  2195. }
  2196. function downloadFollowLatest(evt) {
  2197. evt.preventDefault();
  2198. evt.stopPropagation();
  2199. if (isDownloading)
  2200. return;
  2201. const btn = evt.target;
  2202. const mode = location.pathname.includes('r18') ? 'r18' : 'all';
  2203. const filterOption = getFilterOption();
  2204. let idsGenerators;
  2205. if (btn.classList.contains('pdl-dl-all')) {
  2206. idsGenerators = parser.getFollowLatestGenerator(filterOption, mode);
  2207. }
  2208. else {
  2209. const params = new URLSearchParams(location.search);
  2210. const page = Number(params.get('p')) || 1;
  2211. idsGenerators = parser.getFollowLatestGenerator(filterOption, mode, page);
  2212. }
  2213. downloadAndRetry(idsGenerators);
  2214. }
  2215.  
  2216. const dlBarRef = {
  2217. filter: {
  2218. filterExcludeDownloaded: undefined,
  2219. filterIllusts: undefined,
  2220. filterManga: undefined,
  2221. filterUgoria: undefined
  2222. },
  2223. statusBar: undefined,
  2224. abortBtn: undefined
  2225. };
  2226. function updateStatus(str) {
  2227. dlBarRef.statusBar && (dlBarRef.statusBar.textContent = str);
  2228. }
  2229. function createFilterEl(id, filterType, text) {
  2230. const checkbox = document.createElement('input');
  2231. const label = document.createElement('label');
  2232. checkbox.id = id;
  2233. checkbox.type = 'checkbox';
  2234. checkbox.classList.add('pdl-checkbox');
  2235. checkbox.setAttribute('category', String(filterType));
  2236. checkbox.checked = config.get(filterType);
  2237. label.setAttribute('for', id);
  2238. label.setAttribute('category', String(filterType));
  2239. label.textContent = text;
  2240. checkbox.addEventListener('change', (evt) => {
  2241. const checkbox = evt.currentTarget;
  2242. const category = checkbox.getAttribute('category');
  2243. config.set(category, checkbox.checked);
  2244. });
  2245. dlBarRef.filter[filterType] = checkbox;
  2246. const wrap = document.createElement('div');
  2247. wrap.classList.add('pdl-filter');
  2248. wrap.appendChild(checkbox);
  2249. wrap.appendChild(label);
  2250. return wrap;
  2251. }
  2252. function createFilter() {
  2253. const wrapper = document.createElement('div');
  2254. wrapper.classList.add('pdl-filter-wrap');
  2255. wrapper.appendChild(createFilterEl('pdl-filter-exclude_downloaded', 'filterExcludeDownloaded', t('downloadBar.filter.exclude_downloaded')));
  2256. wrapper.appendChild(createFilterEl('pdl-filter-illusts', 'filterIllusts', t('downloadBar.filter.illusts')));
  2257. wrapper.appendChild(createFilterEl('pdl-filter-manga', 'filterManga', t('downloadBar.filter.manga')));
  2258. wrapper.appendChild(createFilterEl('pdl-filter-ugoria', 'filterUgoria', t('downloadBar.filter.ugoria')));
  2259. return wrapper;
  2260. }
  2261. function createDownloadBar(userId) {
  2262. const nav = document.querySelector('nav[class~="sc-192ftwf-0"]');
  2263. if (!nav)
  2264. return;
  2265. const dlBtn = nav.querySelector('.pdl-btn-all');
  2266. if (dlBtn) {
  2267. if (dlBtn.getAttribute('pdl-userid') === userId)
  2268. return;
  2269. removeDownloadBar();
  2270. }
  2271. const dlBar = document.createElement('div');
  2272. dlBar.classList.add('pdl-dlbar');
  2273. const statusBar = document.createElement('div');
  2274. statusBar.classList.add('pdl-dlbar-status_bar');
  2275. dlBarRef.statusBar = dlBar.appendChild(statusBar);
  2276. const baseClasses = nav.querySelector('a:not([aria-current])').classList;
  2277. dlBarRef.abortBtn = dlBar.appendChild(createPdlBtn({
  2278. attrs: { 'pdl-userId': userId },
  2279. classList: [...baseClasses, 'pdl-stop', 'pdl-hide']
  2280. }, t('downloadBar.button.stop'), { addEvent: false }));
  2281. if (userId !== getSelfId()) {
  2282. const hasWorks = ["a[href$='illustrations']", "a[href$='manga']"].some((selector) => !!nav.querySelector(selector));
  2283. if (hasWorks) {
  2284. const el = createPdlBtn({
  2285. attrs: { 'pdl-userid': userId },
  2286. classList: [...baseClasses, 'pdl-btn-all']
  2287. }, t('downloadBar.button.works'), { addEvent: false });
  2288. el.addEventListener('click', downloadWorks);
  2289. dlBar.appendChild(el);
  2290. }
  2291. if (nav.querySelector("a[href*='bookmarks']")) {
  2292. const el = createPdlBtn({
  2293. attrs: { 'pdl-userid': userId, category: 'bookmarks' },
  2294. classList: [...baseClasses, 'pdl-btn-all']
  2295. }, t('downloadBar.button.bookmarks'), { addEvent: false });
  2296. el.addEventListener('click', downloadBookmarksOrTags);
  2297. dlBar.appendChild(el);
  2298. }
  2299. }
  2300. else {
  2301. if (nav.querySelector("a[href*='bookmarks']")) {
  2302. dlBar.appendChild(createPdlBtn({
  2303. attrs: { 'pdl-userid': userId, category: 'bookmarks', rest: 'all' },
  2304. classList: [...baseClasses, 'pdl-btn-all']
  2305. }, t('downloadBar.button.bookmarks'), { addEvent: false }));
  2306. dlBar.appendChild(createPdlBtn({
  2307. attrs: {
  2308. 'pdl-userid': userId,
  2309. category: 'bookmarks',
  2310. rest: 'show'
  2311. },
  2312. classList: [...baseClasses, 'pdl-btn-all']
  2313. }, t('downloadBar.button.bookmarks_public'), { addEvent: false }));
  2314. dlBar.appendChild(createPdlBtn({
  2315. attrs: {
  2316. 'pdl-userid': userId,
  2317. category: 'bookmarks',
  2318. rest: 'hide'
  2319. },
  2320. classList: [...baseClasses, 'pdl-btn-all']
  2321. }, t('downloadBar.button.bookmarks_private'), { addEvent: false }));
  2322. dlBar.querySelectorAll('.pdl-btn-all').forEach((node) => {
  2323. node.addEventListener('click', downloadBookmarksOrTags);
  2324. });
  2325. }
  2326. }
  2327. const filter = createFilter();
  2328. nav.parentElement.insertBefore(filter, nav);
  2329. nav.appendChild(dlBar);
  2330. }
  2331. function removeDownloadBar() {
  2332. const dlBarWrap = document.querySelector('.pdl-dlbar');
  2333. if (dlBarWrap) {
  2334. dlBarWrap.remove();
  2335. document.querySelector('.pdl-filter-wrap')?.remove();
  2336. }
  2337. }
  2338. function updateFollowLatestDownloadBarBtnText(prevDlBtn, prevDlAllBtn) {
  2339. if (location.pathname.includes('r18') && prevDlBtn.textContent !== t('downloadBar.button.r18_one_page')) {
  2340. prevDlBtn.textContent = t('downloadBar.button.r18_one_page');
  2341. prevDlAllBtn.textContent = t('downloadBar.button.r18');
  2342. }
  2343. else if (!location.pathname.includes('r18') && prevDlBtn.textContent !== t('downloadBar.button.all_one_page')) {
  2344. prevDlBtn.textContent = t('downloadBar.button.all_one_page');
  2345. prevDlAllBtn.textContent = t('downloadBar.button.all');
  2346. }
  2347. }
  2348. function createFollowLatestDownloadBar() {
  2349. const prevDlBtn = document.querySelector('.pdl-btn-all');
  2350. if (prevDlBtn) {
  2351. const prevDlAllBtn = document.querySelector('.pdl-dl-all');
  2352. updateFollowLatestDownloadBarBtnText(prevDlBtn, prevDlAllBtn);
  2353. return;
  2354. }
  2355. const nav = document.querySelector('nav');
  2356. if (!nav || nav.parentElement.childElementCount === 1)
  2357. return;
  2358. const navBar = nav.parentElement;
  2359. const modeSwitch = nav.nextElementSibling;
  2360. const filter = createFilter();
  2361. navBar.parentElement.insertBefore(filter, navBar);
  2362. const dlBar = document.createElement('div');
  2363. dlBar.classList.add('pdl-dlbar');
  2364. dlBar.classList.add('pdl-dlbar-follow_latest');
  2365. const statusBar = document.createElement('div');
  2366. statusBar.classList.add('pdl-dlbar-status_bar');
  2367. dlBarRef.statusBar = dlBar.appendChild(statusBar);
  2368. const baseClasses = nav.querySelector('a:not([aria-current])').classList;
  2369. dlBarRef.abortBtn = dlBar.appendChild(createPdlBtn({
  2370. attrs: { 'pdl-userid': '' },
  2371. classList: [...baseClasses, 'pdl-stop', 'pdl-hide']
  2372. }, t('downloadBar.button.stop'), { addEvent: false }));
  2373. const dlBtn = createPdlBtn({
  2374. attrs: { 'pdl-userid': '' },
  2375. classList: [...baseClasses, 'pdl-btn-all']
  2376. }, t('downloadBar.button.works'), { addEvent: false });
  2377. dlBtn.addEventListener('click', downloadFollowLatest);
  2378. dlBar.appendChild(dlBtn);
  2379. const dlAllBtn = createPdlBtn({
  2380. attrs: { 'pdl-userid': '' },
  2381. classList: [...baseClasses, 'pdl-btn-all', 'pdl-dl-all']
  2382. }, t('downloadBar.button.works'), { addEvent: false });
  2383. dlAllBtn.addEventListener('click', downloadFollowLatest);
  2384. dlBar.appendChild(dlAllBtn);
  2385. navBar.insertBefore(dlBar, modeSwitch);
  2386. }
  2387. function changeDlbarDisplay() {
  2388. document.querySelectorAll('.pdl-dlbar .pdl-btn-all').forEach((ele) => {
  2389. ele.classList.toggle('pdl-hide');
  2390. });
  2391. document.querySelector('.pdl-dlbar .pdl-stop')?.classList.toggle('pdl-hide');
  2392. document.querySelectorAll('.pdl-tag').forEach((ele) => {
  2393. ele.classList.toggle('pdl-tag-hide');
  2394. });
  2395. document.querySelector('.pdl-filter-wrap')?.classList.toggle('unavailable');
  2396. }
  2397.  
  2398. function onProgressCB(progressData) {
  2399. if (typeof progressData === 'string') {
  2400. updateStatus(progressData);
  2401. }
  2402. else {
  2403. logger.info('Update progress by', progressData.illustId, ', completed: ', progressData.completed);
  2404. updateStatus(`Downloading: ${progressData.completed} / ${progressData.avaliable}`);
  2405. }
  2406. }
  2407. async function downloadByIds(total, idsGenerators, signal, onProgress) {
  2408. signal.throwIfAborted();
  2409. const failed = [];
  2410. const unavaliable = [];
  2411. const invalid = [];
  2412. const tasks = [];
  2413. let completed = 0;
  2414. let tooManyRequests = false;
  2415. let wakeTooManyRequest;
  2416. let wakeInterval;
  2417. let resolve;
  2418. let reject;
  2419. const done = new Promise((r, j) => {
  2420. resolve = r;
  2421. reject = j;
  2422. });
  2423. signal.addEventListener('abort', () => {
  2424. if (tasks.length) {
  2425. downloader.del(tasks);
  2426. tasks.length = 0;
  2427. }
  2428. wakeTooManyRequest?.();
  2429. wakeInterval?.();
  2430. reject(signal.aborted ? signal.reason : 'unexpected generator error');
  2431. }, { once: true });
  2432. const afterEach = (illustId) => {
  2433. const avaliable = total - failed.length - unavaliable.length - invalid.length;
  2434. onProgress({
  2435. illustId,
  2436. avaliable,
  2437. completed
  2438. });
  2439. if (completed === avaliable) {
  2440. resolve({ failed, unavaliable });
  2441. }
  2442. };
  2443. onProgress('Downloading...');
  2444. try {
  2445. for (const idsGenerator of idsGenerators) {
  2446. for await (const ids of idsGenerator) {
  2447. logger.info('Got ids:', ids);
  2448. signal.throwIfAborted();
  2449. if (ids.unavaliable.length) {
  2450. unavaliable.push(...ids.unavaliable);
  2451. }
  2452. if (ids.invalid.length) {
  2453. invalid.push(...ids.invalid);
  2454. }
  2455. if (typeof ids.total === 'number' && !Number.isNaN(ids.total)) {
  2456. total = ids.total;
  2457. }
  2458. if (ids.avaliable.length) {
  2459. for (const id of ids.avaliable) {
  2460. signal.throwIfAborted();
  2461. if (tooManyRequests) {
  2462. onProgress('Too many requests, wait 30s');
  2463. const { wake, sleep } = wakeableSleep(30000);
  2464. wakeTooManyRequest = wake;
  2465. await sleep;
  2466. signal.throwIfAborted();
  2467. tooManyRequests = false;
  2468. onProgress('Downloading...');
  2469. }
  2470. parser
  2471. .getAjaxArtworkData(id)
  2472. .then((data) => {
  2473. signal.throwIfAborted();
  2474. const sources = parser.getDownloadSource(data);
  2475. tasks.push(sources[0].taskId);
  2476. return downloader.add(sources);
  2477. })
  2478. .then((taskId) => {
  2479. pixivHistory.add(id);
  2480. if (!signal.aborted) {
  2481. tasks.splice(tasks.indexOf(taskId), 1);
  2482. completed++;
  2483. afterEach(id);
  2484. }
  2485. }, (reason) => {
  2486. if (!signal.aborted) {
  2487. reason && logger.error(reason);
  2488. if (reason instanceof RequestError && reason.response.status === 429) {
  2489. tooManyRequests = true;
  2490. }
  2491. if (reason instanceof JsonDataError) {
  2492. unavaliable.push(id);
  2493. }
  2494. else {
  2495. failed.push(id);
  2496. }
  2497. afterEach(id);
  2498. }
  2499. });
  2500. const { wake, sleep } = wakeableSleep(1000);
  2501. wakeInterval = wake;
  2502. await sleep;
  2503. }
  2504. }
  2505. else {
  2506. afterEach('no avaliable id');
  2507. }
  2508. }
  2509. }
  2510. }
  2511. catch (error) {
  2512. if (!signal.aborted) {
  2513. done.catch((reason) => {
  2514. logger.info('catch unexpected abort: ', reason);
  2515. });
  2516. signal.dispatchEvent(new Event('abort'));
  2517. throw error;
  2518. }
  2519. }
  2520. return done;
  2521. }
  2522. let isDownloading = false;
  2523. async function useDownloadBar(chunksGenerators) {
  2524. if (!dlBarRef.abortBtn)
  2525. return;
  2526. let total = 0;
  2527. let failedResult;
  2528. const idsGenerators = [];
  2529. !Array.isArray(chunksGenerators) && (chunksGenerators = [chunksGenerators]);
  2530. isDownloading = true;
  2531. changeDlbarDisplay();
  2532. try {
  2533. await Promise.all(chunksGenerators).then((gens) => {
  2534. gens.forEach((val) => {
  2535. total += val.total;
  2536. idsGenerators.push(val.generator);
  2537. });
  2538. });
  2539. }
  2540. catch (error) {
  2541. logger.error(error);
  2542. updateStatus('Network error, see console');
  2543. changeDlbarDisplay();
  2544. isDownloading = false;
  2545. return;
  2546. }
  2547. if (total === 0) {
  2548. updateStatus('No works');
  2549. }
  2550. else {
  2551. try {
  2552. logger.info('Total works:', total);
  2553. const controller = new AbortController();
  2554. const signal = controller.signal;
  2555. !signal.throwIfAborted &&
  2556. (signal.throwIfAborted = function () {
  2557. if (this.aborted) {
  2558. throw this.reason;
  2559. }
  2560. });
  2561. if (!('reason' in signal)) {
  2562. const abort = controller.abort;
  2563. controller.abort = function (reason) {
  2564. this.signal.reason = reason ? reason : new DOMException('signal is aborted without reason');
  2565. abort.apply(this);
  2566. };
  2567. }
  2568. dlBarRef.abortBtn?.addEventListener('click', () => {
  2569. controller.abort();
  2570. }, { once: true });
  2571. const { failed, unavaliable } = await downloadByIds(total, idsGenerators, signal, onProgressCB);
  2572. if (failed.length || unavaliable.length) {
  2573. updateStatus(`Failed: ${failed.length + unavaliable.length}. See console.`);
  2574. console.log('[Pixiv Downloader] Failed: ', failed.join(', '));
  2575. console.log('[Pixiv Downloader] Unavaliable: ', unavaliable.join(', '));
  2576. if (failed.length)
  2577. failedResult = failed;
  2578. }
  2579. else {
  2580. console.log('[Pixiv Downloader] Download complete');
  2581. updateStatus('Complete');
  2582. }
  2583. }
  2584. catch (error) {
  2585. if (error instanceof DOMException) {
  2586. updateStatus('Stop');
  2587. }
  2588. else {
  2589. updateStatus('Error, see console');
  2590. logger.error(error);
  2591. }
  2592. }
  2593. }
  2594. changeDlbarDisplay();
  2595. isDownloading = false;
  2596. return failedResult;
  2597. }
  2598.  
  2599. function createTagsBtn(userId, category) {
  2600. const tagsEles = document.querySelectorAll('section> div:nth-child(2) > div > div');
  2601. if (!tagsEles.length)
  2602. return;
  2603. let cate;
  2604. if (category === 'illustrations' || category === 'artworks') {
  2605. cate = 'illusts';
  2606. }
  2607. else {
  2608. cate = category;
  2609. }
  2610. let rest = 'show';
  2611. if (userId === getSelfId() && category === 'bookmarks' && location.search.includes('rest=hide'))
  2612. rest = 'hide';
  2613. tagsEles.forEach((ele) => {
  2614. const tagBtn = ele.querySelector('.pdl-btn');
  2615. if (tagBtn) {
  2616. const btnRest = tagBtn.getAttribute('rest');
  2617. if (rest !== btnRest)
  2618. tagBtn.setAttribute('rest', rest);
  2619. return;
  2620. }
  2621. let tag;
  2622. const tagLink = ele.querySelector('a');
  2623. if (!tagLink)
  2624. return;
  2625. if (tagLink.getAttribute('status') !== 'active') {
  2626. if (rest === 'hide') {
  2627. tag = tagLink.href.slice(tagLink.href.lastIndexOf('/') + 1, tagLink.href.lastIndexOf('?'));
  2628. }
  2629. else {
  2630. tag = tagLink.href.slice(tagLink.href.lastIndexOf('/') + 1);
  2631. }
  2632. }
  2633. else {
  2634. const tagTextEles = ele.querySelectorAll('div[title]');
  2635. if (!tagTextEles.length)
  2636. return logger.info('No Tags Element found.');
  2637. tag = tagTextEles[tagTextEles.length - 1].getAttribute('title').slice(1);
  2638. }
  2639. const attrs = {
  2640. attrs: { 'pdl-userId': userId, category: cate, tag, rest },
  2641. classList: ['pdl-btn', 'pdl-tag']
  2642. };
  2643. if (isDownloading)
  2644. attrs.classList.push('pdl-tag-hide');
  2645. const dlBtn = createPdlBtn(attrs, '', { addEvent: false });
  2646. if (!(tagLink.href.includes('bookmarks') && tagLink.getAttribute('status') !== 'active')) {
  2647. dlBtn.style.backgroundColor = tagLink.getAttribute('color') + '80';
  2648. }
  2649. dlBtn.addEventListener('click', downloadBookmarksOrTags);
  2650. ele.appendChild(dlBtn);
  2651. });
  2652. let modalTagsEles;
  2653. let modal;
  2654. if (category === 'bookmarks') {
  2655. modal = document.querySelector('div[role="presentation"]');
  2656. if (!modal)
  2657. return;
  2658. modalTagsEles = modal.querySelectorAll('a');
  2659. }
  2660. else {
  2661. const charcoalTokens = document.querySelectorAll('.charcoal-token');
  2662. modal = charcoalTokens[charcoalTokens.length - 1];
  2663. if (!modal)
  2664. return;
  2665. modalTagsEles = modal.querySelectorAll('a');
  2666. }
  2667. if (!regexp.userPageTags.exec(modalTagsEles[0]?.href))
  2668. return;
  2669. modalTagsEles.forEach((ele) => {
  2670. if (ele.querySelector('.pdl-btn'))
  2671. return;
  2672. let tag;
  2673. if (rest === 'hide') {
  2674. tag = ele.href.slice(ele.href.lastIndexOf('/') + 1, ele.href.lastIndexOf('?'));
  2675. }
  2676. else {
  2677. tag = ele.href.slice(ele.href.lastIndexOf('/') + 1);
  2678. }
  2679. const attrs = {
  2680. attrs: { 'pdl-userId': userId, category: cate, tag, rest },
  2681. classList: ['pdl-btn', 'pdl-modal-tag']
  2682. };
  2683. if (isDownloading)
  2684. attrs.classList.push('pdl-tag-hide');
  2685. const dlBtn = createPdlBtn(attrs, '', { addEvent: false });
  2686. dlBtn.addEventListener('click', (evt) => {
  2687. modal.querySelector('svg').parentElement.click();
  2688. downloadBookmarksOrTags(evt);
  2689. });
  2690. ele.appendChild(dlBtn);
  2691. });
  2692. }
  2693.  
  2694. function createToolbarBtn(id) {
  2695. if (document.querySelector('.pdl-btn-main'))
  2696. return;
  2697. const handleBar = document.querySelector('main section section');
  2698. if (handleBar) {
  2699. const pdlBtnWrap = handleBar.lastElementChild.cloneNode();
  2700. const attrs = {
  2701. attrs: { 'pdl-id': id },
  2702. classList: ['pdl-btn', 'pdl-btn-main']
  2703. };
  2704. if (pixivHistory.has(id))
  2705. attrs.classList.push('pdl-complete');
  2706. pdlBtnWrap.appendChild(createPdlBtn(attrs));
  2707. handleBar.appendChild(pdlBtnWrap);
  2708. }
  2709. }
  2710.  
  2711. function createWorkScrollBtn(id) {
  2712. const works = document.querySelectorAll("[role='presentation'] > a");
  2713. if (works.length < 2)
  2714. return;
  2715. const containers = Array.from(works).map((node) => node.parentElement.parentElement);
  2716. if (containers[0].querySelector('.pdl-btn'))
  2717. return;
  2718. containers.forEach((node, idx) => {
  2719. const wrapper = document.createElement('div');
  2720. wrapper.classList.add('pdl-wrap-artworks');
  2721. const attrs = {
  2722. attrs: { 'pdl-id': id, 'should-download': String(idx) },
  2723. classList: ['pdl-btn', 'pdl-btn-sub', 'artworks']
  2724. };
  2725. wrapper.appendChild(createPdlBtn(attrs));
  2726. node.appendChild(wrapper);
  2727. });
  2728. }
  2729.  
  2730. const createPresentationBtn = (() => {
  2731. let observer, btn;
  2732. function cb(mutationList) {
  2733. const newImg = mutationList[1]['addedNodes'][0];
  2734. const [pageNum] = regexp.originSrcPageNum.exec(newImg.src) ?? [];
  2735. if (!pageNum)
  2736. throw new Error('[Error]Invalid Image Element.');
  2737. btn?.setAttribute('should-download', String(pageNum));
  2738. }
  2739. return (id) => {
  2740. const containers = document.querySelector("body > [role='presentation'] > div");
  2741. if (!containers) {
  2742. if (observer) {
  2743. observer.disconnect();
  2744. observer = null;
  2745. btn = null;
  2746. }
  2747. return;
  2748. }
  2749. if (containers.querySelector('.pdl-btn'))
  2750. return;
  2751. const img = containers.querySelector('img');
  2752. if (!img)
  2753. return;
  2754. const isOriginImg = regexp.originSrcPageNum.exec(img.src);
  2755. if (!isOriginImg)
  2756. return;
  2757. const [pageNum] = isOriginImg;
  2758. const attrs = {
  2759. attrs: { 'pdl-id': id, 'should-download': pageNum },
  2760. classList: ['pdl-btn', 'pdl-btn-sub', 'presentation']
  2761. };
  2762. btn = createPdlBtn(attrs);
  2763. containers.appendChild(btn);
  2764. if (!img.parentElement)
  2765. return;
  2766. observer = new MutationObserver(cb);
  2767. observer.observe(img.parentElement, { childList: true, subtree: true });
  2768. };
  2769. })();
  2770.  
  2771. function createPreviewModalBtn() {
  2772. const illustModalBtn = document.querySelectorAll('.gtm-manga-viewer-preview-modal-open');
  2773. const mangaModalBtn = document.querySelectorAll('.gtm-manga-viewer-open-preview');
  2774. const mangaViewerModalBtn = document.querySelectorAll('.gtm-manga-viewer-close-icon')?.[1];
  2775. if (!illustModalBtn.length && !mangaModalBtn.length)
  2776. return;
  2777. const btns = [...illustModalBtn, ...mangaModalBtn];
  2778. if (mangaViewerModalBtn)
  2779. btns.push(mangaViewerModalBtn);
  2780. btns.forEach((node) => {
  2781. node.addEventListener('click', handleModalClick);
  2782. });
  2783. }
  2784. function handleModalClick() {
  2785. const timer = setInterval(() => {
  2786. logger.info('Start to find modal.');
  2787. const ulList = document.querySelectorAll('ul');
  2788. const previewList = ulList[ulList.length - 1];
  2789. if (getComputedStyle(previewList).display !== 'grid')
  2790. return;
  2791. clearInterval(timer);
  2792. const [, id] = regexp.artworksPage.exec(location.pathname) ?? [];
  2793. previewList.childNodes.forEach((node, idx) => {
  2794. node.style.position = 'relative';
  2795. const attrs = {
  2796. attrs: { 'pdl-id': id, 'should-download': String(idx) },
  2797. classList: ['pdl-btn', 'pdl-btn-sub']
  2798. };
  2799. node.appendChild(createPdlBtn(attrs));
  2800. });
  2801. }, 300);
  2802. }
  2803.  
  2804. let firstRun = true;
  2805. function observerCallback(records) {
  2806. const addedNodes = [];
  2807. records.forEach((record) => {
  2808. if (!record.addedNodes.length)
  2809. return;
  2810. record.addedNodes.forEach((node) => {
  2811. if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'BUTTON' && node.tagName !== 'IMG') {
  2812. addedNodes.push(node);
  2813. }
  2814. });
  2815. });
  2816. if (!addedNodes.length)
  2817. return;
  2818. if (firstRun) {
  2819. createThumbnailsBtn(document.querySelectorAll('a'));
  2820. firstRun = false;
  2821. }
  2822. else {
  2823. fixPixivPreviewer(addedNodes);
  2824. const thunmnails = addedNodes.reduce((prev, current) => {
  2825. return prev.concat(Array.from(current.querySelectorAll('a')));
  2826. }, []);
  2827. createThumbnailsBtn(thunmnails);
  2828. }
  2829. const isArtworksPage = regexp.artworksPage.exec(location.pathname);
  2830. const isUserPage = regexp.userPage.exec(location.pathname);
  2831. const isTagsPage = regexp.userPageTags.exec(location.pathname);
  2832. if (isArtworksPage) {
  2833. const id = isArtworksPage[1];
  2834. createToolbarBtn(id);
  2835. createWorkScrollBtn(id);
  2836. createPresentationBtn(id);
  2837. createPreviewModalBtn();
  2838. }
  2839. else if (isUserPage) {
  2840. createDownloadBar(isUserPage[1]);
  2841. if (isTagsPage) {
  2842. createTagsBtn(isUserPage[1], isTagsPage[1]);
  2843. }
  2844. }
  2845. else if (regexp.followLatest.test(location.pathname)) {
  2846. createFollowLatestDownloadBar();
  2847. }
  2848. else {
  2849. removeDownloadBar();
  2850. }
  2851. }
  2852.  
  2853. function createModal(args, option = { closeOnClickModal: false }) {
  2854. const modalHtml = `
  2855. <div class="pdl-modal">
  2856. <div class="pdl-dialog">
  2857. <button class="pdl-dialog-close"></button>
  2858. <header class="pdl-dialog-header"></header>
  2859. <div class="pdl-dialog-content"></div>
  2860. <footer class="pdl-dialog-footer"></footer>
  2861. </div>
  2862. </div>`;
  2863. const fragment = stringToFragment(modalHtml);
  2864. const modal = fragment.querySelector('.pdl-modal');
  2865. const dialog = modal.querySelector('.pdl-dialog');
  2866. const closeBtn = modal.querySelector('.pdl-dialog-close');
  2867. const header = modal.querySelector('.pdl-dialog-header');
  2868. const content = modal.querySelector('.pdl-dialog-content');
  2869. const footer = modal.querySelector('.pdl-dialog-footer');
  2870. if (option.closeOnClickModal) {
  2871. dialog.addEventListener('click', (e) => {
  2872. e.stopPropagation();
  2873. });
  2874. modal.addEventListener('click', () => {
  2875. modal.remove();
  2876. });
  2877. }
  2878. closeBtn.addEventListener('click', () => {
  2879. modal.remove();
  2880. });
  2881. args.header && header.appendChild(args.header);
  2882. args.footer && footer.appendChild(args.footer);
  2883. content.appendChild(args.content);
  2884. return fragment;
  2885. }
  2886.  
  2887. function showUpgradeMsg() {
  2888. const headerHtml = `<h3>Pixiv Downloader ${config.get('version')}</h3>`;
  2889. const contentHtml = `
  2890. <div class="pdl-changelog">
  2891. <ul>
  2892. <li>
  2893. <p>文件名现在可以使用 {date} 显示图片发布时间。</p>
  2894. <p>默认格式为YYYY-MM-DD,你也可以自定义格式,如: </p>
  2895. <p>{date(MMM D, YYYY)}, {date(hmm A)}, 更多可用占位符<a target="_blank" style="color: #0096fa; text-decoration: underline" href="https://dayjs.gitee.io/docs/zh-CN/display/format">见此</a>。</p>
  2896. </li>
  2897. <li>修复了部分低版本浏览器无法批量下载的问题。</li>
  2898. </ul>
  2899. </div>`;
  2900. const footerHtml = `
  2901. <style>
  2902. .pdl-dialog-footer {
  2903. position: relative;
  2904. font-size: 12px;
  2905. }
  2906. </style>
  2907. <details style="margin-top: 1.5em">
  2908. <summary style="display: inline-block; list-style: none; cursor: pointer; color: #0096fa; text-decoration: underline">
  2909. 脚本还行?请我喝杯可乐吧!
  2910. </summary>
  2911. ${creditCode}
  2912. <p style="text-align: center">愿你每天都能找到对的色图,就像我每天都能喝到香草味可乐</p>
  2913. </details>
  2914. <a
  2915. target="_blank"
  2916. style="position: absolute; right: 0px; top: 0px; color: #0096fa; text-decoration: underline"
  2917. href="https://greasyfork.org/zh-CN/scripts/432150-pixiv-downloader/feedback"
  2918. >${t('modals.upgradeMsg.feedback')}
  2919. </a>`;
  2920. document.body.appendChild(createModal({
  2921. header: stringToFragment(headerHtml),
  2922. content: stringToFragment(contentHtml),
  2923. footer: stringToFragment(footerHtml)
  2924. }));
  2925. }
  2926.  
  2927. function createTabUgoria() {
  2928. const tabHtml = `<div class="pdl-tab-item">${t('modals.setting.ugoria.tab_title')}</div>`;
  2929. const paneHtml = `
  2930. <div class="pdl-tab-pane">
  2931. <div id="pdl-setting-ugoria">
  2932. <p class="option-header">${t('modals.setting.ugoria.format_label')}</p>
  2933. <div id="pdl-ugoria-format-wrap">
  2934. <div class="pdl-ugoria-format-item">
  2935. <input type="radio" id="pdl-ugoria-zip" value="zip" name="format" /><label for="pdl-ugoria-zip">Zip</label>
  2936. </div>
  2937. <div class="pdl-ugoria-format-item">
  2938. <input type="radio" id="pdl-ugoria-gif" value="gif" name="format" /><label for="pdl-ugoria-gif">Gif</label>
  2939. </div>
  2940. <div class="pdl-ugoria-format-item">
  2941. <input type="radio" id="pdl-ugoria-apng" value="png" name="format" /><label for="pdl-ugoria-apng">Png</label>
  2942. </div>
  2943. <div class="pdl-ugoria-format-item">
  2944. <input type="radio" id="pdl-ugoria-webm" value="webm" name="format" /><label for="pdl-ugoria-webm">Webm</label>
  2945. </div>
  2946. <div class="pdl-ugoria-format-item">
  2947. <input type="radio" id="pdl-ugoria-webp" value="webp" name="format" /><label for="pdl-ugoria-webp">Webp</label>
  2948. </div>
  2949. </div>
  2950. </div>
  2951. </div>`;
  2952. const tab = stringToFragment(tabHtml);
  2953. const pane = stringToFragment(paneHtml);
  2954. const ugoriaFormat = config.get('ugoriaFormat');
  2955. pane.querySelectorAll('.pdl-ugoria-format-item input[type="radio"]').forEach((el) => {
  2956. if (ugoriaFormat === el.value)
  2957. el.checked = true;
  2958. el.addEventListener('change', (ev) => {
  2959. config.set('ugoriaFormat', ev.currentTarget.value);
  2960. });
  2961. });
  2962. return {
  2963. tab,
  2964. pane
  2965. };
  2966. }
  2967.  
  2968. function createTabFilename() {
  2969. const tabHtml = `<div class="pdl-tab-item">${t('modals.setting.filename.tab_title')}</div>`;
  2970. const paneHtml = `
  2971. <div class="pdl-tab-pane">
  2972. <div id="pdl-setting-filename">
  2973. <div>
  2974. <div class="pdl-input-wrap">
  2975. <label for="pdlfolder">${t('modals.setting.filename.input.folder_label')}</label>
  2976. <input type="text" id="pdlfolder" maxlength="100" />
  2977. <button id="pdl-filename-folder-reset" class="pdl-dialog-button icon" disabled>↺</button>
  2978. <button id="pdl-filename-folder-confirm" class="pdl-dialog-button icon primary" disabled>✓</button>
  2979. </div>
  2980. <div class="pdl-input-wrap">
  2981. <label for="pdlfilename">${t('modals.setting.filename.input.filename_label')}</label>
  2982. <input type="text" id="pdlfilename" placeholder="${t('modals.setting.filename.input.folder_placeholder')}" required maxlength="100" />
  2983. <button id="pdl-filename-filename-reset" class="pdl-dialog-button icon" disabled>↺</button>
  2984. <button id="pdl-filename-filename-confirm" class="pdl-dialog-button icon primary" disabled>✓</button>
  2985. </div>
  2986. </div>
  2987. <div class="tags-option">
  2988. <span class="tags-title">${t('modals.setting.filename.input.tag_label')}</span>
  2989. <div class="tags-content">
  2990. <div class="tags-item">
  2991. <input class="pdl-option-tag" type="radio" name="lang" id="lang_ja" value="ja" />
  2992. <label for="lang_ja">日本語(default)</label>
  2993. </div>
  2994. <div class="tags-item">
  2995. <input class="pdl-option-tag" type="radio" name="lang" id="lang_zh" value="zh" />
  2996. <label for="lang_zh">简中</label>
  2997. </div>
  2998. <div class="tags-item">
  2999. <input class="pdl-option-tag" type="radio" name="lang" id="lang_zh_tw" value="zh_tw" />
  3000. <label for="lang_zh_tw">繁中</label>
  3001. </div>
  3002. <div class="tags-item">
  3003. <input class="pdl-option-tag" type="radio" name="lang" id="lang_en" value="en" />
  3004. <label for="lang_en">English</label>
  3005. </div>
  3006. </div>
  3007. </div>
  3008. <p style="font-size: 14px; margin: 0.5em 0">${t('modals.setting.filename.tips.filename_pattern')}</p>
  3009. <p style="font-size: 14px; margin: 0.5em 0">${t('modals.setting.filename.tips.empty_folder')}</p>
  3010. <p style="font-size: 14px; margin: 0.5em 0">${t('modals.setting.filename.tips.tag_translation')}</p>
  3011. <hr />
  3012. <div ${env.isFileSystemAccessAvaliable() ? '' : 'class="pdl-unavailable"'}>
  3013. <div style="display: flex; justify-content: space-between; align-items: center; margin: 12px 0; gap: 12px">
  3014. <label class="pdl-options" style="padding: 0.6em 4px">
  3015. <span style="font-weight: 700">${t('modals.setting.filename.input.fsa_label')}</span>
  3016. <input id="pdl-options-file-system-access" type="checkbox" class="pdl-checkbox" style="margin-left: 8px" />
  3017. </label>
  3018. <hr class="vertical" />
  3019. <div class="pdl-input-wrap" style="flex: 1; margin: 0">
  3020. <input id="pdl-fsa-show-directory" type="text" placeholder="${t('modals.setting.filename.input.fsa_placeholder')}" style="font-size: 14px; padding: 8px 0.5em; line-height: 1.15" disabled/>
  3021. </div>
  3022. <button id="pdl-fsa-change-directory" class="pdl-dialog-button primary">${t('modals.setting.filename.button.fsa_change_dir')}</button>
  3023. </div>
  3024. <div class="tags-option">
  3025. <span class="tags-title">${t('modals.setting.filename.input.filename_conflict_label')}</span>
  3026. <div class="tags-content">
  3027. <div class="tags-item">
  3028. <input class="pdl-option-conflict" type="radio" name="conflict_action" id="action_uniquify" value="uniquify"/>
  3029. <label for="action_uniquify">${t('modals.setting.filename.input.filename_conflict_option_uniquify')}</label>
  3030. </div>
  3031. <div class="tags-item">
  3032. <input class="pdl-option-conflict" type="radio" name="conflict_action" id="action_overwrite" value="overwrite"/>
  3033. <label for="action_overwrite">${t('modals.setting.filename.input.filename_conflict_option_overwrite')}</label>
  3034. </div>
  3035. <div class="tags-item">
  3036. <input class="pdl-option-conflict" type="radio" name="conflict_action" id="action_prompt" value="prompt"/>
  3037. <label for="action_prompt">${t('modals.setting.filename.input.filename_conflict_option_prompt')}</label>
  3038. </div>
  3039. </div>
  3040. </div>
  3041. </div>
  3042. </div>
  3043. </div>`;
  3044. const tab = stringToFragment(tabHtml);
  3045. const pane = stringToFragment(paneHtml);
  3046. const folder = pane.querySelector('#pdlfolder');
  3047. const folderReset = pane.querySelector('#pdl-filename-folder-reset');
  3048. const folderUpdate = pane.querySelector('#pdl-filename-folder-confirm');
  3049. const filename = pane.querySelector('#pdlfilename');
  3050. const filenameReset = pane.querySelector('#pdl-filename-filename-reset');
  3051. const filenameUpdate = pane.querySelector('#pdl-filename-filename-confirm');
  3052. const filenamePattern = config.get('filenamePattern');
  3053. const folderPattern = config.get('folderPattern');
  3054. if (!folder || !filename)
  3055. throw new Error('[Error]Can not create modal.');
  3056. filename.value = filenamePattern;
  3057. if (!env.isSupportSubpath()) {
  3058. folder.setAttribute('disabled', '');
  3059. folder.value = '';
  3060. }
  3061. else {
  3062. folder.value = folderPattern;
  3063. }
  3064. folder.placeholder = env.isViolentmonkey()
  3065. ? t('modals.setting.filename.input.folder_placeholder_vm')
  3066. : !env.isSupportSubpath()
  3067. ? t('modals.setting.filename.input.folder_placeholder_need_api')
  3068. : t('modals.setting.filename.input.folder_placeholder');
  3069. folder.addEventListener('input', () => {
  3070. folderReset?.removeAttribute('disabled');
  3071. folderUpdate?.removeAttribute('disabled');
  3072. });
  3073. folderReset?.addEventListener('click', () => {
  3074. folder.value = config.get('folderPattern');
  3075. folderReset?.setAttribute('disabled', '');
  3076. folderUpdate?.setAttribute('disabled', '');
  3077. });
  3078. folderUpdate?.addEventListener('click', () => {
  3079. const folderPattern = folder.value
  3080. .split('/')
  3081. .map(replaceInvalidChar)
  3082. .filter((path) => !!path)
  3083. .join('/');
  3084. config.set('folderPattern', folderPattern);
  3085. folder.value = folderPattern;
  3086. folderReset?.setAttribute('disabled', '');
  3087. folderUpdate?.setAttribute('disabled', '');
  3088. });
  3089. filename.addEventListener('input', () => {
  3090. filenameReset?.removeAttribute('disabled');
  3091. filenameUpdate?.removeAttribute('disabled');
  3092. });
  3093. filenameReset?.addEventListener('click', () => {
  3094. filename.value = config.get('filenamePattern');
  3095. filenameReset?.setAttribute('disabled', '');
  3096. filenameUpdate?.setAttribute('disabled', '');
  3097. });
  3098. filenameUpdate?.addEventListener('click', () => {
  3099. const filenamePattern = replaceInvalidChar(filename.value);
  3100. if (filenamePattern === '')
  3101. return filenameReset?.click();
  3102. config.set('filenamePattern', filenamePattern);
  3103. filename.value = filenamePattern;
  3104. filenameReset?.setAttribute('disabled', '');
  3105. filenameUpdate?.setAttribute('disabled', '');
  3106. });
  3107. const tagLang = config.get('tagLang');
  3108. pane.querySelectorAll('.tags-content .tags-item input.pdl-option-tag').forEach((input) => {
  3109. if (tagLang === input.value)
  3110. input.checked = true;
  3111. input.addEventListener('change', (ev) => {
  3112. config.set('tagLang', ev.currentTarget.value);
  3113. });
  3114. });
  3115. if (env.isFileSystemAccessAvaliable()) {
  3116. const enableFSA = pane.querySelector('#pdl-options-file-system-access');
  3117. const showDir = pane.querySelector('#pdl-fsa-show-directory');
  3118. const changeDirBtn = pane.querySelector('#pdl-fsa-change-directory');
  3119. const actionInput = pane.querySelectorAll('.tags-content .tags-item input.pdl-option-conflict');
  3120. const interactElems = [changeDirBtn, ...actionInput];
  3121. const isUseFSA = config.get('useFileSystemAccess');
  3122. const conflictAction = config.get('fileSystemFilenameConflictAction');
  3123. if (isUseFSA) {
  3124. folder.placeholder = t('modals.setting.filename.input.folder_placeholder');
  3125. folder.removeAttribute('disabled');
  3126. folder.value = folderPattern;
  3127. }
  3128. enableFSA.checked = isUseFSA;
  3129. if (!isUseFSA) {
  3130. interactElems.forEach((el) => el.setAttribute('disabled', ''));
  3131. }
  3132. enableFSA.addEventListener('change', (ev) => {
  3133. const isEnabled = ev.target.checked;
  3134. config.set('useFileSystemAccess', isEnabled);
  3135. if (isEnabled) {
  3136. folder.placeholder = t('modals.setting.filename.input.folder_placeholder');
  3137. if (folder.hasAttribute('disabled')) {
  3138. folder.removeAttribute('disabled');
  3139. folder.value = config.get('folderPattern');
  3140. }
  3141. interactElems.forEach((el) => el.removeAttribute('disabled'));
  3142. }
  3143. else {
  3144. if (env.isViolentmonkey()) {
  3145. folder.placeholder = t('modals.setting.filename.input.folder_placeholder_vm');
  3146. folder.setAttribute('disabled', '');
  3147. folder.value = '';
  3148. }
  3149. else if (!env.isSupportSubpath()) {
  3150. folder.placeholder = t('modals.setting.filename.input.folder_placeholder_need_api');
  3151. folder.setAttribute('disabled', '');
  3152. folder.value = '';
  3153. }
  3154. interactElems.forEach((el) => el.setAttribute('disabled', ''));
  3155. }
  3156. });
  3157. showDir.value = getCurrentDirName();
  3158. changeDirBtn.addEventListener('click', async () => {
  3159. await updateDirHandle();
  3160. showDir.value = getCurrentDirName();
  3161. });
  3162. actionInput.forEach((input) => {
  3163. if (conflictAction === input.value)
  3164. input.checked = true;
  3165. input.addEventListener('change', (ev) => {
  3166. config.set('fileSystemFilenameConflictAction', ev.currentTarget.value);
  3167. });
  3168. });
  3169. }
  3170. return {
  3171. tab,
  3172. pane
  3173. };
  3174. }
  3175.  
  3176. function createTabAdjustButtonPosition() {
  3177. const style = getComputedStyle(document.documentElement);
  3178. const tabHtml = `<div class="pdl-tab-item">${t('modals.setting.button_pos.tab_title')}</div>`;
  3179. const paneHtml = `
  3180. <div class="pdl-tab-pane">
  3181. <div class="pdl-adjust-button">
  3182. <div class="pdl-adjust-content">
  3183. <datalist id="pdl-adjust-tickmarks">
  3184. <option value="0"></option>
  3185. <option value="25"></option>
  3186. <option value="50"></option>
  3187. <option value="75"></option>
  3188. <option value="100"></option>
  3189. </datalist>
  3190. <div class="pdl-adjust-item">
  3191. <p class="pdl-adjust-title">${t('modals.setting.button_pos.self_bookmark_title')}</p>
  3192. <div class="pdl-adjust-select">
  3193. <span>${t('modals.setting.button_pos.input.horizon_label')}</span
  3194. ><input
  3195. id="pdl-btn-self-bookmark-left"
  3196. type="range"
  3197. max="100"
  3198. min="0"
  3199. step="1"
  3200. list="pdl-adjust-tickmarks"
  3201. value="${style.getPropertyValue('--pdl-btn-self-bookmark-left')}"
  3202. />
  3203. </div>
  3204. <div class="pdl-adjust-select">
  3205. <span>${t('modals.setting.button_pos.input.vertical_label')}</span
  3206. ><input
  3207. id="pdl-btn-self-bookmark-top"
  3208. type="range"
  3209. max="100"
  3210. min="0"
  3211. step="1"
  3212. list="pdl-adjust-tickmarks"
  3213. value="${style.getPropertyValue('--pdl-btn-self-bookmark-top')}"
  3214. />
  3215. </div>
  3216. </div>
  3217. <div class="pdl-adjust-item">
  3218. <p class="pdl-adjust-title">${t('modals.setting.button_pos.preview_title')}</p>
  3219. <div class="pdl-adjust-select">
  3220. <span>${t('modals.setting.button_pos.input.horizon_label')}</span
  3221. ><input
  3222. id="pdl-btn-left"
  3223. type="range"
  3224. max="100"
  3225. min="0"
  3226. step="1"
  3227. list="pdl-adjust-tickmarks"
  3228. value="${style.getPropertyValue('--pdl-btn-left')}"
  3229. />
  3230. </div>
  3231. <div class="pdl-adjust-select">
  3232. <span>${t('modals.setting.button_pos.input.vertical_label')}</span
  3233. ><input
  3234. id="pdl-btn-top"
  3235. type="range"
  3236. max="100"
  3237. min="0"
  3238. step="1"
  3239. list="pdl-adjust-tickmarks"
  3240. value="${style.getPropertyValue('--pdl-btn-top')}"
  3241. />
  3242. </div>
  3243. </div>
  3244. </div>
  3245. <div class="pdl-adjust-preview">
  3246. <div class="pdl-thumbnail-sample">
  3247. <button class="pdl-btn pdl-btn-sub"></button>
  3248. <button class="pdl-btn pdl-btn-sub self-bookmark pdl-complete"></button>
  3249. </div>
  3250. </div>
  3251. </div>
  3252. </div>`;
  3253. const tab = stringToFragment(tabHtml);
  3254. const pane = stringToFragment(paneHtml);
  3255. pane.querySelectorAll('.pdl-adjust-select input[type="range"]').forEach((el) => {
  3256. el.addEventListener('input', (ev) => {
  3257. const el = ev.target;
  3258. const val = el.value;
  3259. document.documentElement.style.setProperty('--' + el.id, val);
  3260. });
  3261. el.addEventListener('change', (ev) => {
  3262. const el = ev.target;
  3263. const key = el.id;
  3264. config.set(key, el.value);
  3265. });
  3266. });
  3267. return {
  3268. tab,
  3269. pane
  3270. };
  3271. }
  3272.  
  3273. function createTabHistory() {
  3274. const tabHtml = `<div class="pdl-tab-item">${t('modals.setting.history.tab_title')}</div>`;
  3275. const paneHtml = `
  3276. <div class="pdl-tab-pane">
  3277. <div id="pdl-setting-history">
  3278. <div>
  3279. <button id="pdl-export" class="btn-history pdl-dialog-button primary">
  3280. ${t('modals.setting.history.button.export')}
  3281. </button>
  3282. </div>
  3283. <div>
  3284. <input type="file" id="pdl-import" accept=".txt" style="display: none" />
  3285. <button id="pdl-import-btn" class="btn-history pdl-dialog-button primary">
  3286. ${t('modals.setting.history.button.import')}
  3287. </button>
  3288. </div>
  3289. <div>
  3290. <input type="file" id="pdl-merge" accept=".txt" style="display: none" />
  3291. <button id="pdl-merge-btn" class="btn-history pdl-dialog-button primary">
  3292. ${t('modals.setting.history.button.merge')}
  3293. </button>
  3294. </div>
  3295. <div>
  3296. <button id="pdl-clear-history" class="btn-history pdl-dialog-button primary">
  3297. ${t('modals.setting.history.button.clear')}
  3298. </button>
  3299. </div>
  3300. </div>
  3301. </div>`;
  3302. const tab = stringToFragment(tabHtml);
  3303. const pane = stringToFragment(paneHtml);
  3304. const importFile = pane.querySelector('#pdl-import');
  3305. importFile?.addEventListener('change', (evt) => {
  3306. const file = evt.currentTarget.files?.[0];
  3307. if (!file)
  3308. return;
  3309. pixivHistory.replace(file);
  3310. });
  3311. const mergeFile = pane.querySelector('#pdl-merge');
  3312. mergeFile?.addEventListener('change', (evt) => {
  3313. const file = evt.currentTarget.files?.[0];
  3314. if (!file)
  3315. return;
  3316. pixivHistory.merge(file);
  3317. });
  3318. const importBtn = pane.querySelector('#pdl-import-btn');
  3319. importBtn?.addEventListener('click', () => importFile?.click());
  3320. const mergeBtn = pane.querySelector('#pdl-merge-btn');
  3321. mergeBtn?.addEventListener('click', () => mergeFile?.click());
  3322. const exportBtn = pane.querySelector('#pdl-export');
  3323. exportBtn?.addEventListener('click', () => {
  3324. const dlEle = document.createElement('a');
  3325. const history = JSON.stringify(pixivHistory.getAll());
  3326. dlEle.href = URL.createObjectURL(new Blob([history], { type: 'text/plain' }));
  3327. dlEle.download = 'Pixiv Downloader ' + new Date().toLocaleString() + '.txt';
  3328. dlEle.click();
  3329. URL.revokeObjectURL(dlEle.href);
  3330. });
  3331. const clearBtn = pane.querySelector('#pdl-clear-history');
  3332. clearBtn?.addEventListener('click', () => pixivHistory.clearHistory());
  3333. return {
  3334. tab,
  3335. pane
  3336. };
  3337. }
  3338.  
  3339. var css = ".pdl-popup-button {\n position: fixed;\n z-index: 1;\n right: 28px;\n bottom: 100px;\n padding: 12px;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n color: rgb(255, 255, 255);\n background-color: rgba(0, 150, 250, 0.5);\n transition: opacity 0.3s ease 0s;\n opacity: 0.32;\n line-height: 0;\n margin: 0;\n}\n\n.pdl-popup-button:hover {\n opacity: 1;\n}\n\n.pdl-popup-button svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n}\n";
  3340. n(css,{});
  3341.  
  3342. function showPopupBtn() {
  3343. if (document.querySelector('.pdl-popup-button'))
  3344. return;
  3345. const btn = document.createElement('button');
  3346. btn.className = 'pdl-popup-button';
  3347. btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></svg>`;
  3348. btn.addEventListener('click', () => {
  3349. showSettings();
  3350. });
  3351. document.body.appendChild(btn);
  3352. }
  3353. function removePopupBtn() {
  3354. document.querySelector('button.pdl-popup-button')?.remove();
  3355. }
  3356.  
  3357. function createTabOthers() {
  3358. const tabHtml = `<div class="pdl-tab-item">${t('modals.setting.others.tab_title')}</div>`;
  3359. const paneHtml = `
  3360. <div class="pdl-tab-pane">
  3361. <div id="pdl-setting-others">
  3362. <div>
  3363. <label class="pdl-options">
  3364. <input id="pdl-options-bundle-illusts" type="checkbox" class="pdl-checkbox" />
  3365. <span>${t('modals.setting.others.input.bundle_illusts')}</span>
  3366. </label>
  3367. </div>
  3368. <hr />
  3369. <div>
  3370. <label class="pdl-options">
  3371. <input id="pdl-options-bundle-manga" type="checkbox" class="pdl-checkbox" />
  3372. <span>${t('modals.setting.others.input.bundle_manga')}</span>
  3373. </label>
  3374. </div>
  3375. <hr />
  3376. <div>
  3377. <label class="pdl-options">
  3378. <input id="pdl-options-add-bookmark" type="checkbox" class="pdl-checkbox" />
  3379. <span>${t('modals.setting.others.input.add_bookmark')}</span>
  3380. </label>
  3381. </div>
  3382. <hr />
  3383. <div>
  3384. <label class="pdl-options sub-option">
  3385. <input id="pdl-options-add-bookmark-tags" type="checkbox" class="pdl-checkbox" />
  3386. <span>${t('modals.setting.others.input.add_bookmark_with_tags')}</span>
  3387. </label>
  3388. </div>
  3389. <hr class="sub" />
  3390. <div>
  3391. <label class="pdl-options sub-option">
  3392. <input id="pdl-options-add-bookmark-private-r18" type="checkbox" class="pdl-checkbox" />
  3393. <span>${t('modals.setting.others.input.add_bookmark_private_r18')}</span>
  3394. </label>
  3395. </div>
  3396. <hr />
  3397. <div>
  3398. <label class="pdl-options">
  3399. <input id="pdl-options-show-popup-button" type="checkbox" class="pdl-checkbox" />
  3400. <span>${t('modals.setting.others.input.show_popup_button')}</span>
  3401. </label>
  3402. </div>
  3403. </div>
  3404. </div>`;
  3405. const tab = stringToFragment(tabHtml);
  3406. const pane = stringToFragment(paneHtml);
  3407. [
  3408. { selector: '#pdl-options-bundle-illusts', settingKey: 'bundleIllusts' },
  3409. { selector: '#pdl-options-bundle-manga', settingKey: 'bundleManga' },
  3410. { selector: '#pdl-options-add-bookmark', settingKey: 'addBookmark' },
  3411. {
  3412. selector: '#pdl-options-add-bookmark-tags',
  3413. settingKey: 'addBookmarkWithTags'
  3414. },
  3415. {
  3416. selector: '#pdl-options-add-bookmark-private-r18',
  3417. settingKey: 'privateR18'
  3418. },
  3419. { selector: '#pdl-options-show-popup-button', settingKey: 'showPopupButton' }
  3420. ].forEach(({ selector, settingKey }) => {
  3421. const optionEl = pane.querySelector(selector);
  3422. if (!optionEl)
  3423. return;
  3424. optionEl.checked = config.get(settingKey);
  3425. optionEl.addEventListener('change', (ev) => {
  3426. config.set(settingKey, ev.currentTarget.checked);
  3427. });
  3428. });
  3429. pane.querySelector('#pdl-options-show-popup-button').addEventListener('change', (ev) => {
  3430. if (ev.currentTarget.checked) {
  3431. showPopupBtn();
  3432. }
  3433. else {
  3434. removePopupBtn();
  3435. }
  3436. });
  3437. return {
  3438. tab,
  3439. pane
  3440. };
  3441. }
  3442.  
  3443. function createTabFeedback() {
  3444. const tabHtml = `<div class="pdl-tab-item">${t('modals.setting.feedback.tab_title')}</div>`;
  3445. const paneHtml = `
  3446. <div class="pdl-tab-pane">
  3447. <div id="pdl-setting-donate">
  3448. ${creditCode}
  3449. <p>如果脚本有帮助到你,欢迎扫码请我喝杯可乐 ^_^</p>
  3450. <p>
  3451. <a
  3452. target="_blank"
  3453. style="color: #0096fa; text-decoration: underline"
  3454. href="https://greasyfork.org/zh-CN/scripts/432150-pixiv-downloader/feedback"
  3455. >${t('modals.setting.feedback.tips.feedback')}</a
  3456. >
  3457. </p>
  3458. </div>
  3459. </div>`;
  3460. return {
  3461. tab: stringToFragment(tabHtml),
  3462. pane: stringToFragment(paneHtml)
  3463. };
  3464. }
  3465.  
  3466. function showSettings() {
  3467. if (document.querySelector('.pdl-modal'))
  3468. return;
  3469. const contentHtml = `
  3470. <div>
  3471. <div class="pdl-tabs-nav">
  3472. <div class="pdl-tabs__active-bar"></div>
  3473. </div>
  3474. <div class="pdl-tabs-content"></div>
  3475. </div>`;
  3476. const modal = createModal({
  3477. content: stringToFragment(contentHtml)
  3478. });
  3479. const tabsNav = modal.querySelector('.pdl-tabs-nav');
  3480. const tabContent = modal.querySelector('.pdl-tabs-content');
  3481. [
  3482. createTabFilename(),
  3483. createTabUgoria(),
  3484. createTabHistory(),
  3485. createTabAdjustButtonPosition(),
  3486. createTabOthers(),
  3487. createTabFeedback()
  3488. ].forEach(({ tab, pane }) => {
  3489. tabsNav.appendChild(tab);
  3490. tabContent.appendChild(pane);
  3491. });
  3492. const panes = Array.from(tabContent.querySelectorAll('.pdl-tab-pane'));
  3493. panes.forEach((el) => {
  3494. el.style.setProperty('display', 'none');
  3495. });
  3496. const activeBar = tabsNav.querySelector('.pdl-tabs__active-bar');
  3497. const tabs = Array.from(modal.querySelectorAll('.pdl-tabs-nav .pdl-tab-item'));
  3498. tabs.forEach((el) => {
  3499. el.addEventListener('click', (ev) => {
  3500. const tab = ev.currentTarget;
  3501. if (!tab)
  3502. return;
  3503. tabs.forEach((tab) => tab.classList.remove('active'));
  3504. tab.classList.add('active');
  3505. activeBar.style.width = getComputedStyle(tab).width;
  3506. activeBar.style.transform = `translateX(${tab.offsetLeft + parseFloat(getComputedStyle(tab).paddingLeft)}px)`;
  3507. panes.forEach((pane) => pane.style.setProperty('display', 'none'));
  3508. panes[tabs.indexOf(tab)].style.removeProperty('display');
  3509. });
  3510. });
  3511. tabs[0].classList.add('active');
  3512. panes[0].style.removeProperty('display');
  3513. document.body.appendChild(modal);
  3514. activeBar.style.width = getComputedStyle(tabs[0]).width;
  3515. activeBar.style.transform = `translateX(${tabs[0].offsetLeft + parseFloat(getComputedStyle(tabs[0]).paddingLeft)}px)`;
  3516. }
  3517.  
  3518. pixivHistory.updateHistory();
  3519. GM_registerMenuCommand(t('gm_menu.setting'), showSettings, 's');
  3520. if (config.get('showMsg')) {
  3521. showUpgradeMsg();
  3522. config.set('showMsg', false);
  3523. }
  3524. if (config.get('showPopupButton')) {
  3525. showPopupBtn();
  3526. }
  3527. ['pdl-btn-self-bookmark-left', 'pdl-btn-self-bookmark-top', 'pdl-btn-left', 'pdl-btn-top'].forEach((key) => {
  3528. let val;
  3529. if ((val = config.get(key)) !== undefined) {
  3530. document.documentElement.style.setProperty('--' + key, val);
  3531. }
  3532. });
  3533. new MutationObserver(observerCallback).observe(document.body, {
  3534. childList: true,
  3535. subtree: true
  3536. });
  3537. document.addEventListener('keydown', (e) => {
  3538. if (e.ctrlKey && e.key === 'q') {
  3539. const pdlMainBtn = document.querySelector('.pdl-btn-main');
  3540. if (pdlMainBtn) {
  3541. e.preventDefault();
  3542. if (!e.repeat) {
  3543. pdlMainBtn.dispatchEvent(new MouseEvent('click'));
  3544. }
  3545. }
  3546. }
  3547. });
  3548.  
  3549. })(workerChunk, GIF, JSZip, dayjs);