nHentai QoL

Tracks history, removes annoying characters from comments, and links replies in comments

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         nHentai QoL
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  Tracks history, removes annoying characters from comments, and links replies in comments
// @author       Exiua
// @match        https://nhentai.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nhentai.net
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

// Blacklisted tags will be highlighted red
const blacklist = [];
// Warning tags will be highlighted orange
const warning = [];

const commentCleaningRegex = /\u3662+/gm;
const postIdRegex = /\/g\/(\d+)\/?/gm;
const pageRegex = /\/\d+\/\d+\//gm;

(function() {
    'use strict';

    window.addEventListener('load', main);
    //setTimeout(main, 10000);
})();

function main(){
    checkIfReloadNeeded();

    const currentUrl = window.location.href;
    if(currentUrl.includes("/g/")){
        commentSectionHandler();
        handlePost(currentUrl);
    }
    else{
        // Add favorites to history
        /*if(currentUrl.includes("/favorites/")){
            favLoader();
        }*/
        const mainPage = currentUrl == "https://nhentai.net/";
        handleGallery(mainPage);
    }
}

function favLoader(){
    const container = document.getElementById("favcontainer");
    for(const post of container.children){
        const id = post.getAttribute("data-id");
        const caption = post.getElementsByClassName("caption")[0];
        const title = caption.innerText;
        GM_setValue(id, title);
    }
    console.log("done");
    const nextBtn = document.getElementsByClassName("next")[0];
    nextBtn.click();
}

function handlePost(url){
    const match = postIdRegex.exec(url);
    const postId = match[1];
    const title = document.getElementsByClassName("title")[0].innerText;
    GM_setValue(postId, title);
    if(pageRegex.test(url)){
        return;
    }

    const tagContainer = document.getElementById("tags");
    for(const tagSet of tagContainer.children){
        const tags = tagSet.getElementsByClassName("tags")[0];
        checkTags(tags);
    }

    const container = document.getElementById("related-container");
    checkPosts(container);
}

function checkTags(tags){
    for(const tag of tags.children){
        const nameTag = tag.getElementsByClassName("name")[0];
        if(nameTag == null){
            continue;
        }

        const name = nameTag.innerText;
        if(blacklist.includes(name)){
            addColor(tag, "#FF0000");
        }
        else if(warning.includes(name)){
            addColor(tag, "#FF9900");
        }
    }
}

function addColor(elem, color){
    elem.setAttribute("style", "color: " + color + ";");
}

function handleGallery(mainPage){
    if(mainPage){
        const popularContainer = document.getElementsByClassName("container index-container index-popular")[0];
        checkPosts(popularContainer);
    }

    const container = document.getElementsByClassName("container index-container")[0];
    checkPosts(container);
}

function checkPosts(postsParent){
    for(const post of postsParent.children){
        if(post.tagName != "DIV"){
            continue;
        }

        const classes = post.getAttribute("class");
        if (classes.includes("blacklisted")){
            continue;
        }

        const anchor = post.getElementsByTagName("a")[0];
        const href = anchor.getAttribute("href");
        postIdRegex.lastIndex = 0;
        const match = postIdRegex.exec(href);
        if(GM_getValue(match[1]) == null){
            continue;
        }

        fadePost(post);
    }
}

function fadePost(post){
    const img = post.getElementsByTagName("img")[0];
    img.setAttribute("style", "opacity: 20%;");
}

function checkIfReloadNeeded(){
    const errors = document.getElementsByTagName("h1");
    if(errors.length == 0){
        return;
    }

    const error = errors[0];
    if(error.innerText != "429 Too Many Requests"){
        return;
    }

    const timeout = getRandomInt(500, 1000);
    setTimeout(reload, timeout);
}

function reload(){
    location.reload();
}

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function cleanText(){
    const comments = document.getElementById("comments");
    for(const child of comments.children){
        const commentBody = child.getElementsByClassName("body")[0];
        let oldText = commentBody.innerText;
        let newText = oldText.replace(new RegExp("[\\u0300-\\u0e4e]+", "gm"), "").replace(commentCleaningRegex, "");
        commentBody.innerText = newText;
        if(newText !== oldText){
            console.log("Cleaned comment");
        }

        const username = child.getElementsByClassName("left")[0].getElementsByTagName("a")[0];
        oldText = commentBody.innerText;
        newText = oldText.replace(new RegExp("[\\u0300-\\u0e4e]+", "gm"), "").replace(commentCleaningRegex, "");
        commentBody.innerText = newText;
        if(newText !== oldText){
            console.log("Cleaned username");
        }
    }
}

function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

function commentSectionHandler(){
    console.log("Waiting for comments");
    waitForElm("#comments > .comment").then((_) => {
        console.log("Found comments");
        setTimeout(() => {
            cleanText();
            linkReplies();
            anchorTagLinks();
        }, 100);
    });
}

function anchorTagLinks(){
    const comments = document.getElementById("comments");
    const children = comments.children;
    for(const comment of children){
        const body = comment.getElementsByClassName("body")[0];
        const words = body.innerHTML.split(" ");
        const links = [];
        let i = 0;
        for(const word of words){
            const [wordPart, punctuation] = getPunctuation(word);
            if (isNumeric(wordPart)){
                const link = {
                    text: wordPart,
                    href: `https://nhentai.net/g/${wordPart}/`,
                    punctuation: punctuation,
                    index: i,
                };
                links.push(link);
                //console.log(link);
            }
            else if(wordPart.startsWith("https://")){
                const link = {
                    text: wordPart,
                    href: wordPart,
                    punctuation: punctuation,
                    index: i,
                };
                links.push(link);
                //console.log(link);
            }
            i++;
        }

        const html = body.innerHTML.split(" ");
        for(const link of links){
            const anchorRaw = `<a href="${link.href}">${link.text}</a>`;
            const anchor = link.punctuation != null ? anchorRaw + link.punctuation : anchorRaw;
            //const toReplace = link.punctuation != null ? link.text + link.punctuation : link.text;
            //html.replace(toReplace, anchor);
            html[link.index] = anchor;
            console.log(html);
        }

        body.innerHTML = html.join(" ");
        //console.log(body);
    }
}

function getPunctuation(word) {
    const punctuation = ['.', ',', '!', '?', ';', ':'];
    let lastChar = word[word.length - 1];

    if (punctuation.includes(lastChar)) {
        return [word.slice(0, -1), lastChar];
    }

    return [word, null];
}

// See: https://stackoverflow.com/a/175787
function isNumeric(str) {
    if (typeof str != "string") return false // we only process strings!
    return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
        !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
}

function linkReplies(){
    const comments = document.getElementById("comments");
    const children = comments.children;
    let spaces = 0;
    const commentLinks = {};
    for(let i = children.length - 1; i >= 0; i--){
        const child = children[i];
        const id = child.getAttribute("id");
        const bodyWrapper = child.getElementsByClassName("body-wrapper")[0];
        const username = bodyWrapper.getElementsByTagName("a")[0].innerText.toLowerCase().trim();
        //console.log("Username: \"" + username + "\"");
        const spaceCount = (username.match(/ /g) || []).length;
        spaces = Math.max(spaceCount, spaces);
        //console.log("Max Spaces: " + spaces);
        commentLinks[username] = id;
        const body = child.getElementsByClassName("body")[0];
        const bodyParts = body.innerText.split(" ");
        let bodyPartIndex = -1;
        for(const part of bodyParts){
            bodyPartIndex++;
            if(!part.startsWith("@")){
                continue;
            }

            let found = false;
            for(let j = 0; j <= spaces && !found; j++){
                // Remove '@'
                const extraParts = bodyParts.slice(bodyPartIndex + 1, bodyPartIndex + 1 + j).join(" ");
                const rawUsername = part.substring(1) + (extraParts === "" ? "" : " " + extraParts);
                const username = rawUsername.toLowerCase();
                //console.log("Tagged Username: \"" + username + "\"");
                for(const user in commentLinks){
                    //console.log(`Checking: ${user} === ${username}, Result: ${user === username}`);
                    if(user !== username && !username.startsWith(user)){
                        continue;
                    }

                    const id = commentLinks[user];
                    const anchor = "<a href=\"#" + id + "\">@" + rawUsername + "</a>";
                    const relpaceTarget = "@" + rawUsername;
                    //console.log("Replace Target: " + relpaceTarget + ", Replacement: " + anchor);
                    let bodyHtml = body.innerHTML;
                    bodyHtml = bodyHtml.replace(relpaceTarget, anchor);
                    //console.log(bodyHtml);
                    body.innerHTML = bodyHtml;
                    //console.log(body.innerHTML, body);
                    found = true;
                    break;
                }
            }
        }
    }
}