Sleazy Fork is available in English.

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.

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