您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A selector and downloader for the various booru imageboards
// ==UserScript== // @name Booru-Selector-Downloader // @namespace http://tampermonkey.net/ // @icon https://yande.re/favicon.ico // @version 4.0.1 // @description A selector and downloader for the various booru imageboards // @description:en A selector and downloader for the various booru imageboards // @description:zh 图站选择下载工具 // @author Beats0 // @license GPL-3.0 License // @match *://yande.re/post* // @match *://konachan.net/* // @match *://konachan.com/* // @match *://gelbooru.com/* // @match *://danbooru.donmai.us/* // @match *://sonohara.donmai.us/* // @include *://yande.re/* // @include *://konachan.net/* // @include *://konachan.com/* // @include *://gelbooru.com/* // @include *://danbooru.donmai.us/* // @include *://sonohara.donmai.us/* // @grant GM_addStyle // @grant GM_download // @grant GM_openInTab // @home-url https://greasyfork.org/zh-CN/scripts/371605-booru-selector-downloader // @home-url2 https://github.com/Beats0/scripter // ==/UserScript== /** * ### Hot keys * * `A`: Previous page * `D`: Next page * `Q`: Select/Deselect all image * `S`: Save sample image * `X`: Save original image(if no original image, the downloader will download the sample image) * `F`: Favorite image * `R`: Remove from favorites * `Ctrl + MouseClick`: Open in the new window * `Alt + MouseClick`: Open in the new window and auto focus the new tab * `Shift + MouseHover`: Show preview image when hover the image, default scale size is `scale(2.5, 2.5)` * */ (function () { 'use strict'; const originUrl = document.location.origin; const locationUrl = document.location.protocol + '//' + window.location.host; const REyande = /yande/, REkonachan = /konachan/, REgelbooru = /gelbooru/, REdanbooru = /danbooru/, REsonohara = /sonohara/; const re1 = /\d\w+/, re2 = /([a-fA-F0-9]{32})/, re3 =/\.[0-9a-z]+$/i; const REyandeResult = REyande.test(originUrl); const REkonachanResult = REkonachan.test(originUrl); const REdanbooruResult = REdanbooru.test(originUrl) || REsonohara.test(originUrl); const REgelbooruResult = REgelbooru.test(originUrl); let parse = null const reTrySvgIcon = `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11702" width="20" height="20"><path d="M512 214.016q141.994667 0 242.005333 100.010667t100.010667 240q0 141.994667-100.992 242.005333t-240.981333 100.010667-240.981333-100.010667-100.992-242.005333l86.016 0q0 105.984 75.008 180.992t180.992 75.008 180.992-75.008 75.008-180.992-75.008-180.992-180.992-75.008l0 171.989333-214.016-214.016 214.016-214.016 0 171.989333z" p-id="11703" fill="#ee8887"></path></svg>` function $(selector) { return document.querySelector(selector) } function $$(selector) { return document.querySelectorAll(selector) } function domParser(fragment) { if(!parse) { const range = document.createRange(); parse = range.createContextualFragment.bind(range); } return parse(fragment) } function promiseFetch(url, data) { return new Promise((resolve, reject) => { fetch(url, data) .then(res => { if (res.ok) { resolve(res); } else { throw res; } }) .catch(err => { reject(err); }); }); } class BooruDownloader { constructor() { this.batchCount = 0 this.downloadLimit = 4 this.hoverEl = null this.cacheImg = {} // {id: src} this.init() } init() { console.log('init BooruDownloader') if (REyandeResult || REkonachanResult) { let posts = $('#post-list-posts'); if (!posts) return this.init_yande_konachan(); } if (REdanbooruResult) { const posts = $('.posts-container') if(!posts) return this.init_danbooru(); } if (REgelbooruResult) { const posts = $('.thumbnail-container') if(!posts) return this.init_gelbooru(); } this.initStyle() this.initMenuPanel() this.initHotKey() } initStyle() { const styleCode = ` :root { --primary-backgroundColor: #eee; --primary-lineBackgroundColor: #ccc; --primary-fontColor: #ee8887; --primary-fontColorHover: #ff4342; --primary-headerFontColor: #ffffff; --primary-border: 1px solid transparent; } [data-theme=light] .darkToggleIcon { display: none; } [data-theme=dark] .lightToggleIcon { display: none; } ul#post-list-posts { padding-bottom: 350px; } ul#post-list-posts li { float: none } div#posts { padding-bottom: 200px; } .imgItem { transition: .2s; } .imgItem:hover, .imgItem:focus { outline: 1px solid var(--primary-fontColor); } .imgItem img { transition: .2s; } .imgItemChecked { outline: 1px solid var(--primary-fontColor); } article.post-preview { float: none; } .helper-board { width: 450px; height: 320px; position: fixed; font-size: 12px; right: 10px; bottom: 4px; background: var(--primary-backgroundColor); color: var(--primary-fontColor); border: var(--primary-border); border-radius: 5px; overflow: hidden; transition: all cubic-bezier(.22,.58,.12,.98) .4s; box-shadow: 0 2px 12px 0 rgba(246, 150, 149, 0.6); } .helper-board.helper-board-small { width: 50px; height: 50px; border-radius: 50%; bottom: 50vh; cursor: pointer; background: var(--primary-fontColor); } .helper-board.helper-board-small .board-header { width: 50px; height: 50px; border-radius: 50%; } .helper-board.helper-board-small .board-header .board-close-button, .helper-board.helper-board-small .board-header .board-header-text, .helper-board.helper-board-small .theme-btn { display: none; } .helper-board.helper-board-small .board-header .board-header-small-tip { display: block; width: 50px; height: 50px; border-radius: 50%; line-height: 50px; text-align: center; font-size: 26px; } .board-header { height: 30px; color: var(--primary-headerFontColor); background: var(--primary-fontColor); font-size: 16px; } .board-header-inner { display: flex; align-items: center; justify-content: space-between; padding-right: 32px; } .board-content { overflow-y: auto; overflow-x: hidden; max-height: 340px; padding: 10px 12px 50px 12px; height: 100%; font-size: 14px; } .board-header-text { line-height: 30px; padding-left: 10px; font-size: 14px; } .board-close-button { position: absolute; top: 6px; right: 3px; width: 20px; height: 20px; margin: 0; padding: 0; cursor: pointer; transition: all .3s; } .board-close-button:hover { color: var(--primary-headerFontColor); } .board-header-small-tip { display: none; } .board-content-row { display: flex; align-items: center; margin-bottom: 8px; height: 20px; } .row-label { color: var(--primary-fontColor); margin-right: 8px; } .row-content { display: flex; align-items: center; } .hover-item-line { color: var(--primary-fontColor); text-decoration: underline!important; cursor: pointer; } .hover-item-line:hover { color: var(--primary-fontColorHover); } .hover-item { color: var(--primary-fontColor); cursor: pointer; } .hover-item:hover { color: var(--primary-fontColorHover); } .download-row-container { margin-top: 15px; padding-bottom: 15px; } .download-row { display: flex; align-items: center; margin-bottom: 5px; } .download-row-title { color: var(--primary-fontColor); margin-right: 10px; } .download-row-line { flex: 1; height: 4px; background: var(--primary-lineBackgroundColor); margin-right: 10px; } .download-row-line-active { height: 100%; background: var(--primary-fontColor); transition: width .4s ease; } .download-row-percent { color: var(--primary-fontColor); width: 50px; } .fav-state { margin-left: 7px; color: var(--primary-fontColor); } .re-try-icon svg { margin-left: 5px; cursor: pointer; transform: translateY(3px); } .theme-btn { background: none; border: none; color: var(--primary-headerFontColor); cursor: pointer; font-family: inherit; padding: 0; align-items: center; border-radius: 50%; display: flex; height: 100%; justify-content: center; transition: all 200ms; } .theme-btn:hover { background: #ebedf0; color: var(--primary-fontColor); } .imgTransform { opacity: 1!important; z-index: 999; } .imgTransform img { transform: scale(2.5, 2.5); transition: .2s; } .previewTip .imgItem { opacity: 0.5; } .imgTransform .thumb { position: absolute; z-index: 1; } .hide { width: 0; height: 0; display: none!important; } ` GM_addStyle(styleCode) } init_yande_konachan() { let posts = $('#post-list-posts'); let postsItems = posts.querySelectorAll('li'); for (let i = 0; i < postsItems.length; i++) { postsItems[i].classList.add('imgItem'); postsItems[i].firstElementChild.firstElementChild.setAttribute('onclick', 'return false'); const template = `<div style="position: relative;text-align: center;"><input type="checkbox" class="checkbox"></div>` postsItems[i].insertAdjacentHTML('afterbegin', template); postsItems[i].addEventListener('mouseover', (e) => this.setTransition(e, 'mouseover', i)) postsItems[i].addEventListener('mouseout', (e) => this.setTransition(e, 'mouseout', i)) } posts.addEventListener('click', (e) => this.handleClickImg(e)) } init_danbooru() { const posts = $('.posts-container') const postsItems = posts.querySelectorAll('article'); for (let i = 0; i < postsItems.length; i++) { postsItems[i].classList.add('imgItem'); postsItems[i].firstElementChild.setAttribute('onclick', 'return false'); const template = `<div style="position: relative;text-align: center;"><input type="checkbox" class="checkbox"></div>` postsItems[i].insertAdjacentHTML('afterbegin', template); postsItems[i].addEventListener('mouseover', (e) => this.setTransition(e, 'mouseover', i)) postsItems[i].addEventListener('mouseout', (e) => this.setTransition(e, 'mouseout', i)) } posts.addEventListener('click', (e) => this.handleClickImg(e)) } init_gelbooru() { const posts = $('.thumbnail-container') const postsItems = posts.querySelectorAll('article'); for (let i = 0; i < postsItems.length; i++) { postsItems[i].classList.add('imgItem'); postsItems[i].style.position = 'relative' postsItems[i].firstElementChild.setAttribute('onclick', 'return false'); const template = `<div style="position: absolute; top: 0; text-align: center;"><input type="checkbox" class="checkbox"></div>` postsItems[i].insertAdjacentHTML('afterbegin', template); postsItems[i].addEventListener('mouseover', (e) => this.setTransition(e, 'mouseover', i)) postsItems[i].addEventListener('mouseout', (e) => this.setTransition(e, 'mouseout', i)) } posts.addEventListener('click', (e) => this.handleClickImg(e)) } initMenuEvent() { const headerEl = $('.board-header') headerEl.onclick = function (e) { const boardEl = headerEl.parentNode if(boardEl.classList.contains('helper-board-small')) { boardEl.classList.remove('helper-board-small') } else { if(e.target.className === 'board-close-button') { boardEl.classList.add('helper-board-small') } } } const theme = localStorage.getItem('h-theme') || 'light' const showToolTip = localStorage.getItem('h-show-tooltip') || '1' this.setTheme(theme) this.handleToggleToolTip(showToolTip) $('.theme-btn').addEventListener('click', (e) => this.handleToggleTheme(e)) $('#buttonSelectAll').addEventListener('click', (e) => this.handleClickMenuAllBtn()) $('#downloadSample').addEventListener('click', (e) => this.handleDownLoadImg(e, 'sample')) $('#downloadOriginal').addEventListener('click', (e) => this.handleDownLoadImg(e, 'original')) $('#addFavorite').addEventListener('click', (e) => this.handleFavorite(true)) $('#removeFavorite').addEventListener('click', (e) => this.handleFavorite(false)) $('.fav-list-container').addEventListener('click', (e) => this.handleClickFavList(e, 'fav-list-container')) $('#showToolTipBtn').addEventListener('click', (e) => { const newShowToolTip = localStorage.getItem('h-show-tooltip') === '1' ? '0' : '1' this.handleToggleToolTip(newShowToolTip) }) } initMenuPanel() { const template = ` <div class="helper-board"> <div class="board-header"> <div class="board-header-inner"> <div class="board-header-text">Booru-Selector-Downloader</div> <button class="theme-btn" type="button" title="Change Theme"> <svg viewBox="0 0 24 24" width="20" height="20" class="lightToggleIcon"> <path fill="currentColor" d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"></path> </svg> <svg viewBox="0 0 24 24" width="20" height="20" class="darkToggleIcon"> <path fill="currentColor" d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path> </svg> </button> </div> <div class="board-close-button" title="Close">X</div> <div class="board-header-small-tip">H</div> </div> <div class="board-content"> <div class="board-content-row"> <div class="row-label hover-item" id="buttonSelectAll" title="Hotkey Q">Select All 0 Image</div> </div> <div class="board-content-row"> <div class="row-label hover-item" id="downloadSample" title="Hotkey S">Download Sample</div> </div> <div class="board-content-row"> <div class="row-label hover-item" id="downloadOriginal" title="Hotkey X">Download Original</div> </div> <div class="board-content-row"> <div class="row-label hover-item" id="addFavorite" title="Hotkey F">Add To Favorite</div> </div> <div class="board-content-row"> <div class="row-label hover-item" id="removeFavorite" title="Hotkey R">Remove From Favorite</div> </div> <div class="board-content-row"> <div class="row-label hover-item" id="showToolTipBtn" title="Hotkey Shift + MouseHover On Image">Show Preview ToolTip: <span>[On]</span></div> </div> <div class="board-content-row"> <div class="row-label">Release Note: </div> <a class="row-content hover-item-line" href="https://greasyfork.org/zh-CN/scripts/371605-booru-selector-downloader" target="_blank">v4.0.1</a> </div> <div class="fav-list-container"></div> <div class="download-row-container"></div> </div> </div> ` document.body.insertAdjacentHTML('beforeend', template) this.initMenuEvent() } initHotKey() { window.addEventListener('keydown', (e) => this.hotKeyHandler(e), false) window.addEventListener('keyup', (e) => this.handleKeyUp(e), false) } /** * @param {KeyboardEvent} e * */ async hotKeyHandler(e) { // ignore input event if(e.target && e.target.tagName === 'INPUT') return // press key `A` or `D` to paginate let pageRight = $('#paginator > div > a.next_page') let pageLeft = $('#paginator > div > a.previous_page') if (REgelbooruResult) { pageRight = $('#paginator b').previousElementSibling pageLeft = $('#paginator b').nextElementSibling } // `A`, `D`: paginate(only for yande.re and danbooru) if (e.key === 'd' && pageRight) { pageRight.click() } if (e.key === 'a' && pageLeft) { pageLeft.click() } // `Q`: Select all, Deselect all if (e.key === 'q') { this.handleClickMenuAllBtn() } // `F`: Favorite if (e.key === 'f') { this.handleFavorite(true) } // `R`: Remove from favorites if (e.key === 'r') { this.handleFavorite(false) } // `S`: Save sample image if (e.key === 's') { this.handleDownLoadImg(null, 'sample').then(res => {}) } // `X`: Save original image if (e.key === 'x') { this.handleDownLoadImg(null, 'original').then(res => {}) } // 'Shift': Show larger image tool tip if (e.key === 'Shift') { let posts = null let el = this.hoverEl if (el) { this.hoverEl.classList.add('imgTransform') } if (REyandeResult || REkonachanResult) { posts = $('#post-list-posts'); } if (REdanbooruResult) { posts = $('.posts-container') if (el) { const id = Number(el.getAttribute('data-id')) if (!this.cacheImg.hasOwnProperty(id)) { this.cacheImg[id] = '' const imgInfo = await this.fetchDetailPage(id) const sample = imgInfo.sample this.cacheImg[id] = sample el.querySelector('source').srcset = sample el.querySelector('img').src = sample } el.classList.add('imgTransform') } } if (REgelbooruResult) { posts = $('.thumbnail-container') if (el) { const id = Number(el.querySelector('a').getAttribute('id').replace('p', '')) if (!this.cacheImg.hasOwnProperty(id)) { this.cacheImg[id] = '' const imgInfo = await this.fetchDetailPage(id) const sample = imgInfo.sample this.cacheImg[id] = sample el.querySelector('img').src = sample } } } posts.classList.add('previewTip') } } /** * @param {KeyboardEvent} e * */ handleKeyUp(e) { // ignore input event if(e.target && e.target.tagName === 'INPUT') return // 'Shift': close larger image tool tip if (e.key === 'Shift') { if (this.hoverEl) { this.hoverEl.classList.remove('imgTransform') } let posts = null if (REyandeResult || REkonachanResult) { posts = $('#post-list-posts'); } if (REdanbooruResult) { posts = $('.posts-container') } if (REgelbooruResult) { posts = $('.thumbnail-container') } posts.classList.remove('previewTip') } } handleToggleTheme(e) { const theme = document.body.getAttribute('data-theme') theme === 'light' ? this.setTheme('dark') : this.setTheme('light') } /** * @param {string} theme light | dark * */ setTheme(theme) { if(theme === 'light') { const lightTheme = [ {key: 'backgroundColor', value: '#eee'}, {key: 'lineBackgroundColor', value: '#ccc'}, {key: 'fontColor', value: '#ee8887'}, {key: 'fontColorHover', value: '#ff4342'}, {key: 'headerFontColor', value: '#ffffff'}, {key: 'border', value: '1px solid #fbe0df'}, ] lightTheme.forEach(item => this.setCssVariable(item)) document.body.setAttribute('data-theme', 'light') localStorage.setItem('h-theme', 'light') } else { const darkTheme = [ {key: 'backgroundColor', value: '#222'}, {key: 'lineBackgroundColor', value: '#ccc'}, {key: 'fontColor', value: '#ee8887'}, {key: 'fontColorHover', value: '#ffffff'}, {key: 'headerFontColor', value: '#ffffff'}, {key: 'border', value: '1px solid #ee8887'}, ] darkTheme.forEach(item => this.setCssVariable(item)) document.body.setAttribute('data-theme', 'dark') localStorage.setItem('h-theme', 'dark') } } setCssVariable({ key, value }) { const propertyName = `--primary-${key}`; document.documentElement.style.setProperty(propertyName, value); } handleClickMenuAllBtn() { const btn = $('#buttonSelectAll') if (this.batchCount >= 1) { btn.innerHTML = "DeselectAll " + this.batchCount + " Image"; this.deselectAll() } else { btn.innerHTML = "SelectAll " + this.batchCount + " Image"; this.selectAll() } } /** * @param {Event} e * @param {string} type sample | original * */ async handleDownLoadImg(e, type = 'sample') { const postsItems = $$('.imgItemChecked'); let imgs = [] if(postsItems.length) { this.updateFetchingProgress(0, postsItems.length) } for (let i = 0; i < postsItems.length; i++) { let id = 0 if (REyandeResult || REkonachanResult) { id = Number(postsItems[i].getAttribute('id').replace('p', '')) } if (REdanbooruResult) { id = Number(postsItems[i].getAttribute('data-id')) } if (REgelbooruResult) { id = Number(postsItems[i].querySelector('a').getAttribute('id').replace('p', '')) } const imgInfo = await this.fetchDetailPage(id) imgs.push({ id, url: imgInfo[type], fileName: `${id}${re3.exec(imgInfo[type])[0]}` }) this.updateFetchingProgress(i + 1, postsItems.length) } this.createDownloadProgress(imgs) // downloadPool await this.downloadPool(imgs) } updateFetchingProgress(i, total) { const el = $('#fetching-download-row') if(!el) { const downloadElContainer = $('.download-row-container') const template = ` <div id="fetching-download-row" class="download-row"> <div class="download-row-title">Fetching</div> <div class="download-row-line"> <div class="download-row-line-active" style="width: 0%;"></div> </div> <div class="download-row-percent">${i}/${total}</div> </div> ` downloadElContainer.insertAdjacentHTML('afterbegin', template) } else { const width = (Math.round(i / total * 10000) / 100.00); el.querySelector('.download-row-line-active').style.width = `${width}%` el.querySelector('.download-row-percent').innerText = `${i}/${total}` } } async downloadPool(imgs = []) { for (let i = 0; i < imgs.length; i++) { this.updateDownloadProgress(imgs[i]) } let pool = [] for (let i = 0; i < imgs.length; i++) { const img = imgs[i] const task = this.downloadHandler(img) pool.push(task) task .then((id) => { console.log(`${ id } ok`) }) .catch((id) => { console.log(`${ id } error`) }) .finally(() => { pool.splice(pool.indexOf(task), 1) }) if (pool.length === this.downloadLimit) { await Promise.race(pool) } } } downloadHandler(img) { // see GM_download: https://www.tampermonkey.net/documentation.php#GM_download return new Promise((resolve, reject) => { const arg = { url: img.url, name: img.fileName, onprogress: (xhr) => { this.downloadProgress(xhr, img.id, false) if(Math.floor(xhr.loaded / xhr.total * 100) >= 100) { resolve(img.id) } }, onload: () => { resolve(img.id) this.downloadProgress(null, img.id, true) }, onerror: () => { // still resolve resolve(img.id) this.onDownloadError(img) }, ontimeout: () => { // still resolve resolve(img.id) this.onDownloadError(img) } } GM_download(arg) }) } /** * @param {Array} imgs * */ createDownloadProgress(imgs) { for (let i = 0; i < imgs.length; i++) { const img = imgs[i] const pageUrl = this.getPageUrl(img.id) const downloadRowEl = $(`#download-row-${img.id}`) if(!downloadRowEl) { // create downloadRow const downloadList = $('.download-row-container') const template = ` <div id="download-row-${img.id}" class="download-row"> <a href="${pageUrl}" class="download-row-title hover-item-line" target="_blank">${img.id}</a> <div class="download-row-line"> <div class="download-row-line-active" style="width: 0%;"></div> </div> <div class="download-row-percent">0%</div> </div> ` downloadList.insertAdjacentHTML('beforeend', template) } } } updateDownloadProgress({id, url, fileName}) { const downloadRowEl = $(`#download-row-${id}`) if(!downloadRowEl) { // create downloadRow const downloadList = $('.download-row-container') const pageUrl = this.getPageUrl(id) const template = ` <div id="download-row-${id}" class="download-row"> <a href="${pageUrl}" class="download-row-title hover-item-line" target="_blank">${id}</a> <div class="download-row-line"> <div class="download-row-line-active" style="width: 0%;"></div> </div> <div class="download-row-percent">0%</div> </div> ` downloadList.insertAdjacentHTML('beforeend', template) } } onDownloadError(img) { const downloadRowEl = $(`#download-row-${img.id}`) const el = downloadRowEl.querySelector('.download-row-percent') if(!el.classList.contains('hover-item')) { el.classList.add('hover-item') } el.innerHTML = reTrySvgIcon el.onclick = this.downloadHandler(img) } /** * @param {ProgressEventInit | null} xhr * @param {number} id * @param {boolean} isFinished * */ downloadProgress(xhr, id, isFinished = false) { let width = 0 if (xhr === null && isFinished) { width = 100 } else { width = xhr.lengthComputable ? Math.floor(xhr.loaded / xhr.total * 100) : 0; } const downloadRowEl = $(`#download-row-${ id }`) if (!downloadRowEl) { // create downloadRow const downloadList = $('.download-row-container') const pageUrl = this.getPageUrl(id) const template = ` <div id="download-row-${ id }" class="download-row"> <a href="${ pageUrl }" class="download-row-title hover-item-line" target="_blank">${ id }</a> <div class="download-row-line"> <div class="download-row-line-active" style="width: ${ width }%;"></div> </div> <div class="download-row-percent">${ width }%</div> </div> ` downloadList.insertAdjacentHTML('beforeend', template) } else { // update downloadRow downloadRowEl.querySelector('.download-row-title').innerText = id downloadRowEl.querySelector('.download-row-line-active').style.width = `${ width }%` downloadRowEl.querySelector('.download-row-percent').innerText = `${ width }%` } } /** * @param {number} id * @return {string} * */ getPageUrl(id) { let pageUrl = `` if(REyandeResult || REkonachanResult) { pageUrl = `${locationUrl}/post/show/${id}` } if(REdanbooruResult) { pageUrl = `${locationUrl}/posts/${id}` } if(REgelbooruResult) { pageUrl = `${locationUrl}/index.php?page=post&s=view&id=${id}` } return pageUrl } /** * @param {string} showCode '0' | '1' * */ handleToggleToolTip(showCode) { let preViewEl = null, infoEl = null; if (REyandeResult || REkonachanResult) { preViewEl = $('#index-hover-overlay') infoEl = $('#index-hover-info') } if (REdanbooruResult) { preViewEl = $('#post-tooltips') } if (showCode === '0') { preViewEl && preViewEl.classList.remove('hide') infoEl && infoEl.classList.remove('hide') } else if (showCode === '1') { preViewEl && preViewEl.classList.add('hide') infoEl && infoEl.classList.add('hide') } $('#showToolTipBtn span').innerText = showCode === '0' ? '[OFF]' : '[ON]' localStorage.setItem('h-show-tooltip', showCode) } /** * @param {boolean} isLike * */ handleFavorite(isLike) { const postsItems = $$('.imgItemChecked'); for (let i = 0; i < postsItems.length; i++) { let id = 0 if (REyandeResult || REkonachanResult) { id = Number(postsItems[i].getAttribute('id').replace('p', '')) } if (REdanbooruResult) { id = Number(postsItems[i].getAttribute('data-id')) } if (REgelbooruResult) { id = Number(postsItems[i].querySelector('a').getAttribute('id').replace('p', '')) } this.fetchFavorite(id, isLike) } } /** * @param {number} id * @param {boolean} isLike * */ fetchFavorite(id, isLike) { let url = `` let data = {} if (REyandeResult || REkonachanResult) { url = `${ locationUrl }/post/vote.json` const csrfToken = $("meta[name=csrf-token]").content data = { "headers": { "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "x-csrf-token": csrfToken, }, "body": `id=${ id }&score=${ isLike ? 3 : 2 }`, "method": "POST", "mode": "cors", "credentials": "include" } } if (REdanbooruResult) { url = isLike ? `${ locationUrl }/favorites?post_id=${ id }` : `${ locationUrl }/favorites/${id}` const csrfToken = $("meta[name=csrf-token]").content data = { "headers": { "accept": "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01", "x-csrf-token": csrfToken, "x-requested-with": "XMLHttpRequest" }, "body": null, "method": isLike ? "POST" : "DELETE", "mode": "cors", "credentials": "include" } } if (REgelbooruResult) { url = isLike ? `${ locationUrl }/public/addfav.php?id=${id}` : `${ locationUrl }/index.php?page=favorites&s=delete&id=${id}` data = { "headers": { "accept": "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01", "x-requested-with": "XMLHttpRequest" }, "body": null, "method": "GET", "mode": "cors", "credentials": "include" } } promiseFetch(url, data) .then(res => { if (res.status === 200) { const log = { id, isLike, result: 'success', } this.addFavoriteLog(log) } }) .catch(e => { const log = { id, isLike, result: 'error', } this.addFavoriteLog(log) console.error('add favorite error') }) } handleClickFavList(e, parentClassName) { let el = e.target let hasEl = false while (el !== document && el.className !== parentClassName) { // click re-try-icon to re add favorite if (el.className === 're-try-icon') { hasEl = true break; } el = el.parentNode } if(!hasEl) return const id = Number(el.getAttribute('data-id')) const isLike = el.getAttribute('data-isLike') === 'true' this.fetchFavorite(id, isLike) } /** * @param {number} log.id * @param {boolean} log.isLike * @param {string} log.result success | error * */ addFavoriteLog(log) { const { id, isLike, result } = log const el = $('.fav-list-container') const logItemEl = $(`#favLog${id}`) if(!logItemEl) { const pageUrl = this.getPageUrl(id) const elItem = document.createElement('div') elItem.className = 'board-content-row' elItem.id = `favLog${id}` elItem.innerHTML = ` <div class="row-label">${isLike ? 'Add' : 'Remove'} Favorites: </div> <div class="row-content"> <a class="hover-item-line" href="${pageUrl}" target="_blank">${id}</a> <span class="fav-state">${result === 'success' ? 'Success' : 'Error'}</span> ${ result === 'error' ? `<span data-id="${id}" data-isLike="${String(isLike)}" class="re-try-icon">${reTrySvgIcon}</span>` : '' } </div>` el.appendChild(elItem) } else { logItemEl.querySelector('.row-label').innerText = isLike ? 'Add Favorites: ' : 'Remove Favorites: ' logItemEl.querySelector('.fav-state').innerText = result === 'success' ? 'Success' : 'Error' let iconEl = logItemEl.querySelector('.re-try-icon') if(result === 'success') { // remove re-icon if(iconEl) iconEl.remove() } else if(result === 'error') { iconEl ? iconEl.setAttribute('data-isLike', String(isLike)) : logItemEl.querySelector('.row-content').insertAdjacentHTML('beforeend', `<span data-id="${id}" data-isLike="${String(isLike)}" class="re-try-icon">${reTrySvgIcon}</span>`); } } } /** * @interface imgInfo * @param {number} id * @return {Promise<imgInfo | Error>} * */ fetchDetailPage(id) { const link = this.getPageUrl(id) return new Promise((resolve, reject) => { let imgInfo = { sample: '', original: '', } if(REyandeResult || REkonachanResult) { promiseFetch(link) .then(res => res.text()) .then(res => { const bodyText = res const dom = domParser(bodyText) const sampleSrc = dom.querySelector('#image').src const originalSrc = dom.querySelector('#highres').href imgInfo = { sample: sampleSrc, original: originalSrc, } resolve(imgInfo) }).catch(e => { console.log(e) reject(e) }) } if(REdanbooruResult) { promiseFetch(link) .then(res => res.text()) .then(res => { const bodyText = res const dom = domParser(bodyText) const sampleSrc = dom.querySelector('#image').src const originalEl = dom.querySelector('.image-view-original-link') const originalSrc = originalEl ? originalEl.href : sampleSrc imgInfo = { sample: sampleSrc, original: originalSrc, } resolve(imgInfo) }).catch(e => { console.log(e) reject(e) }) } if(REgelbooruResult) { promiseFetch(link) .then(res => res.text()) .then(res => { const bodyText = res const dom = domParser(bodyText) const sampleSrc = dom.querySelector('#image').src const originalEl = dom.querySelector("a[rel='noopener']") const originalSrc = originalEl ? originalEl.href : sampleSrc imgInfo = { sample: sampleSrc, original: originalSrc, } resolve(imgInfo) }).catch(e => { console.log(e) reject(e) }) } }) } handleClickImg(e) { let el = e.target let hasEl = false while (el !== document) { if ((REyandeResult || REkonachanResult) && el.tagName.toLowerCase() === 'li') { hasEl = true break; } if ((REdanbooruResult || REgelbooruResult) && el.tagName.toLowerCase() === 'article') { hasEl = true break; } el = el.parentNode } if(!hasEl) return; // press ctrlKey: open in new window, loadInBackground true, won't auto focus if(e.ctrlKey) { let link = `` if(REyandeResult || REkonachanResult) { link = el.querySelector('a.thumb').href } if(REdanbooruResult) { link = el.querySelector('a.post-preview-link').href } if(REgelbooruResult) { link = el.querySelector('a').href } GM_openInTab(link, true) return; } // press altKey: open in new window, loadInBackground false, will auto focus if(e.altKey) { let link = `` if(REyandeResult || REkonachanResult) { link = el.querySelector('a.thumb').href } if(REdanbooruResult) { link = el.querySelector('a.post-preview-link').href } if(REgelbooruResult) { link = el.querySelector('a').href } GM_openInTab(link, false) return; } const cbEl = el.getElementsByClassName('checkbox')[0] cbEl.checked = !cbEl.checked cbEl.checked ? el.classList.add('imgItemChecked') : el.classList.remove('imgItemChecked') this.updateBatchCount() } /** * @param {MouseEvent} e * @param {string} mouseEventName mouseover | mouseout * **/ async setTransition(e, mouseEventName) { let el = e.target let hasEl = false while (el !== document) { if ((REyandeResult || REkonachanResult) && el.tagName.toLowerCase() === 'li') { hasEl = true break; } if ((REdanbooruResult || REgelbooruResult) && el.tagName.toLowerCase() === 'article') { hasEl = true break; } el = el.parentNode } if(!hasEl) return; if(mouseEventName === 'mouseout') { el.classList.remove('imgTransform') this.hoverEl = null } if(mouseEventName === 'mouseover') { this.hoverEl = el } } updateBatchCount() { let checked = 0; $$('.checkbox').forEach(function (checkbox) { if (checkbox.checked) { ++checked; } }); this.batchCount = checked; const btn = $('#buttonSelectAll') if (this.batchCount >= 1) { btn.innerHTML = "DeselectAll " + this.batchCount + " Image"; } else { btn.innerHTML = "SelectAll " + this.batchCount + " Image"; } } selectAll() { $$('.checkbox').forEach(function (checkbox) { checkbox.checked = true; checkbox.parentNode.parentNode.classList.add('imgItemChecked'); }); this.updateBatchCount(); } deselectAll() { $$('.checkbox').forEach(function (checkbox) { checkbox.checked = false; checkbox.parentNode.parentNode.classList.remove('imgItemChecked'); }); this.updateBatchCount(); } } const booruDownloader = new BooruDownloader() window.booruDownloader = booruDownloader })();