您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键(快捷键)下载Pixiv各页面原图。支持多图下载,动图下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。
当前为
// ==UserScript== // @name Pixiv Downloader // @namespace https://greasyfork.org/zh-CN/scripts/432150 // @version 0.4.0 // @description 一键(快捷键)下载Pixiv各页面原图。支持多图下载,动图下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。 // @author ruaruarua // @match https://www.pixiv.net/* // @icon https://www.pixiv.net/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect i.pximg.net // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js // ==/UserScript== (async function () { let convertFormat = localStorage.pdlFormat || (localStorage.pdlFormat = 'zip'); let gifWS, apngWS; if (!(gifWS = await GM_getValue('gifWS'))) { gifWS = await fetch('https://raw.githubusercontent.com/jnordberg/gif.js/master/dist/gif.worker.js').then((res) => res.blob()).then((blob) => blob.text()); GM_setValue('gifWS', gifWS); } if (!(apngWS = await GM_getValue('apngWS'))) { const pako = await fetch('https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.4/pako.min.js').then((res) => res.text()); const upng = await fetch('https://cdnjs.cloudflare.com/ajax/libs/upng-js/2.1.0/UPNG.min.js').then((res) => res.text()).then((js) => js.replace('window.UPNG', 'UPNG').replace('window.pako', 'pako')); const workerEvt = `onmessage = (evt)=>{ const {data, width, height, delay }= evt.data; const png = UPNG.encode(data, width, height, 0, delay, {loop: 0}); if (!png) console.log('Convert Apng failed.'); postMessage(png); };`; apngWS = workerEvt + pako + upng; GM_setValue('apngWS', apngWS); } gifWS = URL.createObjectURL(new Blob([gifWS], { type: 'text/javascript' })); apngWS = URL.createObjectURL(new Blob([apngWS], { type: 'text/javascript' })); const converter = (() => { const zip = new JSZip(); const gifConfig = { workers: 2, quality: 10, workerScript: gifWS, }; const freeApngWorkers = []; const apngWorkers = []; const MAX_CONVERT = 2; let queue = []; let active = []; let isStop = false; const convert = (convertMeta) => { const { blob, meta, callBack } = convertMeta; meta.abort = () => void (meta.state = 0); active.push(convertMeta); meta.onProgress && meta.onProgress(0, 'zip'); zip.folder(meta.id) .loadAsync(blob) .then((zip) => { const promises = []; zip.forEach((relativePath, file) => { promises.push(new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { resolve(image); }; file.async('blob') .then((blob) => void (image.src = URL.createObjectURL(blob))); })); }); return Promise.all(promises); }) .then((imgList) => { zip.remove(meta.id); if (!meta.state) throw 'Convert stop manually, reject when unzip. ' + meta.id; if (convertFormat === 'zip') throw 'no need to convert'; return convert2[convertFormat](imgList, meta, callBack); }) .catch((err) => { meta.reject('Error when converting. ' + err); }) .finally(() => { active.splice(active.indexOf(convertMeta), 1); if (queue.length) convert(queue.shift()); }); }; const toGif = (frames, meta, callBack) => { return new Promise((resolve, reject) => { let gif = new GIF(gifConfig); meta.abort = () => { meta.state = 0; gif.abort(); reject('Convert stop manually, reject when convert gif. ' + meta.id); }; debugLog('Convert:', meta.path); frames.forEach((frame, i) => { gif.addFrame(frame, { delay: meta.ugoiraMeta.frames[i].delay }); }); gif.on('progress', (() => { const type = 'gif'; return (progress) => { debugLog('Convert progress:', meta.path, progress); meta.onProgress && meta.onProgress(progress, type); }; })()); gif.on('finished', (gifBlob) => { if (typeof callBack == 'function') callBack(gifBlob, meta); frames.forEach((frame) => { URL.revokeObjectURL(frame.src); }); gif = null; resolve(); }); gif.on('abort', () => { gif = null; }); gif.render(); }); }; const toApng = (frames, meta, callBack) => { return new Promise((resolve, reject) => { let canvas = document.createElement('canvas'); const width = canvas.width = frames[0].naturalWidth; const height = canvas.height = frames[0].naturalHeight; const context = canvas.getContext('2d'); const data = []; const delay = meta.ugoiraMeta.frames.map((frame) => { return Number(frame.delay); }); frames.forEach((frame) => { if (!meta.state) throw 'Convert stop manually, reject when drawImage. ' + meta.id; context.clearRect(0, 0, width, height); context.drawImage(frame, 0, 0, width, height); data.push(context.getImageData(0, 0, width, height).data); }); canvas = null; debugLog('Convert:', meta.path, apngWorkers.length); let worker; if (apngWorkers.length === MAX_CONVERT) { worker = freeApngWorkers.shift(); } else { worker = new Worker(apngWS); apngWorkers.push(worker); } meta.abort = () => { meta.state = 0; reject('Convert stop manually, reject when convert apng. ' + meta.id); worker.terminate(); apngWorkers.splice(apngWorkers.indexOf(worker), 1); debugLog('abort: apngWorkers.length', apngWorkers.length); }; worker.onmessage = function (e) { if (queue.length) { freeApngWorkers.push(worker); } else { worker.terminate(); apngWorkers.splice(apngWorkers.indexOf(worker), 1); debugLog('complete: apngWorkers.length', apngWorkers.length); } if (!e.data) { return reject('apng data is null. ' + meta.id); } const pngBlob = new Blob([e.data], { type: 'image/png' }); if (typeof callBack == 'function') callBack(pngBlob, meta); resolve(); }; const cfg = { data, width, height, delay }; worker.postMessage(cfg); }) }; const toWebm = (frames, meta, callBack) => { return new Promise((resolve, reject) => { let canvas = document.createElement('canvas'); const width = canvas.width = frames[0].naturalWidth; const height = canvas.height = frames[0].naturalHeight; const context = canvas.getContext('2d'); const stream = canvas.captureStream(); const recorder = new MediaRecorder(stream, { mimeType: 'video/webm', videoBitsPerSecond: 80000000 }); const delay = meta.ugoiraMeta.frames.map((frame) => { return Number(frame.delay); }); let data = []; let frame = 0; let timer; const displayFrame = () => { context.clearRect(0, 0, width, height); context.drawImage(frames[frame], 0, 0); if (!meta.state) { return recorder.stop(); } timer = setTimeout(() => { timer = null; meta.onProgress && meta.onProgress((frame + 1) / frames.length, 'webm'); if (frame == frames.length - 1) { return recorder.stop(); } else { frame++; } displayFrame(); }, delay[frame]); }; recorder.ondataavailable = (event) => { if (event.data && event.data.size) { data.push(event.data); } }; recorder.onstop = () => { if (!meta.state) { return reject('Convert stop manually, reject when convert webm.' + meta.id); }; callBack(new Blob(data, { type: 'video/webm' }), meta); canvas = null; resolve(); }; displayFrame(); recorder.start(); }); }; const convert2 = { 'gif': toGif, 'png': toApng, 'webm': toWebm, }; return { add: (blob, meta, callBack) => { debugLog('Convert add', meta.path); queue.push({ blob, meta, callBack }); while (active.length < MAX_CONVERT && queue.length && !isStop) { convert(queue.shift()); } }, del: (metas) => { if (!metas.length) return; isStop = true; active = active.filter((meta) => { if (metas.includes(meta.meta)) { meta.meta.abort(); } else { return true; } }); queue = queue.filter((meta) => !metas.includes(meta.meta)); isStop = false; while (active.length < MAX_CONVERT && queue.length) { convert(queue.shift()); } }, }; })(); const parser = (() => { const TYPE_ILLUSTS = 0; const TYPE_MANGA = 1; const TYPE_UGOIRA = 2; const replaceInvalidChar = (string) => { if (!string) return; const temp = document.createElement('div'); temp.innerHTML = string; string = temp.textContent; return string.trim() .replace(/^\.|\.$/g, '') .replace(/[\u200b-\u200f\uFEFF\u202a-\u202e\\/:*?"|]/g, "") .replace(/"/g, "'") .replace(/</g, '﹤') .replace(/>/g, '﹥'); }; const parseByIllust = async (illustId) => { const res = await fetch('https://www.pixiv.net/artworks/' + illustId); if (!res.ok) throw new Error('fetch artworksURL failed: ' + res.status); const htmlText = await res.text(); const preloadData = JSON.parse(htmlText.match(/"meta-preload-data" content='(.*)'>/)[1]); if (!preloadData.illust) throw new Error('Fail to parse meta preload data.'); const illustInfo = preloadData.illust[illustId]; const user = replaceInvalidChar(illustInfo.userName) || 'userId-' + illustInfo.userId; const title = replaceInvalidChar(illustInfo.illustTitle) || 'illustId-' + illustInfo.illustId; const illustType = illustInfo.illustType; let metas = []; if (illustType == TYPE_ILLUSTS || illustType == TYPE_MANGA) { const firstImgSrc = illustInfo.urls.original; const srcPrefix = firstImgSrc.slice(0, firstImgSrc.indexOf('_') + 2); const srcSuffix = firstImgSrc.slice(-4); for (let i = 0; i < illustInfo.pageCount; i++) { metas.push({ id: illustId, illustType: illustType, path: user + '/' + user + '_' + title + '_' + illustId + '_p' + i + srcSuffix, src: srcPrefix + i + srcSuffix, }); } } if (illustType == TYPE_UGOIRA) { const ugoiraRes = await fetch('https://www.pixiv.net/ajax/illust/' + illustId + '/ugoira_meta'); if (!ugoiraRes.ok) throw new Error('fetch ugoira meta failed: ' + res.status); const ugoira = await ugoiraRes.json(); metas.push({ id: illustId, illustType: illustType, path: user + '/' + user + '_' + title + '_' + illustId + '.' + convertFormat, src: ugoira.body.originalSrc, ugoiraMeta: ugoira.body, }); } return metas; }; const parseByUser = async (userId, type) => { const res = await fetch('https://www.pixiv.net/ajax/user/' + userId + '/profile/all'); if (!res.ok) throw new Error('fetch user profile failed: ' + res.status); const profile = await res.json(); let illustIds; if (type) { illustIds = Object.keys(profile.body[type]); } else { illustIds = Object.keys(profile.body.illusts).concat(Object.keys(profile.body.manga)); } return illustIds; }; return { id: parseByIllust, user: parseByUser, }; })(); const downloader = (() => { const MAX_DOWNLOAD = 5; const MAX_RETRY = 3; let isStop = false; let queue = []; let active = []; const errHandler = (meta) => { if (!meta.retries) { meta.retries = 1; } else { meta.retries++; } if (meta.retries > MAX_RETRY) { meta.reject('xmlhttpRequest failed: ' + meta.src); console.log('Fail:', meta.path, meta.src); active.splice(active.indexOf(meta), 1); if (queue.length && !isStop) download(queue.shift()); } else { debugLog('retry xhr', meta.src); download(meta); } }; const save = (blob, meta) => { const imgUrl = URL.createObjectURL(blob); const request = { url: imgUrl, name: meta.path, onerror: (error) => { debugLog('error when save.', meta.path); meta.reject && meta.reject(error); }, onload: () => { if (typeof meta.onLoad == 'function') meta.onLoad(); URL.revokeObjectURL(imgUrl); meta.resolve(meta); }, }; meta.abort = GM_download(request).abort; } const download = (meta) => { debugLog('Download:', meta.path); active.push(meta); const request = { url: meta.src, timeout: 20000, method: 'GET', headers: { referer: 'https://www.pixiv.net' }, responseType: 'blob', ontimeout: () => { debugLog('xmlhttpRequest timeout:', meta.src); errHandler(meta); }, onprogress: (e) => { if (e.lengthComputable && typeof meta.onProgress == 'function') { meta.onProgress(e.loaded / e.total); } }, onload: (e) => { debugLog('Download complete', meta.path); if (!meta.state) return debugLog('meta.state = 0, stop already'); if (meta.illustType == 2 && convertFormat !== 'zip') { converter.add(e.response, meta, save); } else { save(e.response, meta); } active.splice(active.indexOf(meta), 1); if (queue.length && !isStop) download(queue.shift()); }, onerror: (error) => { debugLog('xmlhttpRequest failed:', meta.src); errHandler(meta); }, }; const abortObj = GM_xmlhttpRequest(request); meta.abort = () => { meta.state = 0; abortObj.abort(); meta.reject('xhr abort manually. ' + meta.src); debugLog('xhr abort:', meta.path); }; }; const add = (metas) => { debugLog('Downloader add:', metas); if (metas.length < 1) return; const promises = []; metas.forEach((meta) => { promises.push(new Promise((resolve, reject) => { meta.state = 1; meta.resolve = resolve; meta.reject = reject; })); }); queue = queue.concat(metas); while (active.length < MAX_DOWNLOAD && queue.length && !isStop) { download(queue.shift()); } return Promise.all(promises); }; const del = (metas) => { if (!metas.length) return; isStop = true; active = active.filter((meta) => { if (metas.includes(meta)) { meta.abort(); } else { return true; } }); queue = queue.filter((meta) => !metas.includes(meta)); isStop = false; while (active.length < MAX_DOWNLOAD && queue.length) { download(queue.shift()); } }; return { add: add, del: del, }; })(); const getIllustId = (thumbnail) => { if (thumbnail.childElementCount === 0) return false; const isHrefMatch = /artworks\/(\d+)$/.exec(thumbnail.href); if (isHrefMatch) { if (thumbnail.getAttribute('data-gtm-value') || thumbnail.classList.contains('gtm-illust-recommend-thumbnail-thumbnail') || thumbnail.classList.contains('gtm-discover-user-recommend-thumbnail') || thumbnail.classList.contains('work')) { return isHrefMatch[1]; } if (location.href.indexOf('bookmark_new_illust') != -1 && thumbnail.getAttribute('class')) { return isHrefMatch[1]; } } else { const isActivityMatch = /illust_id=(\d+)/.exec(thumbnail.href); if (isActivityMatch && thumbnail.classList.contains('work')) { return isActivityMatch[1]; } } return ''; } const createPdlBtn = (ele, attributes, textContent = '') => { if (!ele) ele = document.createElement('a'); ele.href = 'javascript:void(0)'; ele.textContent = textContent; if (!attributes) return ele; const { attrs, classList } = attributes; if (classList && classList.length > 0) { for (const cla of classList) { ele.classList.add(cla); } } if (attrs) { for (const key in attrs) { ele.setAttribute(key, attrs[key]); } } return ele; } const handleDownload = (pdlBtn, illustId) => { let pageCount, pageComplete = 0; const onProgress = (progress = 0, type = null) => { if (pageCount > 1) return; progress = Math.floor(progress * 100); switch (type) { case null: pdlBtn.style.setProperty('--pdl-progress', progress + '%'); case 'gif': case 'webm': pdlBtn.textContent = progress; break; case 'zip': pdlBtn.textContent = ''; break; } }; const onLoad = function () { if (pageCount < 2) return; const progress = Math.floor((++pageComplete / pageCount) * 100); pdlBtn.textContent = progress; pdlBtn.style.setProperty('--pdl-progress', progress + '%'); }; pdlBtn.classList.add('pdl-progress'); parser.id(illustId) .then((metas) => { pageCount = metas.length; metas.forEach((meta) => { meta.onProgress = onProgress; meta.onLoad = onLoad; }); return downloader.add(metas); }) .then(() => { pixivStorage.add(illustId); localStorage.setItem(`pdlTemp-${illustId}`, ''); pdlBtn.classList.remove('pdl-error'); pdlBtn.classList.add('pdl-complete'); }) .catch((err) => { if (err) console.log(err); pdlBtn.classList.remove('pdl-complete'); pdlBtn.classList.add('pdl-error'); }) .finally(() => { pdlBtn.innerHTML = ''; pdlBtn.style.removeProperty('--pdl-progress'); pdlBtn.classList.remove('pdl-progress'); }); }; const handleDownloadAll = (userId, type = '') => { let worksCount = 0, worksComplete = 0, failed = 0; let isCanceled = false; let metasRecord = []; const timers = []; const placeholder = body.querySelector('.pdl-nav-placeholder'); const control = body.querySelector('.pdl-btn-control'); const isExcludeDled = body.querySelector('#pdl-filter').checked; return new Promise((resolve, reject) => { control.onclick = () => { isCanceled = true; for (const timer of timers) { if (timer) clearTimeout(timer); } if (metasRecord.length) { downloader.del(metasRecord); converter.del(metasRecord); metasRecord = []; } control.onclick = null; reject('Download stopped'); }; const onProgressCB = (illustId) => { debugLog('update progress by', illustId); placeholder.textContent = `Downloading: ${++worksComplete} / ${worksCount}`; if (worksComplete == worksCount - failed) { placeholder.textContent = worksComplete == worksCount ? 'Complete' : 'Incomplete, see console.'; resolve(); } }; placeholder.textContent = 'Download...'; parser.user(userId, type) .then((illustIds) => { if (isCanceled) throw 'Download stopped'; if (isExcludeDled) { updateHistory(); debugLog('Before filter', illustIds.length); illustIds = illustIds.filter((illustId) => !pixivStorage.has(illustId)); debugLog('After filter', illustIds.length); } if (!illustIds.length) throw 'Exclude'; worksCount = illustIds.length; illustIds.forEach((illustId, idx) => { if (isCanceled) throw 'Download stopped'; let timer = setTimeout(() => { timer = null; parser.id(illustId) .then(metas => { if (isCanceled) throw 'Download stop manually: ' + metas[0].id; metasRecord = metasRecord.concat(metas); return downloader.add(metas); }) .then((metas) => { pixivStorage.add(illustId); localStorage.setItem(`pdlTemp-${illustId}`, ''); if (isCanceled) throw 'download stopped already, will not update progress.' + illustId; metasRecord = metasRecord.filter((meta) => !metas.includes(meta)); onProgressCB(illustId); }) .catch((err) => { debugLog(err); failed++; }); }, idx * 300); timers.push(timer); }); }) .catch((err) => { debugLog(err); reject(err); }); }); }; const toggerDlAll = (evt) => { const target = evt.target; if (target.classList.contains('pdl-btn-all')) { evt.stopPropagation(); const dlBars = target.parentElement.querySelectorAll('[pdl-userid]'); const placeholder = body.querySelector('.pdl-nav-placeholder'); const userId = target.getAttribute('pdl-userid'); dlBars.forEach((ele) => { ele.classList.toggle('pdl-hide') }); handleDownloadAll(userId, target.getAttribute('pdl-type')) .catch((err) => { placeholder.textContent = err; }) .finally(() => { dlBars.forEach((ele) => { ele.classList.toggle('pdl-hide') }); }); } } const observerCallback = () => { const isArtworksPage = /artworks\/(\d+)$/.exec(location.href); const isUserPage = /users\/(\d+)/.exec(location.pathname); if (isArtworksPage && !body.querySelector('.pdl-btn-main')) { const addFavousBtn = body.querySelector('.gtm-main-bookmark'); if (addFavousBtn) { const handleBar = addFavousBtn.parentElement.parentElement; const pdlBtnWrap = addFavousBtn.parentElement.cloneNode(); const attrs = { attrs: { 'pdl-id': isArtworksPage[1] }, classList: ['pdl-btn', 'pdl-btn-main'] }; if (pixivStorage.has(isArtworksPage[1])) attrs.classList.push('pdl-complete'); pdlBtnWrap.appendChild(createPdlBtn(null, attrs)); handleBar.appendChild(pdlBtnWrap); body.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key == 'q') { const pdlMainBtn = body.querySelector('.pdl-btn-main'); if (pdlMainBtn) { e.preventDefault(); if (!e.repeat) { pdlMainBtn.dispatchEvent(new MouseEvent('click', { "bubbles": true })); } } } }); } } body.querySelectorAll('a').forEach((e) => { if (!e.querySelector('.pdl-btn-sub')) { const illustId = getIllustId(e); if (illustId) { const attrs = { attrs: { 'pdl-id': illustId }, classList: ['pdl-btn', 'pdl-btn-sub'] }; if (pixivStorage.has(illustId)) attrs.classList.push('pdl-complete'); e.appendChild(createPdlBtn(null, attrs)); } } }); if (isUserPage) { const nav = body.querySelector('nav'); if (!nav || body.querySelector('.pdl-nav-placeholder')) return; const fragment = document.createDocumentFragment(); const placeholder = document.createElement('div'); placeholder.classList.add('pdl-nav-placeholder'); fragment.appendChild(placeholder); const baseEle = nav.querySelector('a:not([aria-current])').cloneNode(); fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userId': isUserPage[1] }, classList: ['pdl-btn-control', 'pdl-stop', 'pdl-hide'] }, 'Stop')); fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userId': isUserPage[1] }, classList: ['pdl-btn-all'] }, 'All')); if (nav.querySelector('a[href$=illustrations]') && nav.querySelector('a[href$=manga]')) { fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userid': isUserPage[1], 'pdl-type': 'illusts' }, classList: ['pdl-btn-all'] }, 'Illusts')); fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userid': isUserPage[1], 'pdl-type': 'manga' }, classList: ['pdl-btn-all'] }, 'Manga')); } const wrapper = document.createElement('div'); const checkbox = document.createElement('input'); const label = document.createElement('label'); wrapper.classList.add('pdl-wrap'); checkbox.id = 'pdl-filter'; checkbox.type = 'checkbox'; label.setAttribute('for', 'pdl-filter'); label.textContent = 'Exclude downloaded'; wrapper.appendChild(checkbox); wrapper.appendChild(label); nav.parentElement.insertBefore(wrapper, nav); nav.appendChild(fragment); nav.addEventListener('click', toggerDlAll); } } const updateHistory = () => { Object.keys(localStorage).forEach((key) => { const matchResult = /pdlTemp-(\d+)/.exec(key); if (matchResult) { pixivStorage.add(matchResult[1]); localStorage.removeItem(matchResult[0]); } }); localStorage.pixivDownloader = JSON.stringify([...pixivStorage]); } const isDebug = false; localStorage.pixivDownloader = localStorage.pixivDownloader || '[]'; let pixivStorage = new Set(JSON.parse(localStorage.pixivDownloader)); updateHistory(); GM_registerMenuCommand('Apng', () => { convertFormat = localStorage.pdlFormat = 'png'; }, 'a'); GM_registerMenuCommand('Gif', () => { convertFormat = localStorage.pdlFormat = 'gif'; }, 'g'); GM_registerMenuCommand('Zip', () => { convertFormat = localStorage.pdlFormat = 'zip'; }, 'z'); GM_registerMenuCommand('Webm', () => { convertFormat = localStorage.pdlFormat = 'webm'; }, 'w'); GM_registerMenuCommand('Clear history', () => { updateHistory(); pixivStorage = new Set(); localStorage.pixivDownloader = '[]'; }, 'c'); const pdlStyle = document.createElement('style'); pdlStyle.innerHTML = ` @property --pdl-progress { syntax: '<percentage>'; inherits: true; initial-value: 0%; } @keyframes pdl_loading { 100% { transform: translate(-50%, -50%) rotate(360deg); } } .pdl-btn { position: relative; border-top-right-radius: 8px; background: no-repeat center/85%; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%233C3C3C' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E"); color: #01b468; display: inline-block; font-size: 13px; font-weight: bold; height: 32px; line-height: 32px; margin: 0; overflow: hidden; padding: 0; text-decoration: none!important; text-align: center; text-overflow: ellipsis; user-select: none; white-space: nowrap; width: 32px; z-index: 1; } .pdl-btn-main { margin: 0 0 0 10px; } .pdl-btn-sub { bottom: 0; background-color: rgba(255, 255, 255, .5); left: 0; position: absolute; } .pdl-error { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23EA0000' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-complete { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%2301B468' d='M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-progress { background-image: none; cursor: default; } .pdl-progress:after{ content: ''; display: inline-block; position: absolute; top: 50%; left: 50%; width: 27px; height: 27px; transform: translate(-50%, -50%); -webkit-mask: radial-gradient(transparent, transparent 54%, #000 57%, #000); mask: radial-gradient(transparent, transparent 54%, #000 57%, #000); border-radius: 50%; } .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-progress:empty:after { background: conic-gradient(#01B468 0, #01B468 25%, #01B46833 25%, #01B46833); animation: 1.5s infinite linear pdl_loading; } .pdl-nav-placeholder { flex-grow: 1; height: 42px; line-height: 42px; text-align: right; font-weight: bold; font-size: 16px; color: rgb(133, 133, 133); border-top: 4px solid transparent; cursor: default; white-space: nowrap; } .pdl-btn-all::before, .pdl-stop::before { content: ''; height: 24px; width: 24px; transition: background-image 0.2s ease 0s; background: no-repeat center/85%; } .pdl-btn-all::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-stop::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-btn-all:hover::before{ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-stop:hover::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E"); } .pdl-hide { display: none!important; } .pdl-wrap { text-align: right; padding-right: 24px; font-weight: bold; font-size: 14px; line-height: 14px; color: rgb(133, 133, 133); transition: color 0.2s ease 0s; } .pdl-wrap:hover { color: rgb(31, 31, 31); } .pdl-wrap label { padding-left: 8px; cursor: pointer; } .pdl-wrap input { vertical-align: top; appearance: none; position: relative; box-sizing: border-box; width: 28px; border: 2px solid transparent; cursor: pointer; border-radius: 14px; height: 14px; background-color: rgba(133, 133, 133); transition: background-color 0.2s ease 0s, box-shadow 0.2s ease 0s; } .pdl-wrap input:hover { background-color: rgba(31, 31, 31); } .pdl-wrap input::after { content: ""; position: absolute; display: block; top: 0px; left: 0px; width: 10px; height: 10px; transform: translateX(0px); background-color: rgb(255, 255, 255); border-radius: 10px; transition: transform 0.2s ease 0s; } .pdl-wrap input:checked { background-color: rgb(0, 150, 250); } .pdl-wrap input:checked::after { transform: translateX(14px); }`; document.head.appendChild(pdlStyle); const body = document.body; new MutationObserver(observerCallback).observe(body, { attributes: false, childList: true, subtree: true, }); body.addEventListener('click', (event) => { const pdlNode = event.target; if (pdlNode.hasAttribute('pdl-id')) { event.stopPropagation(); if (!pdlNode.classList.contains('pdl-progress')) { handleDownload(pdlNode, pdlNode.getAttribute('pdl-id')); } } }); function debugLog(...msgs) { if (isDebug) console.log(...msgs); } })();