Better Rule34

A script to improve the use of rule34!

As of 05. 08. 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         Better Rule34
// @name:fr      Meilleure règle 34
// @namespace    http://tampermonkey.net/
// @version      0.4.1
// @description  A script to improve the use of rule34!
// @description:fr Un script pour améliorer l'utilisation de rule34!
// @author       You
// @match        https://rule34.xxx/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

const settings = `{
    "imageResizeNotice" : "resize",
    "theme": "dark",
    "undeletePosts" : "true",
    "smallerPosts" : "false",
    "clickAnywhereToStart" : "true",
    "htmlVideoPlayer" : "false"
}`;
const settingsObject = JSON.parse(settings);

const dark = {
    "primary": "#121212",
    "secondary": "#000011",
    "contrast" : "#4a4a4a",
    "complementary" : "#666666"
};

const themes = {
    "dark": dark
}




const params = new URLSearchParams(window.location.search);

function setTheme(){
    let currentTheme = themes[settingsObject.theme];
    if(currentTheme){
        const css = `
            body {
                background-color: ${currentTheme.primary};
            }
            .flat-list{
                background-color: ${currentTheme.secondary};
            }
            div#header ul#subnavbar {
                background-color: ${currentTheme.secondary};
            }
            div#header ul#navbar li.current-page {
                background-image: url(https://imgs.search.brave.com/77L3MmxBu09NuN5WiX4HlbmWjjUe7eAsmBbakS7-DTo/rs:fit:120:120:1/g:ce/aHR0cHM6Ly91cGxv/YWQud2lraW1lZGlh/Lm9yZy93aWtpcGVk/aWEvY29tbW9ucy90/aHVtYi8wLzAyL1Ry/YW5zcGFyZW50X3Nx/dWFyZS5zdmcvMTIw/cHgtVHJhbnNwYXJl/bnRfc3F1YXJlLnN2/Zy5wbmc);;
            }
            .current-page {
                background-color: ${currentTheme.secondary};
                background-color: brightness(110%);
            }
            .manual-page-chooser>input[type=text]{
                background-color: ${currentTheme.secondary};
            }
            .manual-page-chooser>input[type=submit]{
                background-color: ${currentTheme.secondary};
                color: ${currentTheme.contrast};
            }
            div.tag-search input[type=text]{
                background-color: ${currentTheme.secondary};
                color: ${currentTheme.contrast};
            }
            div.tag-search input[type=submit]{
                background-color: ${currentTheme.secondary};
                color: ${currentTheme.contrast};
            }
            .col2 {
                color: ${currentTheme.contrast};
            }
            h6 {
                color: ${currentTheme.contrast};
            }
            h5 {
                color: ${currentTheme.contrast};
            }
            .tag-count {
                color: ${currentTheme.contrast};
            }
            b {
                color: ${currentTheme.contrast};
            }
            li {
                color: ${currentTheme.contrast};
            }
            ul {
                color: ${currentTheme.contrast};
            }
            button {
                background-color: ${currentTheme.secondary};
                color: ${currentTheme.contrast};
                box-sizing: border-box;
                border: 1px solid;
                margin-top: 3px;
                border-color: ${currentTheme.contrast};
            }
            table.highlightable td {
                color: ${currentTheme.contrast};
            }
            h2 {
                color: ${currentTheme.contrast};
            }
            table.form p {
                color: ${currentTheme.contrast};
            }
            table {
                color: ${currentTheme.contrast};
            }
            label {
                color: ${currentTheme.contrast};
            }
        `;
        GM_addStyle(css);
        const thumbs = document.querySelectorAll(".thumb");
        thumbs.forEach(thumb => {
            const images = thumb.getElementsByTagName("img");
            for (let i = 0; i < images.length; i++) {
                images[i].style.border = `3px solid ${currentTheme.complementary}`;
            }
        });

        const e=document.getElementById("user-index");e&&[...e.getElementsByTagName("p")].map(e=>(e.style.color=currentTheme.contrast));


        if(settingsObject.resizePosts == "true" && window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=view")){
            GM_addStyle(".content{max-height: 45%; max-width: 45%; overflow: auto;}");
            document.getElementById("image").style.maxHeight = "50%";
            document.getElementById("image").style.maxWidth = "fit-content";
            document.getElementById("image").style.overflow = "auto";
        }
    }
}

let randNum

function getFromRule34(tags, index, limit) {
    let pid = Math.floor(index / 42); // Change 'const' to 'let'
    const pidRemainder = index % 42;

    if (pidRemainder <= 0) {
        pid = Math.max(0, pid - 1);
    }

    const url = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&tags=${encodeURIComponent(tags)}&limit=${limit}&pid=${pid}&json=1`;

    return fetch(url)
        .then(response => response.json())
        .then(data => {
            const slicedData = data.slice(pidRemainder);
            return slicedData;
        });
}


function getFromRule34WithId(id) {

    const url = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&id=${id}&json=1`;

    return fetch(url)
        .then(response => response.json())
        .then(data => {
            return data[0];
        });
}


function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

function getTagsFromUrl(currentUrl) {
    if(currentUrl.startsWith("https://rule34.xxx/index.php?page=post&s=list&tags=")) {
        return currentUrl.replace("https://rule34.xxx/index.php?page=post&s=list&tags=", "");
    }
}

function creatLinks() {
    try {
        if (window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=list&tags=")) {
            var anchors = document.getElementsByClassName("image-list")[0].getElementsByTagName("a");

            if (anchors.length > 0) {
                for (var i = 0; i < anchors.length; i++) {
                    const urlParams = new URLSearchParams(window.location.search);
                    anchors[i].href = anchors[i].href + "&srchTags=" + getTagsFromUrl(window.location.href) + "&index=" + (i + parseInt(urlParams.get("pid"))).toString();
                }
            } else {
                throw new Error("No elements found with class name 'image-list' or no anchor elements found within that class.");
            }
        } else {
            throw new Error("The current URL does not start with 'https://rule34.xxx/index.php?page=post&s=list&tags='.");
        }
    } catch (error) {
        console.error("An error occurred in creatLinks: " + error);
    }
}


function nextPost() {
    const urlParams = new URLSearchParams(window.location.search);
    const srchTags = urlParams.get("srchTags");
    const currentIndex = parseInt(urlParams.get("index"));

    if (!srchTags || isNaN(currentIndex)) {
        console.error("Invalid URL parameters. Cannot proceed.");
        return;
    }

    const nextIndex = currentIndex + 1;
    const limit = 1000;

    // Preload data for the next post in parallel
    const nextPostPromise = getFromRule34(srchTags, nextIndex, 1);

    getFromRule34(srchTags, currentIndex, limit)
        .then(jsonInfo => {
            if (!jsonInfo || !jsonInfo.length) {
                console.error("No data received from API. Cannot proceed.");
                return;
            }

            const nextPostId = jsonInfo[0].id;
            const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${nextPostId}&srchTags=${encodeURIComponent(srchTags)}&index=${nextIndex}`;

            // Handle case where currentIndex is the first post on a page
            if (currentIndex % 42 === 0) {
                const nextPageUrl = `https://rule34.xxx/index.php?page=post&s=list&tags=${encodeURIComponent(srchTags)}`;
                window.location.href = nextPageUrl;
                return;
            }

            // Navigate to the next post once it's ready (and the data is preloaded)
            nextPostPromise.then(() => {
                window.location.href = newUrl;
            });
        })
        .catch(error => {
            console.error("An error occurred during API request:", error);
        });
}









function backPost() {
    const urlParams = new URLSearchParams(window.location.search);
    const srchTags = urlParams.get("srchTags");
    const currentIndex = parseInt(urlParams.get("index"));

    if (!srchTags || isNaN(currentIndex)) {
        console.error("Invalid URL parameters. Cannot proceed.");
        return;
    }

    const nextIndex = currentIndex - 1;
    const limit = 1000;

    // Preload data for the next post in parallel
    const nextPostPromise = getFromRule34(srchTags, nextIndex, 1);

    getFromRule34(srchTags, currentIndex, limit)
        .then(jsonInfo => {
            if (!jsonInfo || !jsonInfo.length) {
                console.error("No data received from API. Cannot proceed.");
                return;
            }

            const nextPostId = jsonInfo[0].id;
            const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${nextPostId}&srchTags=${encodeURIComponent(srchTags)}&index=${nextIndex}`;

            // Navigate to the next post once it's ready (and the data is preloaded)
            nextPostPromise.then(() => {
                window.location.href = newUrl;
            });
        })
        .catch(error => {
            console.error("An error occurred during API request:", error);
        });
}

async function randomVideo() {
    const urlParams = new URLSearchParams(window.location.search);
    let srchTags = urlParams.get("tags");

    if (!srchTags) {
        // If tags parameter is not found in the URL, get the value from the input element
        const tagsInput = document.querySelector("input[name='tags']");
        srchTags = tagsInput.value.replace(/ /g, "+");
    }

    const posts = await getFromRule34(srchTags, 1, 1000);

    if (posts.length === 0) {
        console.error("No posts found for the given tags. Cannot proceed.");
        return;
    }

    const randNum = Math.floor(Math.random() * posts.length);
    const postId = posts[randNum].id;

    const newUrl = `https://rule34.xxx/index.php?page=post&s=view&id=${postId}&tags=${encodeURIComponent(srchTags)}&index=${randNum}`;
    window.location.href = newUrl;
}




function makeButtons(){
    let btn = document.createElement("button");
    btn.innerHTML = "Random";
    btn.onclick = randomVideo;
    if(document.getElementsByClassName("tag-search")[0]){document.getElementsByClassName("tag-search")[0].appendChild(btn)};
    if(document.getElementsByClassName("image-sublinks")[0]){
        let btn3 = document.createElement("button");
        btn3.innerHTML = "back";
        btn3.onclick = backPost;
        document.getElementsByClassName("image-sublinks")[0].appendChild(btn3);
        let btn2 = document.createElement("button");
        btn2.innerHTML = "next";
        btn2.onclick = nextPost;
        document.getElementsByClassName("image-sublinks")[0].appendChild(btn2);
    }
}

const imageSublinks = document.getElementsByClassName("image-sublinks")[0];
if (imageSublinks) {
    document.addEventListener("keydown", function(event) {
        // Check if the active element is an input element
        const activeElement = document.activeElement;
        const isInputElement = activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA";

        // If it's an input element, don't execute the functions
        if (isInputElement) {
            return;
        }

        // If it's not an input element, execute the functions based on the key press
        if (event.keyCode === 39) {
            nextPost();
        } else if (event.keyCode === 37) {
            backPost();
        }
    });
}


async function addDeletedPosts(id){
    if(document.getElementById("status-notices")){
        if(document.getElementsByClassName("status-notice")[0].firstChild.data.startsWith("This post was")){
            const urlParts = window.location.href.split("&");
            let notices = document.getElementById("status-notices");
            let video = document.createElement("video");
            try {
                const videoJson = await getFromRule34WithId(id);
                video.src = videoJson.file_url;
                video.controls = true;
                video.style = "max-height: 70%; max-width: 70%; overflow: auto;";
                document.getElementById("fit-to-screen").appendChild(video);
                document.getElementById("status-notices").remove()
            } catch (e) {
                console.error(e);
            }
        } else {
            console.log("This post is not deleted.")
        }
    } else {
        console.log("The status-notices element is not present on this page.")
    }
}

function getLinksInDiv(element) {
    var elements = element.parentNode.querySelectorAll("a");
    return(elements[elements.length - 1])
}

function resizePostPopup(){
    try {
        if(settingsObject.imageResizeNotice == "resize"){$('resized_notice').hide()}
        if(settingsObject.imageResizeNotice == "orignal"){Post.highres(); $('resized_notice').hide();}
    } catch (e) {
        console.error(e);
    }
}

function startVideo(){
    if (document.getElementById("gelcomVideoPlayer_fluid_initial_play")) {
        document.getElementById("gelcomVideoPlayer").autoplay = true;
    }
}

function addTagButtons(){
    const classList = ["tag-type-copyright", "tag-type-general", "tag-type-character", "tag-type-artist","tag-type-metadata"]
    for (const curClass of classList) {
        const elements = document.getElementsByClassName(curClass);

        for (const element of elements) {
            const button = document.createElement("button");
            button.innerHTML = "+";
            button.onclick = function() {console.log(" " + getLinksInDiv(this).innerText); document.getElementsByName("tags")[0].value += " " + (getLinksInDiv(this).innerText).replaceAll(" ","_")}
            element.insertBefore(button, element.firstChild);
        }
    }
}

function stretchyDiv(isImage){
    let div;
    if(isImage == 0){div = document.getElementById("fluid_video_wrapper_gelcomVideoPlayer")} else {div = document.getElementById("image")}
    if(isImage == 1){
        let newDiv = document.createElement("div");
        newDiv.style.position = "relative";
        div.parentNode.insertBefore(newDiv, div);
        newDiv.appendChild(div);
        div = newDiv;
        document.getElementById("image").maxHeight = 9999
    }
    const resizer = document.createElement("div");
    resizer.style.width = "10px";
    resizer.style.height = "10px";
    resizer.style.backgroundColor = "white";
    resizer.style.position = "absolute";
    resizer.style.bottom = "0";
    resizer.style.right = "0";
    resizer.style.cursor = "se-resize";
    resizer.style.zIndex = "10";


    let isResizing = false;
    let currentX;
    let currentY;
    let initialWidth;
    let initialHeight;

    resizer.addEventListener("mousedown", function(e) {
        document.body.style.userSelect = 'none';
        isResizing = true;
        currentX = e.clientX;
        currentY = e.clientY;
        initialWidth = parseFloat(getComputedStyle(div, null).getPropertyValue("width").replace("px", ""));
        initialHeight = parseFloat(getComputedStyle(div, null).getPropertyValue("height").replace("px", ""));
    });

    document.addEventListener("mouseup", function() {
        document.body.style.userSelect = '';
        isResizing = false;
    });

    document.addEventListener("mousemove", function(e) {
        if (isResizing) {
            let inner = div.getElementsByTagName("img")[0];
            let newWidth = initialWidth + (e.clientX - currentX);
            let newHeight = initialHeight + (e.clientY - currentY);
            if (e.shiftKey) {
                // Resize both width and height at the same rate
                let ratio = initialWidth / initialHeight;
                newHeight = newWidth / ratio;
            }
            if(isImage == 1){inner.style.width = newWidth + "px"; inner.style.height = newHeight + "px";}
            div.style.width = newWidth + "px";
            div.style.height = newHeight + "px";
        }
    });

    div.appendChild(resizer);
}

function addInputBox(){
    let inputBox = document.createElement("input");
    let tagsElement = document.querySelector("[name='tags']");
    inputBox.type = "text"
    tagsElement.after(inputBox);
}

setTimeout(function(){
    if (document.getElementById("fluid_video_wrapper_gelcomVideoPlayer")) {
        stretchyDiv(0);
    } else if (document.getElementById("image")) {
        stretchyDiv(1);
    }
}, 300);

function setTags(tags){
    document.getElementsByName("tags")[0].value = tags
}

async function htmlVideoPlayer(id){
    const urlParts = window.location.href.split("&");
    let notices = document.getElementById("status-notices");
    let video = document.createElement("video");
    try {
        const videoUrl = await getFromRule34(id, 0 ,1);
        video.src = videoUrl;
        video.controls = true;
        video.style = "max-height: 70%; max-width: 70%; overflow: auto;";
        document.getElementById("fit-to-screen").appendChild(video);
        document.getElementById("status-notices").remove()
    } catch (e) {
        console.error(e);
    }
}

function addCloseButtonToStatusNotice() {
    const statusNoticeElements = document.querySelectorAll('.status-notice');

    statusNoticeElements.forEach(element => {
        const closeButton = document.createElement('button');
        closeButton.textContent = 'x';
        closeButton.addEventListener('click', () => {
            element.parentNode.removeChild(element);
        });
        closeButton.style.background = 'none';
        closeButton.style.border = 'none';
        closeButton.style.cursor = 'pointer';
        element.appendChild(closeButton);
    });
}

function convertSearchToLink(){
    document.getElementsByName("commit")[0].innerHTML = `<a href="https://rule34.xxx/index.php?page=post&s=list&tags=all">${document.getElementsByName("commit")[0].innerHTML}</a>`
}

if(window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=view")){
    setTags(params.get("srchTags"))
} else if(window.location.href.startsWith("https://rule34.xxx/index.php?page=post&s=list")){
    setTags(params.get("tags"))
}

let isFirstClick = true;

document.addEventListener("click", function() {
    if (isFirstClick) {
        startVideo()
        isFirstClick = false;
    }
});
makeButtons();
creatLinks()
addCloseButtonToStatusNotice()
setTheme()
setTimeout(resizePostPopup, 100)
setTimeout(addTagButtons, 100)
if(settingsObject.undeletePosts == "true"){setTimeout(addDeletedPosts(window.location.href.split("&").find(part => part.startsWith("id=")).replace("id=", "")), 500);}
if(settingsObject.htmlVideoPlayer == "true"){setTimeout(htmlVideoPlayer(window.location.href.split("&").find(part => part.startsWith("id=")).replace("id=", "")), 500);}