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.

Per 05-12-2022. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name Booru+
// @namespace -
// @version 1.0.2
// @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 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;
    }

    .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)
                })
            }

            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 scrollToEl = isComms ? document.querySelector('.image-sublinks, .response-list') ?? document.documentElement : document.documentElement

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