TSM Tag Catcher

Gets Tags/Details/json for Tachiyomi/Suwayomi/Mihon from nhentai, hitomi, imhentai, e-hentai

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Advertisement:

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

Advertisement:

// ==UserScript==
// @name         TSM Tag Catcher
// @namespace    https://sleazyfork.org/en/users/1261593-john-doe4
// @version      1.5
// @description  Gets Tags/Details/json for Tachiyomi/Suwayomi/Mihon from nhentai, hitomi, imhentai, e-hentai
// @author       john doe4
// @match        *://nhentai.net/g/*
// @match        *://hitomi.la/doujinshi/*
// @match        *://imhentai.xxx/gallery/*
// @match        *://e-hentai.org/g/*/*
// @exclude      *://nhentai.net/g/
// @exclude      *://hitomi.la/doujinshi/
// @exclude      *://imhentai.xxx/gallery/
// @exclude      *://e-hentai.org/g/
// @icon         https://docs.google.com/uc?export=download&id=1jRJOg4mZUI04_NMatSMFFkKidKrtuPce
// @grant        GM_setClipboard
// @license      GPLv3
// ==/UserScript==

(function() {
    'use strict';

    var domObserver = null;
    var reinjectTimer = null;
    var pageLoadDownloadFired = false;
    var host = window.location.hostname.replace('www.', '');

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

    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');
            var el = document.getElementById('page-load-download');
            if (el) el.checked = false;
        } else if (key === 'PageLoadDownload' && value) {
            localStorage.setItem('nhAutoDownload', 'false');
            var el = document.getElementById('auto-download');
            if (el) el.checked = false;
        }
    }

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

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

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

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

    function updateStatusButtonsStyle() {
        var theme = getTheme();
        var selected = localStorage.getItem('nhStatus') || '2';
        document.querySelectorAll('.status-label').forEach(function(label) {
            var radio = label.parentElement.querySelector('input[type="radio"]');
            if (radio && radio.value === selected) {
                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() {
        var theme = getTheme();
        ['copy-btn', 'download-btn', 'catch-tags-btn'].forEach(function(id) {
            var btn = document.getElementById(id);
            if (btn) {
                btn.style.backgroundColor = theme === 'dark' ? '#3a3a3a' : '#f0f0f0';
                btn.style.color = theme === 'dark' ? '#ffffff' : '#333333';
                btn.style.borderColor = theme === 'dark' ? '#555' : '#bbb';
            }
        });
    }

    function updateDescriptionBox() {
        var el = document.getElementById('nh-description');
        if (el) el.style.borderColor = getTheme() === 'dark' ? '#555' : '#bbb';
    }

    function applyTheme() {
        var theme = getTheme();
        var container = document.getElementById('tachiyomi-json');
        if (!container) return;
        var textarea = container.querySelector('textarea#json-output');
        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 = '#555';
        } else {
            container.style.backgroundColor = '#f5f5f5';
            container.style.color = '#333333';
            container.style.borderColor = '#e0e0e0';
            textarea.style.backgroundColor = '#ffffff';
            textarea.style.color = '#333333';
            textarea.style.borderColor = '#bbb';
        }
        updateStatusButtonsStyle();
        updateButtonStyles();
        updateDescriptionBox();
    }

    var buttonCooldowns = {};

    function copyToClipboard() {
        var jsonOutput = document.getElementById('json-output');
        if (!jsonOutput || !jsonOutput.value) return;
        GM_setClipboard(jsonOutput.value, 'text');
        var btn = document.getElementById('copy-btn');
        if (btn && !buttonCooldowns['copy']) {
            var orig = btn.textContent;
            btn.textContent = 'Copied!';
            buttonCooldowns['copy'] = true;
            setTimeout(function() { btn.textContent = orig; buttonCooldowns['copy'] = false; }, 1500);
        }
    }

    function copyTagsToClipboard() {
        var tags = getSiteData().genre.join(', ');
        if (!tags) return;
        GM_setClipboard(tags, 'text');
        var btn = document.getElementById('catch-tags-btn');
        if (btn && !buttonCooldowns['tags']) {
            var orig = btn.textContent;
            btn.textContent = 'Tags Copied!';
            buttonCooldowns['tags'] = true;
            setTimeout(function() { btn.textContent = orig; buttonCooldowns['tags'] = false; }, 1500);
        }
    }

    function downloadJson() {
        var jsonOutput = document.getElementById('json-output');
        if (!jsonOutput || !jsonOutput.value) return;
        var blob = new Blob([jsonOutput.value], { type: 'application/json' });
        var url = URL.createObjectURL(blob);
        var 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 cleanText(str) {
        return str.replace(/[♀♂]/g, '').trim();
    }

    function getDirectText(el) {
        var text = '';
        el.childNodes.forEach(function(node) {
            if (node.nodeType === Node.TEXT_NODE) text += node.textContent;
        });
        return text.trim();
    }

    function getSiteData() {
        var data = { title: '', author: '', artist: '', genre: [], status: localStorage.getItem('nhStatus') || '2' };
        var descEl = document.getElementById('nh-description');
        data.description = descEl ? descEl.value : '';

        if (host === 'nhentai.net') {
            data.title = getNhentaiTitle();
            data.author = getNhentaiAuthor();
            data.artist = data.author;
            data.genre = getNhentaiTags('Tags:');
        } else if (host === 'hitomi.la') {
            data.title = getHitomiTitle();
            data.author = getHitomiArtist();
            data.artist = data.author;
            data.genre = getHitomiTags();
        } else if (host === 'imhentai.xxx') {
            data.title = getImhentaiTitle();
            data.author = getImhentaiField('Artists:');
            data.artist = data.author;
            data.genre = getImhentaiTags();
        } else if (host === 'e-hentai.org') {
            data.title = getEhentaiTitle();
            data.author = getEhentaiUploader() || getEhentaiField('artist');
            data.artist = data.author;
            data.genre = getEhentaiTags();
        }

        var statusEl = document.querySelector('input[name="nhstatus"]:checked');
        if (statusEl) data.status = statusEl.value;
        return data;
    }

    function getNhentaiTitle() {
        var el = document.querySelector('.title .pretty');
        if (el) return el.textContent.trim();
        var h1 = document.querySelector('h1.title');
        if (h1) { var p = h1.querySelector('.pretty'); return p ? p.textContent.trim() : h1.textContent.trim(); }
        return '';
    }

    function getNhentaiTagContainerByLabel(label) {
        var containers = document.querySelectorAll('#tags .tag-container');
        for (var i = 0; i < containers.length; i++) {
            if (containers[i].textContent.trim().startsWith(label)) return containers[i];
        }
        return null;
    }

    function getNhentaiTags(label) {
        var container = getNhentaiTagContainerByLabel(label);
        if (!container) return [];
        return Array.from(container.querySelectorAll('.name')).map(function(el) { return el.textContent.trim(); });
    }

    function getNhentaiAuthor() {
        var artists = getNhentaiTags('Artists:');
        if (artists.length > 0) return artists[0];
        var container = getNhentaiTagContainerByLabel('Groups:');
        if (container) {
            var link = container.querySelector('a[href*="/group/"]');
            if (link) {
                var match = link.getAttribute('href').match(/\/group\/([^/]+)\//);
                if (match) return match[1].replace(/-/g, ' ');
            }
        }
        var gid = document.getElementById('gallery_id');
        return gid ? gid.textContent.replace('#', '').trim() : '';
    }

    function getHitomiTitle() {
        var el = document.querySelector('#gallery-brand a');
        return el ? el.textContent.trim() : '';
    }

    function getHitomiArtist() {
        var el = document.querySelector('#artists ul li a');
        if (el) return el.textContent.trim();
        var group = document.querySelector('#groups ul li a');
        return group ? group.textContent.trim() : '';
    }

    function getHitomiTags() {
        var items = document.querySelectorAll('#tags li a');
        return Array.from(items).map(function(el) { return cleanText(el.textContent); });
    }

    function getImhentaiTitle() {
        var el = document.querySelector('.right_details h1');
        return el ? el.textContent.trim() : '';
    }

    function getImhentaiField(label) {
        var items = document.querySelectorAll('.galleries_info li');
        for (var i = 0; i < items.length; i++) {
            var span = items[i].querySelector('.tags_text');
            if (span && span.textContent.trim() === label) {
                var link = items[i].querySelector('a.tag');
                return link ? getDirectText(link) : '';
            }
        }
        return '';
    }

    function getImhentaiTags() {
        var items = document.querySelectorAll('.galleries_info li');
        for (var i = 0; i < items.length; i++) {
            var span = items[i].querySelector('.tags_text');
            if (span && span.textContent.trim() === 'Tags:') {
                return Array.from(items[i].querySelectorAll('a.tag')).map(function(el) {
                    return getDirectText(el);
                });
            }
        }
        return [];
    }

    function getEhentaiTitle() {
        var el = document.getElementById('gn');
        return el ? el.textContent.trim() : '';
    }

    function getEhentaiUploader() {
        var el = document.querySelector('#gdn a');
        return el ? el.textContent.trim() : '';
    }

    function getEhentaiField(category) {
        var rows = document.querySelectorAll('#taglist table tr');
        for (var i = 0; i < rows.length; i++) {
            var tc = rows[i].querySelector('td.tc');
            if (tc && tc.textContent.replace(':', '').trim() === category) {
                var links = rows[i].querySelectorAll('td:last-child a');
                if (links.length > 0) return links[0].textContent.trim();
            }
        }
        return '';
    }

    function getEhentaiTags() {
        var tags = [];
        var skipCategories = ['parody', 'character', 'group', 'language'];
        var rows = document.querySelectorAll('#taglist table tr');
        rows.forEach(function(row) {
            var tc = row.querySelector('td.tc');
            if (!tc) return;
            var cat = tc.textContent.replace(':', '').trim();
            if (skipCategories.indexOf(cat) !== -1) return;
            var links = row.querySelectorAll('td:last-child a');
            links.forEach(function(a) { tags.push(a.textContent.trim()); });
        });
        return tags;
    }

    function getEhentaiButtonAnchor() {
        var rows = document.querySelectorAll('#taglist table tr');
        for (var i = 0; i < rows.length; i++) {
            var tc = rows[i].querySelector('td.tc');
            if (tc && tc.textContent.replace(':', '').trim() === 'other') {
                return rows[i].querySelector('td:last-child');
            }
        }
        var lastRow = rows.length > 0 ? rows[rows.length - 1] : null;
        return lastRow ? lastRow.querySelector('td:last-child') : null;
    }

    function updateJsonData() {
        var jsonOutput = document.getElementById('json-output');
        if (!jsonOutput) return;
        try {
            var data = getSiteData();
            jsonOutput.value = JSON.stringify({
                title: data.title,
                author: data.author,
                artist: data.artist,
                description: data.description,
                genre: data.genre,
                status: data.status
            }, null, 4);
        } catch (e) {
            console.error('TSM Tag Catcher: error building JSON', e);
        }
    }

    function generateJson() {
        var jsonDiv = document.getElementById('tachiyomi-json');
        if (!jsonDiv) return;

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

        var isDark = getTheme() === 'dark';

        jsonDiv.innerHTML = '<div style="padding: 15px; border-radius: 6px; 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 #bbb; font-size: 14px; resize: vertical; box-sizing: border-box;"></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: 8px;">' +
            '<button id="copy-btn" style="padding: 7px 14px; min-width: 120px; font-size: 13px; flex: 1; border-radius: 4px; border: 1px solid #bbb; cursor: pointer; font-weight: 600; letter-spacing: 0.02em;">Copy to Clipboard</button>' +
            '<button id="download-btn" style="padding: 7px 14px; min-width: 120px; font-size: 13px; flex: 1; border-radius: 4px; border: 1px solid #bbb; cursor: pointer; font-weight: 600; letter-spacing: 0.02em;">Download JSON</button>' +
            '<button id="catch-tags-btn" style="padding: 7px 14px; min-width: 100px; font-size: 13px; flex: 1; border-radius: 4px; border: 1px solid #bbb; cursor: pointer; font-weight: 600; letter-spacing: 0.02em;">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; font-size: 13px; cursor: pointer;">' +
            '<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; font-size: 13px; cursor: pointer;">' +
            '<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: 6px; margin-left: auto; align-self: flex-end;">' +
            '<span style="font-size: 13px;">Theme:</span>' +
            '<label class="tsm-switch"><input type="checkbox" id="theme-toggle" ' + (isDark ? 'checked' : '') + '><span class="tsm-slider"></span></label>' +
            '<span id="theme-label" style="font-size: 13px; min-width: 36px;">' + (isDark ? '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'); });

        var descEl = document.getElementById('nh-description');
        if (descEl) descEl.addEventListener('input', updateJsonData);

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

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

    function buildControlsHTML() {
        var savedStatus = localStorage.getItem('nhStatus') || '2';
        return '<div id="nh-controls" style="margin-top: 10px;">' +
            '<div 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="nhstatus" value="1" ' + (savedStatus === '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; cursor: pointer;">Ongoing</span>' +
            '</label>' +
            '<label class="status-btn" style="flex: 1; position: relative;">' +
            '<input type="radio" name="nhstatus" value="2" ' + (savedStatus === '2' ? '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; cursor: pointer;">Completed</span>' +
            '</label>' +
            '</div>' +
            '</div>' +
            '<div style="margin-bottom: 15px;">' +
            '<textarea id="nh-description" placeholder="Add description" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #bbb; margin-top: 5px; background: inherit; font-size: 14px; box-sizing: border-box;"></textarea>' +
            '</div>' +
            '</div>';
    }

    function makeCatcherButton() {
        var btn = document.createElement('button');
        btn.id = 'nh-catcher-btn';
        btn.textContent = 'Catcher';
        btn.style.cssText = 'margin: 4px 0 0 8px; padding: 6px 16px; font-size: 13px; font-weight: 700; letter-spacing: 0.04em; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 4px; border: 2px solid #4CAF50; background: #4CAF50; color: #fff; transition: background 0.2s, border-color 0.2s;';
        btn.addEventListener('mouseover', function() {
            this.style.background = '#43a047';
            this.style.borderColor = '#43a047';
        });
        btn.addEventListener('mouseout', function() {
            this.style.background = '#4CAF50';
            this.style.borderColor = '#4CAF50';
        });
        btn.addEventListener('click', generateJson);
        return btn;
    }

    function isUIPresent() {
        return !!document.getElementById('nh-catcher-btn');
    }

    function injectUI() {
        if (isUIPresent()) return;

        var config = getSiteConfig();
        if (!config) return;

        var anchorEl = typeof config.buttonAnchor === 'function' ? config.buttonAnchor() : document.querySelector(config.buttonAnchor);
        var containerEl = document.querySelector(config.jsonContainer);
        var controlsAnchorEl = config.controlsAfterJson ? true : document.querySelector(config.controlsAnchor);

        if (!anchorEl || !containerEl || !controlsAnchorEl) return;

        var catcherBtn = makeCatcherButton();

        if (config.buttonInsertMode === 'append') {
            anchorEl.appendChild(catcherBtn);
        } else if (config.buttonInsertMode === 'prepend') {
            anchorEl.insertBefore(catcherBtn, anchorEl.firstChild);
        } else {
            anchorEl.parentNode.insertBefore(catcherBtn, anchorEl.nextSibling);
        }

        var jsonDiv = document.createElement('div');
        jsonDiv.id = 'tachiyomi-json';
        jsonDiv.style.cssText = 'display:none;margin-top:15px;width:100%;clear:both;';

        if (config.jsonInsertMode === 'after') {
            var panelWidth = containerEl.getBoundingClientRect().width;
            var panelWrap = document.createElement('div');
            panelWrap.style.cssText = 'width:' + panelWidth + 'px;max-width:100%;margin-left:auto;margin-right:auto;box-sizing:border-box;';
            containerEl.parentNode.insertBefore(panelWrap, containerEl.nextSibling);
            panelWrap.appendChild(jsonDiv);
        } else {
            containerEl.appendChild(jsonDiv);
        }

        var controlsDiv = document.createElement('div');
        controlsDiv.innerHTML = buildControlsHTML();

        if (config.controlsAfterJson) {
            jsonDiv.parentNode.insertBefore(controlsDiv, jsonDiv.nextSibling);
        } else {
            controlsAnchorEl.appendChild(controlsDiv);
        }

        initStatusButtons();
        updateStatusButtonsStyle();
        applyThemeClass();

        if (getPreference('PageLoadDownload') && !pageLoadDownloadFired) {
            pageLoadDownloadFired = true;
            setTimeout(function() {
                generateJson();
                var jd = document.getElementById('tachiyomi-json');
                if (jd && jd.style.display === 'block') downloadJson();
            }, 500);
        }
    }

    function getSiteConfig() {
        if (host === 'nhentai.net') {
            return {
                buttonAnchor: '.buttons',
                buttonInsertMode: 'append',
                jsonContainer: '#info',
                jsonInsertMode: 'append',
                controlsAnchor: '#tags',
                readySelectors: ['.buttons', '#info', '#tags']
            };
        }
        if (host === 'hitomi.la') {
            return {
                buttonAnchor: '#tags',
                buttonInsertMode: 'after',
                jsonContainer: '.gallery-info',
                jsonInsertMode: 'append',
                controlsAnchor: '.gallery-info',
                readySelectors: ['#tags', '.gallery-info']
            };
        }
        if (host === 'imhentai.xxx') {
            return {
                buttonAnchor: '.g_buttons',
                buttonInsertMode: 'append',
                jsonContainer: '.right_details',
                jsonInsertMode: 'append',
                controlsAnchor: '.galleries_info',
                readySelectors: ['.g_buttons', '.right_details', '.galleries_info']
            };
        }
        if (host === 'e-hentai.org') {
            return {
                buttonAnchor: getEhentaiButtonAnchor,
                buttonInsertMode: 'append',
                jsonContainer: '.gm',
                jsonInsertMode: 'after',
                controlsAfterJson: true,
                readySelectors: ['#taglist', '.gm']
            };
        }
        return null;
    }

    function scheduleReinject() {
        if (reinjectTimer) clearTimeout(reinjectTimer);
        reinjectTimer = setTimeout(function() {
            reinjectTimer = null;
            injectUI();
        }, 300);
    }

    function startObserver() {
        if (domObserver) return;
        domObserver = new MutationObserver(function(mutations) {
            for (var i = 0; i < mutations.length; i++) {
                var mutation = mutations[i];
                if (mutation.type !== 'childList' || mutation.removedNodes.length === 0) continue;
                for (var j = 0; j < mutation.removedNodes.length; j++) {
                    var node = mutation.removedNodes[j];
                    if (node.id === 'nh-catcher-btn' ||
                        node.id === 'tachiyomi-json' ||
                        node.id === 'nh-controls' ||
                        (node.querySelector && (
                            node.querySelector('#nh-catcher-btn') ||
                            node.querySelector('#nh-controls')
                        ))) {
                        scheduleReinject();
                        return;
                    }
                }
            }
        });
        domObserver.observe(document.body, { childList: true, subtree: true });
    }

    var style = document.createElement('style');
    style.textContent = '.tsm-switch{position:relative;display:inline-block;width:50px;height:24px}' +
        '.tsm-switch input{opacity:0;width:0;height:0}' +
        '.tsm-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;transition:.4s;border-radius:24px}' +
        '.tsm-slider:before{position:absolute;content:"";height:16px;width:16px;left:4px;bottom:4px;background-color:white;transition:.4s;border-radius:50%}' +
        'input:checked+.tsm-slider{background-color:#4CAF50}' +
        'input:checked+.tsm-slider:before{transform:translateX(26px)}' +
        '.status-btn span{transition:all 0.3s ease}' +
        '.status-btn:hover span{filter:brightness(0.9)}' +
        '#copy-btn,#download-btn,#catch-tags-btn{display:flex!important;align-items:center!important;justify-content:center!important;transition:background-color 0.2s,border-color 0.2s;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){#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:12px!important}' +
        '.status-label{padding:8px!important}}';
    document.head.appendChild(style);

    function waitForReady(callback, maxWait) {
        var config = getSiteConfig();
        if (!config) return;
        var selectors = config.readySelectors;
        var waited = 0;
        var interval = setInterval(function() {
            var allFound = selectors.every(function(sel) { return !!document.querySelector(sel); });
            if (allFound) {
                clearInterval(interval);
                callback();
            } else {
                waited += 100;
                if (waited >= (maxWait || 10000)) clearInterval(interval);
            }
        }, 100);
    }

    waitForReady(function() {
        injectUI();
        startObserver();
    }, 10000);

})();