Pixiv Downloader

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

Mint 2024.03.05.. Lásd a legutóbbi verzió

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