Nhentai TSM Tag Catcher

Gets Tags/Details from Nhentai.net Comics for Tachiyomi/Suwayomi/Mihon with improvements!

// ==UserScript==
// @name         Nhentai TSM Tag Catcher
// @namespace    https://sleazyfork.org/en/users/1261593-john-doe4
// @version      1.3
// @description  Gets Tags/Details from Nhentai.net Comics for Tachiyomi/Suwayomi/Mihon with improvements!
// @author       john doe4
// @match        *://nhentai.net/g/*
// @exclude      *://nhentai.net/g/
// @icon         https://nhentai.net/favicon.ico
// @grant        GM_setClipboard
// @license GPLv3
// ==/UserScript==

(function() {
    'use strict';

    function initPreferences() {
        if (localStorage.getItem('nhTheme') === null) {
            localStorage.setItem('nhTheme', 'light');
        }
        if (localStorage.getItem('nhStatus') === null) {
            localStorage.setItem('nhStatus', '2');
        }
    }
    initPreferences();

        var jsonButton = document.createElement('a');
    jsonButton.id = 'download';
    jsonButton.className = 'btn btn-secondary';
    jsonButton.textContent = 'Catcher';
    jsonButton.style.marginLeft = '2px';
    jsonButton.style.padding = '6px 12px';
    jsonButton.style.lineHeight = '1';
    jsonButton.style.fontSize = '14px';
    jsonButton.style.display = 'inline-flex';
    jsonButton.style.alignItems = 'center';
    jsonButton.style.justifyContent = 'center';
    jsonButton.style.boxSizing = 'border-box';
    jsonButton.addEventListener('click', generateJson);
    document.querySelector('.buttons').appendChild(jsonButton);

    var jsonContainer = document.createElement('div');
    jsonContainer.id = 'tachiyomi-json';
    jsonContainer.style.display = 'none';
    jsonContainer.style.marginTop = '15px';
    jsonContainer.style.width = '100%';
    document.getElementById("info").appendChild(jsonContainer);

    var tachiyomiControls = document.createElement('div');
    tachiyomiControls.innerHTML = `
        <div class="tag-container field-name" style="margin-bottom: 10px;">
            <div class="status-buttons" style="display: flex; gap: 10px; margin-top: 5px;">
                <label class="status-btn" style="flex: 1; position: relative;">
                    <input type="radio" name="status" value="1" ${localStorage.getItem('nhStatus') === '1' ? 'checked' : ''} style="position: absolute; opacity: 0; width: 0; height: 0;">
                    <span class="status-label" style="display: flex; align-items: center; justify-content: center; height: 100%; padding: 6px; text-align: center; border-radius: 4px; font-size: 0.9em; background: ${localStorage.getItem('nhStatus') === '1' ? '#4CAF50' : 'inherit'}; color: ${localStorage.getItem('nhStatus') === '1' ? 'white' : 'inherit'}; cursor: pointer;">Ongoing</span>
                </label>
                <label class="status-btn" style="flex: 1; position: relative;">
                    <input type="radio" name="status" value="2" ${localStorage.getItem('nhStatus') !== '1' ? 'checked' : ''} style="position: absolute; opacity: 0; width: 0; height: 0;">
                    <span class="status-label" style="display: flex; align-items: center; justify-content: center; height: 100%; padding: 6px; text-align: center; border-radius: 4px; font-size: 0.9em; background: ${localStorage.getItem('nhStatus') !== '1' ? '#4CAF50' : 'inherit'}; color: ${localStorage.getItem('nhStatus') !== '1' ? 'white' : 'inherit'}; cursor: pointer;">Completed</span>
                </label>
            </div>
        </div>
        <div class="tag-container field-name" style="margin-bottom: 15px;">
            <textarea type="text" id="description" placeholder="Add description"
                      style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid var(--border-color); margin-top: 5px; background: inherit; font-size: 14px;"></textarea>
        </div>
    `;
    document.getElementById("tags").appendChild(tachiyomiControls);

    function getPreference(key) {
        return localStorage.getItem(`nh${key}`) === 'true';
    }

    function setPreference(key, value) {
        localStorage.setItem(`nh${key}`, value.toString());

        if (key === 'AutoDownload' && value) {
            localStorage.setItem('nhPageLoadDownload', 'false');
            const pageLoadDownload = document.getElementById('page-load-download');
            if (pageLoadDownload) pageLoadDownload.checked = false;
        } else if (key === 'PageLoadDownload' && value) {
            localStorage.setItem('nhAutoDownload', 'false');
            const autoDownload = document.getElementById('auto-download');
            if (autoDownload) autoDownload.checked = false;
        }
    }

    function getTheme() {
        return localStorage.getItem('nhTheme') || 'light';
    }

    function setTheme(theme) {
        localStorage.setItem('nhTheme', theme);
        applyTheme();
        updateThemeLabel();
        updateStatusButtonsStyle();
        updateButtonStyles();
        updateDescriptionBox();
    }

    function updateThemeLabel() {
        const themeLabel = document.getElementById('theme-label');
        if (themeLabel) {
            themeLabel.textContent = getTheme() === 'dark' ? 'Dark' : 'Light';
        }
    }

    function updateStatusButtonsStyle() {
        const theme = getTheme();
        const selectedStatus = localStorage.getItem('nhStatus');
        document.querySelectorAll('.status-label').forEach(label => {
            const radio = label.parentElement.querySelector('input[type="radio"]');
            if (radio.value === selectedStatus) {
                label.style.backgroundColor = '#4CAF50';
                label.style.color = 'white';
            } else {
                label.style.backgroundColor = theme === 'dark' ? '#3d3d3d' : '#f0f0f0';
                label.style.color = theme === 'dark' ? '#ffffff' : '#333333';
            }
        });
    }

    function updateButtonStyles() {
        const theme = getTheme();
        const buttons = ['copy-btn', 'download-btn', 'catch-tags-btn'];
        buttons.forEach(id => {
            const btn = document.getElementById(id);
            if (btn) {
                btn.style.backgroundColor = theme === 'dark' ? '#3d3d3d' : '#f0f0f0';
                btn.style.color = theme === 'dark' ? '#ffffff' : '#333333';
                btn.style.borderColor = theme === 'dark' ? '#444' : '#ddd';
            }
        });
    }

    function updateDescriptionBox() {
        const description = document.getElementById('description');
        if (description) {
            description.style.borderColor = getTheme() === 'dark' ? '#444' : '#ddd';
        }
    }

    function applyTheme() {
        const theme = getTheme();
        const container = document.getElementById('tachiyomi-json');
        if (!container) return;

        const textarea = container.querySelector('textarea');
        if (!textarea) return;

        if (theme === 'dark') {
            container.style.backgroundColor = '#2d2d2d';
            container.style.color = '#ffffff';
            container.style.borderColor = '#444';
            textarea.style.backgroundColor = '#1a1a1a';
            textarea.style.color = '#ffffff';
            textarea.style.borderColor = '#444';
        } else {
            container.style.backgroundColor = '#f5f5f5';
            container.style.color = '#333333';
            container.style.borderColor = '#e0e0e0';
            textarea.style.backgroundColor = '#ffffff';
            textarea.style.color = '#333333';
            textarea.style.borderColor = '#ddd';
        }
        updateStatusButtonsStyle();
        updateButtonStyles();
        updateDescriptionBox();
    }

    function getAuthorFallback() {
        const authorTag = document.querySelectorAll('.tags')[3]?.querySelector('.name');
        if (authorTag) return authorTag.textContent;

        const tagContainers = document.querySelectorAll('.tag-container');
        let groupName = null;

        tagContainers.forEach(container => {
            if (container.textContent.includes('Groups:')) {
                const groupTag = container.querySelector('.tags a');
                if (groupTag) {
                    const groupHref = groupTag.getAttribute('href');
                    groupName = groupHref.split('/group/')[1].replace('/', '');
                }
            }
        });

        if (groupName) return groupName;
        return document.getElementById('gallery_id').textContent.replace('#', '').trim();
    }

    const buttonCooldowns = {};

    function copyToClipboard() {
        const textContent = document.getElementById('json-output')?.value;
        if (textContent) {
            GM_setClipboard(textContent, 'text');

            const btn = document.getElementById('copy-btn');
            if (btn) {
                if (buttonCooldowns['copy']) return;

                const originalText = btn.textContent;
                btn.textContent = 'Copied!';
                buttonCooldowns['copy'] = true;

                setTimeout(() => {
                    btn.textContent = originalText;
                    buttonCooldowns['copy'] = false;
                }, 1500);
            }
        }
    }

    function copyTagsToClipboard() {
        const genreTags = Array.from(document.querySelectorAll('.tags')[2]?.querySelectorAll('.name') || [])
            .map(el => el.textContent)
            .join(', ');

        if (genreTags) {
            GM_setClipboard(genreTags, 'text');

            const btn = document.getElementById('catch-tags-btn');
            if (btn) {
                if (buttonCooldowns['tags']) return;

                const originalText = btn.textContent;
                btn.textContent = 'Tags Copied!';
                buttonCooldowns['tags'] = true;

                setTimeout(() => {
                    btn.textContent = originalText;
                    buttonCooldowns['tags'] = false;
                }, 1500);
            }
        }
    }

    function downloadJson() {
        const textContent = document.getElementById('json-output')?.value;
        if (textContent) {
            const blob = new Blob([textContent], { type: 'application/json' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            a.download = 'details.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
    }

    function updateJsonData() {
        const jsonOutput = document.getElementById('json-output');
        if (jsonOutput && jsonOutput.style.display !== 'none') {
            try {
                const genreTags = Array.from(document.querySelectorAll('.tags')[2]?.querySelectorAll('.name') || [])
                    .map(el => el.textContent);

                const jsonData = {
                    title: document.querySelector('.title .pretty')?.textContent || '',
                    author: getAuthorFallback(),
                    artist: getAuthorFallback(),
                    description: document.getElementById('description')?.value || '',
                    genre: genreTags,
                    status: document.querySelector('input[name="status"]:checked')?.value || '2'
                };

                jsonOutput.value = JSON.stringify(jsonData, null, 4);
            } catch (e) {
                console.error('Error updating JSON:', e);
            }
        }
    }

    function generateJson() {
        const jsonDiv = document.getElementById('tachiyomi-json');

        if (jsonDiv.style.display === 'none') {
            jsonDiv.style.display = 'block';
        } else {
            jsonDiv.style.display = 'none';
            return;
        }

        updateJsonData();

        jsonDiv.innerHTML = `
            <div class="tag-container" style="padding: 15px; border-radius: 4px; border: none; width: 100%; box-sizing: border-box;">
                <textarea id="json-output"
                          style="width: 100%; height: 200px; padding: 10px;
                                 border-radius: 4px; margin-bottom: 10px;
                                 font-family: monospace; border: 1px solid var(--border-color);
                                 font-size: 14px; resize: vertical;"></textarea>
                <div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; width: 100%;">
                    <div style="display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 200px;">
                        <div style="display: flex; flex-wrap: wrap; gap: 10px;">
                            <button id="copy-btn" class="btn btn-secondary"
                                    style="padding: 8px 12px; min-width: 120px; font-size: 14px; flex: 1;">
                                Copy to Clipboard
                            </button>
                            <button id="download-btn" class="btn btn-secondary"
                                    style="padding: 8px 12px; min-width: 120px; font-size: 14px; flex: 1;">
                                Download JSON
                            </button>
                            <button id="catch-tags-btn" class="btn btn-secondary"
                                    style="padding: 8px 12px; min-width: 120px; font-size: 14px; flex: 1;">
                                Catch Tags
                            </button>
                        </div>
                        <div style="display: flex; flex-wrap: wrap; gap: 15px; margin-top: 5px;">
                            <label style="display: flex; align-items: center; gap: 5px; color: var(--text-color); font-size: 14px;">
                                <input type="checkbox" id="auto-download"
                                       ${getPreference('AutoDownload') ? 'checked' : ''}
                                       style="margin: 0; accent-color: #4CAF50;">
                                Auto Download
                            </label>
                            <label style="display: flex; align-items: center; gap: 5px; color: var(--text-color); font-size: 14px;">
                                <input type="checkbox" id="page-load-download"
                                       ${getPreference('PageLoadDownload') ? 'checked' : ''}
                                       style="margin: 0; accent-color: #4CAF50;">
                                Download on Load
                            </label>
                        </div>
                    </div>
                    <div style="display: flex; align-items: center; gap: 5px; margin-left: auto; align-self: flex-end;">
                        <span style="color: var(--text-color); font-size: 14px;">Theme:</span>
                        <label class="switch">
                            <input type="checkbox" id="theme-toggle" ${getTheme() === 'dark' ? 'checked' : ''}>
                            <span class="slider round"></span>
                        </label>
                        <span id="theme-label" style="color: var(--text-color); font-size: 14px; min-width: 40px;">${getTheme() === 'dark' ? 'Dark' : 'Light'}</span>
                    </div>
                </div>
            </div>
        `;

        applyTheme();
        updateJsonData();

        document.getElementById('copy-btn')?.addEventListener('click', copyToClipboard);
        document.getElementById('download-btn')?.addEventListener('click', downloadJson);
        document.getElementById('catch-tags-btn')?.addEventListener('click', copyTagsToClipboard);

        document.getElementById('auto-download')?.addEventListener('change', function() {
            setPreference('AutoDownload', this.checked);
        });

        document.getElementById('page-load-download')?.addEventListener('change', function() {
            setPreference('PageLoadDownload', this.checked);
        });

        document.getElementById('theme-toggle')?.addEventListener('change', function() {
            setTheme(this.checked ? 'dark' : 'light');
        });

        document.querySelectorAll('.status-btn input[type="radio"]').forEach(radio => {
            radio.addEventListener('change', function() {
                if (this.checked) {
                    localStorage.setItem('nhStatus', this.value);
                    updateStatusButtonsStyle();
                    updateJsonData();
                }
            });
        });

        document.getElementById('description')?.addEventListener('input', function() {
            updateJsonData();
        });

        if (getPreference('AutoDownload')) {
            setTimeout(downloadJson, 100);
        }
    }

    function initStatusButtons() {
        document.querySelectorAll('.status-btn input[type="radio"]').forEach(radio => {
            radio.addEventListener('change', function() {
                if (this.checked) {
                    localStorage.setItem('nhStatus', this.value);
                    updateStatusButtonsStyle();
                }
            });
        });
        updateStatusButtonsStyle();
    }

    const style = document.createElement('style');
    style.textContent = `
        :root {
            --border-color: #ddd;
            --text-color: #666;
        }
        .dark-theme {
            --border-color: #444;
            --text-color: #aaa;
        }
        .switch {
            position: relative;
            display: inline-block;
            width: 50px;
            height: 24px;
        }
        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 24px;
        }
        .slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        input:checked + .slider {
            background-color: #4CAF50;
        }
        input:checked + .slider:before {
            transform: translateX(26px);
        }
        .status-btn span {
            transition: all 0.3s ease;
        }
        .status-btn:hover span {
            filter: brightness(0.9);
        }
        #tachiyomi-json .tag-container {
            border: none !important;
            background: inherit !important;
        }
        #copy-btn, #download-btn, #catch-tags-btn {
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: all 0.3s ease;
            white-space: nowrap;
        }
        #json-output::-webkit-scrollbar {
            width: 8px;
        }
        #json-output::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 4px;
        }
        #json-output::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 4px;
        }
        .dark-theme #json-output::-webkit-scrollbar-track {
            background: #3d3d3d;
        }
        .dark-theme #json-output::-webkit-scrollbar-thumb {
            background: #666;
        }
        @media (max-width: 600px) {
            #tachiyomi-json .tag-container {
                padding: 10px !important;
            }
            #json-output {
                height: 150px !important;
                font-size: 13px !important;
            }
            .status-buttons {
                flex-direction: column !important;
            }
            #copy-btn, #download-btn, #catch-tags-btn {
                padding: 8px 10px !important;
                min-width: 100px !important;
                font-size: 13px !important;
            }
            .status-label {
                padding: 8px !important;
            }
        }
    `;
    document.head.appendChild(style);

    function applyThemeClass() {
        if (getTheme() === 'dark') {
            document.body.classList.add('dark-theme');
        } else {
            document.body.classList.remove('dark-theme');
        }
    }
    applyThemeClass();

    setTimeout(initStatusButtons, 100);

    if (getPreference('PageLoadDownload')) {
        window.addEventListener('load', function() {
            setTimeout(function() {
                generateJson();
                if (document.getElementById('tachiyomi-json')?.style.display === 'block') {
                    downloadJson();
                }
            }, 500);
        });
    }
})();