TSM Tag Catcher

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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);

})();