Sleazy Fork is available in English.

NHentai Tag Highlighter

Highlight thumbnails of works on nhentai.net based on tags you decide to highlight.

// ==UserScript==
// @name         NHentai Tag Highlighter
// @namespace    https://github.com/erasels
// @version      3.1
// @description  Highlight thumbnails of works on nhentai.net based on tags you decide to highlight.
// @author       erasels
// @match        *://nhentai.net/*
// @icon         https://nhentai.net/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_addStyle
// @license      MIT
// @supportURL   https://sleazyfork.org/en/scripts/495767-nhentai-tag-highlighter
// ==/UserScript==

(function() {
    'use strict';

    const highlightCutoffThreshold = 0; // Prevents rendering more than this many tags on an item if this is > 0

    // If no priority is defined for a tag in tagDetails, it'll use this
    const defaultPriority = 10;

    // Colors
    const removeHighlightColor = '#AD2204';
    const addHighlightColor = '#0A900A';

    // Retrieve tags from storage or set default if not present
    const tagDetails = {};
    const tagKeys = GM_listValues();
    tagKeys.forEach(key => {
        if (key.startsWith('tag_')) {
            const tagId = key.slice(4);
            tagDetails[tagId] = GM_getValue(key);
        }
    });

    // Modal dialogue style
    GM_addStyle(`
    #tag-modal {
        display: none;
        position: fixed;
        z-index: 1000;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 300px;
        padding: 20px;
        background-color: #2b2b2b;
        color: #fff;
        border: 1px solid #444;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
        border-radius: 5px;
    }
    #tag-modal label {
        display: block;
        margin-bottom: 5px;
        color: #fff;
    }
    #tag-modal input[type="text"],
    #tag-modal input[type="number"],
    #tag-modal input[type="color"] {
        width: calc(100% - 10px);
        margin-bottom: 10px;
        padding: 5px;
        background-color: #444;
        color: #fff;
        border: 1px solid #555;
        border-radius: 3px;
    }
    #tag-modal button {
        margin-right: 10px;
        background-color: #555;
        color: #fff;
        border: none;
        padding: 5px 10px;
        border-radius: 3px;
        cursor: pointer;
    }
    #tag-modal button:hover {
        background-color: #666;
    }
    #modal-overlay {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 999;
    }
    #existing-tags-container {
        margin-top: 20px;
    }
    #existing-tags-list {
        max-height: 150px;
        overflow-y: auto;
        background-color: #333;
        padding: 10px;
        border: 1px solid #444;
        border-radius: 5px;
    }
    #existing-tags-list div {
        margin-bottom: 5px;
    }
`);


    // Create the modal dialog HTML (the thing that shows up when you click the highlight tag button)
    const modalHtml = `
    <div id="modal-overlay"></div>
    <div id="tag-modal">
        <label for="tag-name">Tag Name:</label>
        <input type="text" id="tag-name">
        <label for="tag-priority">Priority:</label>
        <input type="number" id="tag-priority" value="10">
        <label for="tag-color">Color:</label>
        <input type="color" id="tag-color" value="#32a852">
        <button id="save-tag">Save</button>
        <button id="cancel-tag">Cancel</button>
        <div id="existing-tags-container">
            <h3>Existing Tags</h3>
            <div id="existing-tags-list"></div>
        </div>
    </div>
`;

    const modalContainer = document.createElement('div');
    modalContainer.innerHTML = modalHtml;
    document.body.appendChild(modalContainer);

    // SVG graphic for the star icon
    const starIcon = `
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.62L12 2L9.19 8.62L2 9.24L7.46 13.97L5.82 21L12 17.27Z" fill="currentColor"/>
                    </svg>
                `;

    // Add style for hover effect
    GM_addStyle(`
    .addTagButton:hover div {
        color: gold;
    }`);

    function updateButtonAppearance(button, isHighlighted) {
        if (isHighlighted) {
            button.style.color = removeHighlightColor;
            button.setAttribute('title', 'Remove Highlight');
        } else {
            button.style.color = addHighlightColor;
            button.setAttribute('title', 'Add Highlight');
        }
    }

    function updateTagContainerAppearance(tagContainer, tagId, isHighlighted) {
        if (isHighlighted && tagDetails[tagId]) {
            tagContainer.style.outline = `2px solid ${tagDetails[tagId].color}`;
            tagContainer.style.outlineOffset = '-2px'; // Ensure the outline is within the border
            tagContainer.style.borderRadius = '5px';
        } else {
            tagContainer.style.outline = 'none';
        }
    }

    // Converts the old way of saving tag highlights to the new way
    function convertTagDetailsToIndividualEntries() {
        const existingTagDetails = GM_getValue('tagDetails', null);
        if(existingTagDetails !== null) {
            console.log("Converting old saves to new ones.");

            // Iterate over each entry in the existing tagDetails object
            for (const [tagId, tagDetail] of Object.entries(existingTagDetails)) {
                // Save each tag individually
                GM_setValue(`tag_${tagId}`, tagDetail);
            }

            // delete the old tagDetails entry to avoid confusion
            GM_deleteValue('tagDetails');
        }
    }




    // Process items to see if they require additional logic
    function processItem(item) {
        const dataTags = item.getAttribute('data-tags').split(' ').map(Number);
        const matchingTags = dataTags.filter(tag => tag in tagDetails);

        if (matchingTags.length > 0) {
            // Insert functions that take matched items here
            highlightDotItem(item, matchingTags);
        }
    }

    // Log item name and matched tags
    function logItem(item, matchingTags) {
        const itemName = item.querySelector('.caption').innerText;
        const matchedTagNames = matchingTags.map(tag => tagDetails[tag].name).join(', ');
        console.log(`${itemName}: ${matchedTagNames}`);
    }

    // Highlight item with dots based on matched tags
    function highlightDotItem(item, matchingTags) {
        // Ensure the parent element is positioned relative for the dots to be positioned correctly
        item.style.position = 'relative';

        // Sort matching tags by priority, higher first
        matchingTags.sort((a, b) => {
            const priorityA = tagDetails[a]?.priority ?? defaultPriority;
            const priorityB = tagDetails[b]?.priority ?? defaultPriority;
            return priorityB - priorityA;
        });

        let tagHighlightAmt = matchingTags.length;
        if(highlightCutoffThreshold > 0) {
            tagHighlightAmt = Math.min(highlightCutoffThreshold, tagHighlightAmt);
        }

        for (let i = 0; i < tagHighlightAmt; i++) {
            const tag = matchingTags[i];
            const tagDetail = tagDetails[tag];
            if (tagDetail) {
                const dotContainer = document.createElement('div');
                dotContainer.style.display = 'flex';
                dotContainer.style.alignItems = 'center';
                dotContainer.style.position = 'absolute';
                dotContainer.style.top = `${5 + i * 20}px`;
                dotContainer.style.left = '5px';
                dotContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.66)';
                dotContainer.style.padding = '1px 5px';
                dotContainer.style.borderRadius = '5px';
                dotContainer.style.pointerEvents = 'none'; // Make the container ignore mouse events

                const dot = document.createElement('div');
                dot.style.width = '15px';
                dot.style.height = '15px';
                dot.style.backgroundColor = tagDetail.color;
                dot.style.borderRadius = '50%';
                dot.style.marginRight = '5px';

                const tagName = document.createElement('span');
                tagName.innerText = tagDetail.name;
                tagName.style.color = '#fff';
                tagName.style.fontSize = '12px';

                dotContainer.appendChild(dot);
                dotContainer.appendChild(tagName);
                // Calculate if the dot container's bottom would exceed the item's height
                const potentialBottomPosition = parseInt(dotContainer.style.top, 10);
                if (potentialBottomPosition <= item.offsetHeight) {
                    // Only append the dot container if it doesn't exceed the item's boundary
                    item.appendChild(dotContainer);
                } else {
                    break;
                }
            }
        }
    }





    // Function to create a new button for each tag
    function addTagButtons() {
        const tags = document.querySelectorAll('.tag');
        tags.forEach(tag => {
            const match = tag.className.match(/tag-(\d+)/);
            if (match) {
                const tagId = match[1];
                const buttonContainer = tag.querySelector('.count');
                if (buttonContainer) {
                    const addButton = document.createElement('div');
                    addButton.classList.add('addTagButton');
                    addButton.setAttribute('alt', 'add-tag');
                    addButton.style.cursor = 'pointer';
                    addButton.style.display = 'inline-block';

                    const addButtonIcon = document.createElement('div');
                    addButtonIcon.innerHTML = starIcon;
                    addButtonIcon.style.width = '18px';
                    addButtonIcon.style.height = '18px';

                    const isHighlighted = tagId in tagDetails;
                    updateButtonAppearance(addButton, isHighlighted);
                    updateTagContainerAppearance(tag, tagId, isHighlighted);

                    addButton.appendChild(addButtonIcon);
                    buttonContainer.appendChild(addButton);

                    addButton.addEventListener('click', (event) => {
                        event.preventDefault(); // Prevent default behavior
                        toggleTagPopup(tagId, tag.querySelector('.name').innerText, addButton, tag);
                    });
                }
            }
        });
    }

    // Function to toggle the popup for adding tag details
    function toggleTagPopup(tagId, tagName, button, tagContainer) {
        const tagModal = document.getElementById('tag-modal');
        const modalOverlay = document.getElementById('modal-overlay');
        const existingTagsList = document.getElementById('existing-tags-list');

        // Populate existing tags list sorted by priority
        existingTagsList.innerHTML = '';
        const sortedTagDetails = Object.entries(tagDetails).sort((a, b) => {
            const priorityA = a[1].priority ?? defaultPriority;
            const priorityB = b[1].priority ?? defaultPriority;
            return priorityB - priorityA;
        });

        for (const [id, details] of sortedTagDetails) {
            const tagItem = document.createElement('div');
            tagItem.innerHTML = `
            <span style="color: ${details.color};">${details.name}</span>
            <span> (${details.priority})</span>`;
            existingTagsList.appendChild(tagItem);
        }

        if (tagDetails[tagId]) {
            // If the tag is already added, remove it
            delete tagDetails[tagId];
            GM_deleteValue(`tag_${tagId}`);
            updateButtonAppearance(button, false);
            updateTagContainerAppearance(tagContainer, tagId, false);
        } else {
            // Show the modal dialog
            document.getElementById('tag-name').value = tagName;
            document.getElementById('tag-priority').value = defaultPriority;
            document.getElementById('tag-color').value = '#32a852';
            tagModal.style.display = 'block';
            modalOverlay.style.display = 'block';

            // Handle save button click
            document.getElementById('save-tag').onclick = () => {
                const newTagName = document.getElementById('tag-name').value;
                const priority = parseInt(document.getElementById('tag-priority').value);
                const color = document.getElementById('tag-color').value;

                if (newTagName && !isNaN(priority)) {
                    tagDetails[tagId] = { name: newTagName, color: color, priority: priority };
                    GM_setValue(`tag_${tagId}`, tagDetails[tagId]);
                    updateButtonAppearance(button, true);
                    updateTagContainerAppearance(tagContainer, tagId, true);
                }

                // Hide the modal dialog
                tagModal.style.display = 'none';
                modalOverlay.style.display = 'none';
            };

            // Handle cancel button click
            document.getElementById('cancel-tag').onclick = () => {
                // Hide the modal dialog
                tagModal.style.display = 'none';
                modalOverlay.style.display = 'none';
            };
        }
    }



    // Function to observe new nodes (required for NH Imporved infinite load compatibility)
    function observeNewNodes(mutations) {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.classList.contains('gallery')) {
                    processItem(node);
                } else if (node.nodeType === 1) {
                    node.querySelectorAll('.gallery').forEach(item => {
                        processItem(item);
                    });
                }
            });
        });
    }

    // Initial function to set up the script
    function init() {
        //convertTagDetailsToIndividualEntries();
        addTagButtons();
        const observer = new MutationObserver(observeNewNodes);
        observer.observe(document.body, { childList: true, subtree: true });

        const galleryItems = document.querySelectorAll('.gallery');
        galleryItems.forEach(item => {
            processItem(item);
        });
    }

    init();
})();