Booru+

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.

As of 08. 03. 2023. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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
    }
})()