nHentai QoL

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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