Better Rule34

A script to improve the use of rule34!

Fra 05.08.2023. Se den seneste versjonen.

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