您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
adds new useful features for booru-like websites, such as: ad block, back to top button, fast image view etc. works for rule34.xxx, e621.net and more.
// ==UserScript== // @name Booru+ // @namespace - // @version 1.0.4 // @description adds new useful features for booru-like websites, such as: ad block, back to top button, fast image view etc. works for rule34.xxx, e621.net and more. // @author NotYou // @match *://rule34.xxx/* // @match *://e621.net/* // @match *://e926.net/* // @match *://tbib.org/* // @match *://gelbooru.com/* // @match *://danbooru.donmai.us/* // @match *://hypnohub.net/* // @match *://xbooru.com/* // @match *://safebooru.org/* // @run-at document-end // @compatible Firefox Version 78 // @compatible Chrome Version 88 // @compatible Edge Version 88 // @compatible Opera Version 74 // @compatible Safari Version 14 // @license GPL-3.0-or-later // @grant none // ==/UserScript== (function() { let adBlockEnabled = 'true' // change to 'false' if you want to support original website authors // Image View let movementSensitivity = 1 let scaleSensitivity = 1 // Selectors let contentSelector = location.host === 'xbooru.com' ? 'html > body' : '#content, #static-index, #page, #container' let firstPosts = ':where(.thumb, article.post-preview, .thumbnail-preview):not(:where(:nth-child(3) ~ *))' let thumbSelector = '.thumb img:not([style*="border"]), .post-preview:not([data-tags*="video"]) img, .thumbnail-preview img' let navigationSelector = '#subnavbar, #container header, #nav > :last-child' let tagSelector = '[class*="tag-type"] a:not([href*="://"]), .search-tag' let paginatorSelector = '#paginator, .paginator' let pagesSelector = '#paginator a, [class*="tag"] a:nth-last-child(2), .paginator .numbered-page a, .arrow a' let postListSelector = '#post-list, #c-posts, .thumbnail-container' // Styling let accentColor = (function() { switch(location.host) { case 'rule34.xxx': return 'rgb(147, 195, 147)' case 'e621.net': case 'e926.net': return 'rgb(21, 47, 86)' case 'tbib.org': case 'gelbooru.com': case 'danbooru.donmai.us': case 'safebooru.org': return 'rgb(7, 115, 251)' case 'hypnohub.net': return 'rgb(238, 136, 135)' case 'xbooru.com': return 'rgb(122, 89, 30)' default: return 'rgb(0, 0, 0)' } })() let css = ` :root { --accent: ${accentColor}; } ${strToBool(adBlockEnabled) ? ` /* rule34.xxx */ #post-list > :not(:where(.content, .sidebar)), [src*="${location.origin}/images/"], [href*="kanako.store"], #navbar > :nth-last-child(3), #paginator ~ .horizontalFlexWithMargins, /* e621.net / e926.net */ #ad-leaderboard, .adzone, /* tbib.org / xbooru.com */ iframe[width][height][frameborder="0"], /* gelbooru.org */ .headerAd, .exo_wrapper, .exo-native-widget-outer-container, .exo-native-widget, .footerAd, .footerAd2, main .mainBodyPadding > :where(:first-child, :last-child), /* hypnohub.net */ #content #right-col .flexi > :not([id]), #post-list .content > span[data-nosnippet] { display: none !important; }` : ''} .loading::before { content: ''; width: 50px; height: 50px; display: block; border-radius: 50%; border: 10px solid rgb(255, 255, 255); border-top: var(--accent) 10px solid; position: fixed; animation: 1s infinite loading-ani; left: calc(50% - 25px); top: calc(50% - 25px); z-index: 2147483646; } .loading { opacity: 0.65; } #backtotop { position: fixed; right: 10px; bottom: 10px; width: 50px; height: 50px; border-radius: 50%; background-color: rgba(0, 0, 0, .35); transition: .3s; } #backtotop { cursor: pointer; } #backtotop::before { transform: rotate(45deg); left: 20px; top: 20px; } #backtotop::after { transform: rotate(-45deg); left: 10px; top: 13px; } #backtotop::after, #backtotop::before { content: ''; display: block; width: 20px; height: 8px; background-color: var(--accent); position: relative; border-radius: 10px; } .image-view { position: fixed; left: 0; top: 0; transition: .5s opacity; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, .7); z-index: 100; } .image-view img { height: 100%; position: absolute; background-color: rgba(0, 0, 0, .1); } .image-view div { position: fixed; right: 5px; top: 5px; width: 50px; height: 50px; opacity: .5; transition: .2s opacity; background: none; } .image-view div:hover { opacity: 1; } .image-view div::before, .image-view div::after { content: ''; display: block; width: 100%; height: 4px; background-color: rgb(255, 255, 255); position: absolute; top: calc(50% - 5px); border-radius: 10px; } .image-view div::before { transform: rotate(45deg); } .image-view div::after { transform: rotate(-45deg); } .blockquote { padding-left: 15px; border-left: 5px solid var(--accent); border-radius: 2px; } #tag-view { position: absolute; z-index: 50; background-color: var(--accent); border-radius: 10px; padding-top: 20px; left: 0; top: 0; display: flex; } #tag-view::before { content: ''; display: block; position: absolute; top: -8px; z-index: 40; border-right: transparent 15px; border-top: transparent 15px; border-left: var(--accent) 15px; border-bottom: var(--accent) 7.5px; border-style: solid; } .error-notification-box { position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 2147483647; background: rgba(0, 0, 0, .5); } .error-notification-body { width: 70%; height: 40%; background: var(--accent); position: absolute; padding: 1em; left: calc(15% - 1em); /* 15% = 100% - 70% / 2 */ top: calc(30% - 1em); /* 30% = 100% - 40% / 2 */ border-radius: 4px; } .error-notification-body h1 { display: block; width: 100%; } @keyframes loading-ani { 0% { rotate: 0deg } 100% { rotate: 360deg } }` let style = document.createElement('style') style.appendChild(document.createTextNode(css)) document.head.appendChild(style) init(location.search.includes('s=view') && document.querySelector(pagesSelector)) window.addEventListener('popstate', () => { replaceBody(location.href) }) function init(isComms) { let postList = document.querySelector(postListSelector) let content = document.querySelector(contentSelector) let tags = document.querySelectorAll(tagSelector) let enterTimeout let show = true if(tags.length) { let tagView = document.createElement('div') tagView.id = 'tag-view' for (let i = 0; i < tags.length; i++) { let tag = tags[i] tag.addEventListener('mouseenter', () => { show = true enterTimeout = setTimeout(() => { let url = tag.href getDocument(url).then(doc => { if(show) { let thumbs = doc.querySelectorAll(firstPosts) let result = '' for (let j = 0; j < thumbs.length; j++) { let thumb = thumbs[j].outerHTML result += thumb } tagView.innerHTML = result setVisibility(1, tagView) tagView.style.left = tag.offsetLeft + 'px' tagView.style.top = tag.offsetTop + tag.offsetHeight + 8 + 'px' } }) }, 1e3) }) tag.addEventListener('mouseleave', () => { setVisibility(0, tagView) clearTimeout(enterTimeout) show = false }) } setVisibility(0, tagView) content.appendChild(tagView) } if((location.search.includes('s=list') || matches(/\/posts[^\/]/)) || postList) { let searchForm = document.querySelector('form[action*="search"]') if(searchForm) { searchForm.addEventListener('submit', e => { e.preventDefault() let url = location.origin + '/index.php?page=post&s=list&tags=' + searchForm.querySelector('input[name="tags"]').value.replace(/\s/g, '+') replaceBody(url) }) } if(postList) { fastView() } function fastView() { let fastViewEl = document.createElement('div') let f_img = document.createElement('img') let f_close = document.createElement('div') fastViewEl.className = 'image-view' setVisibility(0, fastViewEl) fastViewEl.appendChild(f_img) fastViewEl.appendChild(f_close) fastViewEl.addEventListener('contextmenu', e => { if(e.target !== f_img) { e.preventDefault() } }) postList.addEventListener('contextmenu', e => { if(e.target.matches(thumbSelector)) { e.preventDefault() } }) postList.addEventListener('auxclick', e => { if(e.button === 2 && e.which === 3) { e.preventDefault() let image = e.target if(image.matches(thumbSelector)) { setVisibility(1, fastViewEl) getDocument(image.closest('a').href).then(doc => { try { let _image = doc.querySelector('#image') f_img.src = _image.poster || _image.src } catch(_) { notifyError('cannot get access to image') } }) f_img.addEventListener('load', () => { f_img.style.left = `calc(50% - ${f_img.offsetWidth / 2}px)` let scale = 1 let x = 0, y = 0 let reTranslate = /translate\(.*?\)/ let reScale = /scale\(.*?\)/ let _y = window.pageYOffset f_img.addEventListener('mousedown', e => { e.preventDefault() window.addEventListener('mousemove', onMouseMove) }) window.addEventListener('mouseup', () => { window.removeEventListener('mousemove', onMouseMove) }) f_img.addEventListener('wheel', e => { window.scrollTo(window.pageXOffset, _y) _y = window.pageYOffset scale = convert(scale, 0.5, 5, 0.1 * scaleSensitivity) // scale - current; 0.5 - minimal; 5 - maximal; 0.1 - difference; setTransform(reScale, `scale(${scale})`) function convert(n, min, max, diff) { return Math.max(min, Math.min(max, (n + (e.deltaY < 0 ? diff : (diff * -1))))) } }) function onMouseMove(e) { x += convert(e.movementX) y += convert(e.movementY) setTransform(reTranslate, `translate(${x}px, ${y}px)`) function convert(value) { return value / scale * movementSensitivity } } function setTransform(re, prop) { f_img.style.transform = f_img.style.transform.replace(re, prop) } }) } } }) let defaultFImgStyle = 'scale(1) translate(0, 0)' fastViewEl.addEventListener('click', e => { if(e.target.tagName.toLowerCase() === 'div') { setVisibility(0, fastViewEl) f_img.style.transform = defaultFImgStyle f_img.removeAttribute('src') } }) f_img.style.transform = defaultFImgStyle content.appendChild(fastViewEl) } } if(location.search.includes('s=view')) { let rightCol = document.querySelector('#right-col, #a-show #content') let similarPosts = document.createElement('div') let _tags = Array.from(tags).filter((_, i) => i % 2 !== 0) let amoutOfTags = Math.floor(Math.sqrt(_tags.length)) let tries = 0 if(rightCol) { similarPosts.innerHTML = '<h2>You may also like:</h2>' rightCol.append(similarPosts) getSimilar() function getSimilar() { let _continue = true let searchUrl = location.host === 'danbooru.donmai.us' ? location.origin + '/posts?tags=' : location.origin + '/index.php?page=post&s=list&tags=' let __tags = 0 _tags.forEach(e => { if(Math.floor(Math.random() * 2) === 1 && _continue) { searchUrl += (__tags === 0 ? '' : '+') + e.textContent.replace(/\s/g, '_') __tags++ if(__tags >= amoutOfTags) { _continue = false } } }) getDocument(searchUrl).then(doc => { let posts = doc.querySelectorAll(firstPosts+`:not([id*="${new URLSearchParams(location.search).get('id')}"])`) let _posts = '' tries++ posts.forEach(e => { _posts += e.innerHTML }) similarPosts.innerHTML += _posts if(posts === '' && tries < 3) { getSimilar() } else if (_posts === '') { similarPosts.innerHTML = '<h1>Not Found Similar Posts</h1>' } }) } } let commentList = document.querySelector('#comment-list, .mainBodyPadding') if(commentList && commentList.children.length > 1) { let comments = commentList.querySelectorAll('br ~ [id^="c"], h2 + br ~ div') for (let i = 0; i < comments.length; i++) { let comment = comments[i] if(comment.matches(':not([style])')) { let commentText = comment.querySelector('.col2, .comment-right-col') || comment.querySelector('.commentBody br + br').nextSibling let commentActualText = commentText.textContent.replace(/\\n/g, '').trim() let prevComment = comment let j = 0 if(commentActualText.startsWith('^')) { let col2 = prevComment.querySelector('.col2') while(commentActualText[j++] === '^') { prevComment = prevComment.previousElementSibling } col2 = prevComment.querySelector('.col2') if(col2) { commentText.innerHTML = `<div class="blockquote">${col2.innerHTML}</div>` + commentText.textContent.replace(/\^/g, '') } } } } } } backToTop() function matches(re) { return re.test(location.href) } function backToTop() { let backtotop = document.createElement('div') backtotop.id = 'backtotop' setVisibility(0, backtotop) window.addEventListener('scroll', () => { if(window.pageYOffset > 0) { setVisibility(1, backtotop) } else { setVisibility(0, backtotop) } }) backtotop.addEventListener('click', () => { document.body.scrollIntoView({ block: 'start', behavior: 'smooth' }) }) content.appendChild(backtotop) } function setVisibility(n, el) { if(n === 1) { el.style.opacity = '1' el.style.pointerEvents = 'auto' } else { el.style.opacity = '0' el.style.pointerEvents = 'none' } } initPagesEvs(isComms) } function getStyles(el) { return window.getComputedStyle(el) } function initPagesEvs(isComms) { let pages = document.querySelectorAll(pagesSelector) for (let i = 0; i < pages.length; i++) { pages[i].onclick = e => { e.preventDefault() let target = e.target let url = target.getAttribute('onclick') && isComms ? location.pathname+target.getAttribute('onclick').match(/'(.*?)'/)[1] : target.href target.onclick = null replaceBody(url, isComms) } } } function replaceBody(url, isComms) { let content = document.querySelector(contentSelector) if(content) { content.classList.add('loading') } getDocument(url).then(doc => { let htmlEl = document.documentElement let scrollToEl = isComms ? document.querySelector('.image-sublinks, .response-list') ?? htmlEl : htmlEl let hiddenImages = doc.querySelectorAll('img[data-cfsrc]') if(hiddenImages) { for (let i = 0; i < hiddenImages.length; i++) { let hiddenImage = hiddenImages[i] hiddenImage.style.cssText = '' hiddenImage.src = hiddenImage.dataset.cfsrc } } content.innerHTML = doc.querySelector(contentSelector).innerHTML scrollToEl.scrollIntoView() content.classList.remove('loading') history.pushState('', '', '?'+url.split('?')[1]) document.title = doc.title let autocomplete = window.autocomplete_setup if(autocomplete) { autocomplete() } let paginator = document.querySelector(paginatorSelector) let _paginator = doc.querySelector(paginatorSelector) if(paginator) { paginator.innerHTML = (_paginator || paginator).innerHTML } init(isComms) }).catch(expection => { let timeout = 1e4 notifyError(expection, timeout) setTimeout(() => { location.replace(url) }, timeout) }) } function notifyError(body, time) { let notify = document.createElement('div') let notifBody = document.createElement('div') notify.className = 'error-notification-box' notifBody.className = 'error-notification-body' notifBody.innerHTML = '<h1>Rule34+</h1>Hey! Looks like you just got error, probably that\'s reason: "' + body + '"!, you got do a report about problem <a target="_blank" href="//greasyfork.org/scripts/456048/feedback">here</a>.' if(time) { notifBody.innerHTML += ' You will be redirected in ' + time / 1e3 + ' seconds.' } notify.appendChild(notifBody) document.body.appendChild(notify) } function getDocument(url) { return fetch(url).then(r => r.text()).then(c => new DOMParser().parseFromString(c, 'text/html')) } function strToBool(str) { return str === 'fasle' ? false : true } })()