E-Hentai Comment Watcher

Monitor new comments on e-hentai galleries.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         E-Hentai Comment Watcher
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Monitor new comments on e-hentai galleries.
// @author       moodyclaus
// @match        *://e-hentai.org/*
// @match        *://exhentai.org/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_addValueChangeListener
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.notification
// @grant        GM.addStyle
// @grant        GM.addValueChangeListener
// @connect      e-hentai.org
// @connect      exhentai.org
// @exclude      *://e-hentai.org/archiver.php*
// @exclude      *://exhentai.org/archiver.php*
// @exclude      *://e-hentai.org/gallery*
// @exclude      *://exhentai.org/gallery*
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // --- Configuration & Constants ---
    const LOCK_TIMEOUT = 5 * 60 * 1000;
    const STORAGE_KEY = 'eh_comment_watcher_data';
    const LOCK_KEY = 'eh_comment_watcher_lock';
    const LAST_CHECK_KEY = 'eh_comment_watcher_last_check';
    const SETTINGS_KEY = 'eh_comment_watcher_settings';
    const REFRESH_STATE_KEY = 'eh_comment_watcher_refresh_state';

    let _needsRender = true;

    const DEFAULT_SETTINGS = {
        refreshInterval: 30,
        username: '',
        alertOnEdits: false,
        openInNewTab: true,
        enableNotifications: true,
        ignoreUploaderComment: false
    };

    const THEME = {
        primary: '#f1f1f1',
        accent: '#5ca945',
        bg: 'rgba(26, 26, 26, 0.95)',
        text: '#ddd',
        border: '#444',
        shadow: '0 8px 32px 0 rgba(0, 0, 0, 0.8)'
    };

    // --- Compatibility Wrapper ---
    const _GM_getValue = typeof GM_getValue !== 'undefined' ? GM_getValue : (typeof GM !== 'undefined' ? GM.getValue : null);
    const _GM_setValue = typeof GM_setValue !== 'undefined' ? GM_setValue : (typeof GM !== 'undefined' ? GM.setValue : null);
    const _GM_xmlhttpRequest = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : (typeof GM !== 'undefined' ? GM.xmlHttpRequest : null);
    const _GM_notification = typeof GM_notification !== 'undefined' ? GM_notification : null;
    const _GM_addValueChangeListener = typeof GM_addValueChangeListener !== 'undefined' ? GM_addValueChangeListener : (typeof GM !== 'undefined' && GM.addValueChangeListener ? GM.addValueChangeListener : null);
    const _GM_addStyle = typeof GM_addStyle !== 'undefined' ? GM_addStyle : (css => {
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    });

    // --- State Management ---
    async function getState() {
        try {
            const val = _GM_getValue ? await _GM_getValue(STORAGE_KEY) : null;
            return val || { watched: {} };
        } catch (e) {
            console.error('[EH-CW] Error getting state:', e);
            return { watched: {} };
        }
    }

    async function saveState(state) {
        try {
            if (_GM_setValue) await _GM_setValue(STORAGE_KEY, state);
            broadcastSync();
        } catch (e) {
            console.error('[EH-CW] Error saving state:', e);
        }
    }

    async function getSettings() {
        try {
            const val = _GM_getValue ? await _GM_getValue(SETTINGS_KEY) : null;
            return { ...DEFAULT_SETTINGS, ...(val || {}) };
        } catch (e) {
            console.error('[EH-CW] Error getting settings:', e);
            return { ...DEFAULT_SETTINGS };
        }
    }

    async function saveSettings(settings) {
        try {
            if (_GM_setValue) await _GM_setValue(SETTINGS_KEY, settings);
            if (!_GM_addValueChangeListener && _syncChannel) {
                _syncChannel.postMessage({ type: 'settings', settings: settings });
            }
        } catch (e) {
            console.error('[EH-CW] Error saving settings:', e);
        }
    }

    async function updateSetting(key, value) {
        const settings = await getSettings();
        settings[key] = value;
        await saveSettings(settings);
    }

    async function getCheckIntervalMs() {
        const settings = await getSettings();
        const mins = parseInt(settings.refreshInterval, 10);
        return (isNaN(mins) || mins < 5 ? 5 : mins) * 60 * 1000;
    }

    // --- Lock handling on page unload ---
    let _ownsLock = false;

    function releaseLockOnUnload() {
        if (_ownsLock) {
            _ownsLock = false;
            // Best effort release when tab closes mid-scan
            if (_GM_setValue) _GM_setValue(LOCK_KEY, 0);
            // Notify other tabs that the refresh was aborted
            setRefreshing(false);
        }
    }

    window.addEventListener('pagehide', releaseLockOnUnload);
    window.addEventListener('beforeunload', releaseLockOnUnload);

    // --- Cross-tab sync via GM listener or BroadcastChannel ---
    const _syncChannel = (typeof BroadcastChannel !== 'undefined' && !_GM_addValueChangeListener) ? new BroadcastChannel('eh_comment_watcher_sync') : null;

    if (_GM_addValueChangeListener) {
        _GM_addValueChangeListener(STORAGE_KEY, async (name, old_value, new_value, remote) => {
            if (remote) {
                const parsedState = typeof new_value === 'string' ? JSON.parse(new_value) : new_value;
                await updateUI(parsedState);
            }
        });

        _GM_addValueChangeListener(REFRESH_STATE_KEY, (name, old_value, new_value, remote) => {
            if (remote) {
                _isRefreshing = !!new_value;
                updateRefreshButton(_isRefreshing);
            }
        });

        _GM_addValueChangeListener(SETTINGS_KEY, (name, old_value, new_value, remote) => {
            if (remote && new_value && typeof new_value.openInNewTab !== 'undefined') {
                applyOpenInNewTab(new_value.openInNewTab);
            }
        });
    } else if (_syncChannel) {
        _syncChannel.onmessage = async (event) => {
            const msg = event.data;
            if (msg && typeof msg === 'object') {
                if (msg.type === 'refresh') {
                    _isRefreshing = msg.inProgress;
                    updateRefreshButton(_isRefreshing);
                } else if (msg.type === 'settings') {
                    if (msg.settings && typeof msg.settings.openInNewTab !== 'undefined') {
                        applyOpenInNewTab(msg.settings.openInNewTab);
                    }
                }
            } else {
                await updateUI();
            }
        };
    }

    let _broadcastTimer = null;
    function broadcastSync() {
        if (!_syncChannel) return;
        // Debounce: coalesce rapid successive saves into a single broadcast (fallback only)
        if (_broadcastTimer) clearTimeout(_broadcastTimer);
        _broadcastTimer = setTimeout(() => {
            _syncChannel.postMessage('update');
            _broadcastTimer = null;
        }, 300);
    }

    // --- Refresh state: shared across all open tabs ---
    let _isRefreshing = false;

    function updateRefreshButton(inProgress) {
        const btn = document.querySelector('.refresh-all');
        if (!btn) return;
        btn.textContent = inProgress ? '...' : '🔄';
        btn.title = inProgress ? 'Refreshing...' : 'Refresh all galleries';
        btn.disabled = inProgress;
    }

    function setRefreshing(inProgress) {
        _isRefreshing = inProgress;
        updateRefreshButton(inProgress);

        if (_GM_addValueChangeListener && _GM_setValue) {
            _GM_setValue(REFRESH_STATE_KEY, inProgress);
        } else if (_syncChannel) {
            _syncChannel.postMessage({ type: 'refresh', inProgress });
        }
    }

    async function openSettings() {
        const modal = document.getElementById('eh-settings-modal');
        if (!modal) return;
        const settings = await getSettings();

        const intervalInput = modal.querySelector('#eh-setting-interval');
        const usernameInput = modal.querySelector('#eh-setting-username');
        const editsCheckbox = modal.querySelector('#eh-setting-edits');
        const ignoreUploaderCheckbox = modal.querySelector('#eh-setting-ignore-uploader');
        const newTabCheckbox = modal.querySelector('#eh-setting-newtab');
        const notifCheckbox = modal.querySelector('#eh-setting-notif');

        if (intervalInput) intervalInput.value = settings.refreshInterval || 30;
        if (usernameInput) usernameInput.value = settings.username || '';
        if (editsCheckbox) editsCheckbox.checked = !!settings.alertOnEdits;
        if (ignoreUploaderCheckbox) ignoreUploaderCheckbox.checked = !!settings.ignoreUploaderComment;
        if (newTabCheckbox) newTabCheckbox.checked = settings.openInNewTab !== false;
        if (notifCheckbox) notifCheckbox.checked = settings.enableNotifications !== false;

        modal.classList.add('open');
        const panel = document.getElementById('eh-watcher-panel');
        if (panel) panel.classList.add('settings-active');
    }

    // --- Utilities ---
    const escapeHtml = (unsafe) => {
        return (unsafe || '').toString().replace(/[&<"'>]/g, function (match) {
            const escapeMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
            return escapeMap[match];
        });
    };

    const getGalleryId = (url) => {
        const match = url.match(/\/g\/(\d+)\/([a-z0-9]+)/);
        return match ? `${match[1]}_${match[2]}` : null;
    };

    const normalizeGalleryUrl = (url) => {
        let base = url.split(/[?#]/)[0];
        if (!base.endsWith('/')) base += '/';
        return base;
    };

    const isGalleryPage = () => {
        return window.location.pathname.startsWith('/g/') && getGalleryId(window.location.href);
    };

    const findNewerVersion = (htmlDoc) => {
        const gnd = htmlDoc.getElementById('gnd');
        if (!gnd) return null;
        const links = gnd.querySelectorAll('a[href*="/g/"]');
        // Return the last link, which is the most recent version
        return links.length > 0 ? links[links.length - 1].href : null;
    };

    const parseEHDate = (str) => {
        const months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
        const match = str.match(/(\d{1,2}) (\w+) (\d{4}), (\d{2}):(\d{2})/);
        if (!match) return null;
        const [_, day, monthStr, year, hour, min] = match;
        const monthIndex = months.indexOf(monthStr.toLowerCase().substring(0, 3));
        if (monthIndex === -1) return null;
        return Date.UTC(parseInt(year), monthIndex, parseInt(day), parseInt(hour), parseInt(min));
    };

    // --- Core Logic: Comment Extraction ---
    function extractLatestComments(htmlDoc, lastTimestamp = 0, lastEditTimestamp = 0, filterUsername = '', alertOnEdits = false, ignoreUploaderComment = false) {
        let commentDivs = Array.from(htmlDoc.querySelectorAll('#cdiv .c1'));
        if (ignoreUploaderComment) {
            commentDivs = commentDivs.filter(div =>
                !div.querySelector('#comment_0')
            );
        }

        if (!commentDivs.length) return { timestamp: 0, count: 0, newCount: 0, lastCommenter: '', latestEditTs: 0, newEditCount: 0, oldestUnreadId: null, lastCommentId: null };

        const normalizedFilter = filterUsername.trim().toLowerCase();
        let latestTs = 0;
        let lastCommenter = '';
        let lastCommentId = null;
        let newCount = 0;
        let latestEditTs = 0;
        let newEditCount = 0;
        let minUnreadTs = Infinity;
        let oldestUnreadId = null;

        commentDivs.forEach(div => {
            const header = div.querySelector('.c3');
            const commenterLink = header ? header.querySelector('a') : null;
            const commenterName = commenterLink ? commenterLink.textContent.trim() : '';
            // Own-comment check: only active when a non-empty filter is configured
            const isOwnComment = normalizedFilter !== '' && commenterName.toLowerCase() === normalizedFilter;

            const c6 = div.querySelector('.c6');
            const commentId = c6 ? c6.id : null;

            if (header) {
                const ts = parseEHDate(header.textContent);
                if (ts) {
                    // Always update display metadata (unfiltered)
                    if (ts > latestTs) {
                        latestTs = ts;
                        lastCommenter = commenterName || 'Unknown';
                        lastCommentId = commentId;
                    }
                    // Only count toward new if not own comment
                    if (ts > lastTimestamp && !isOwnComment) {
                        newCount++;
                        if (ts < minUnreadTs && commentId) {
                            minUnreadTs = ts;
                            oldestUnreadId = commentId;
                        }
                    }
                }
            }

            // Parse edit timestamp from div.c8 ("Last edited on <strong>DD Month YYYY, HH:MM</strong>.")
            const editDiv = div.querySelector('.c8');
            if (editDiv) {
                const strongEl = editDiv.querySelector('strong');
                if (strongEl) {
                    const editTs = parseEHDate(editDiv.textContent);
                    if (editTs) {
                        // Always update display metadata (unfiltered)
                        if (editTs > latestEditTs) latestEditTs = editTs;
                        // Only count toward new edits if not own edit
                        if (editTs > lastEditTimestamp && !isOwnComment) {
                            newEditCount++;
                            if (alertOnEdits && editTs < minUnreadTs && commentId) {
                                minUnreadTs = editTs;
                                oldestUnreadId = commentId;
                            }
                        }
                    }
                }
            }
        });

        return {
            timestamp: latestTs,
            count: commentDivs.length,
            newCount: newCount,
            lastCommenter: lastCommenter,
            lastCommentId: lastCommentId,
            latestEditTs: latestEditTs,
            newEditCount: newEditCount,
            oldestUnreadId: oldestUnreadId
        };
    }

    // --- Background Polling with Coordination ---
    async function checkGalleries(force = false) {
        const now = Date.now();
        const lastGlobalCheck = (await _GM_getValue(LAST_CHECK_KEY)) || 0;
        const currentLock = (await _GM_getValue(LOCK_KEY)) || 0;

        if (!force) {
            const intervalMs = await getCheckIntervalMs();
            if (now - lastGlobalCheck < intervalMs) return;
            if (currentLock && (now - currentLock < LOCK_TIMEOUT)) return;
        }

        await _GM_setValue(LOCK_KEY, now);
        _ownsLock = true;
        console.log('[EH-CW] ' + (force ? 'Manual' : 'Lock') + ' acquired. Starting background poll...');
        setRefreshing(true);

        try {

            const state = await getState();
            // Sort galleries so the ones checked longest ago are processed first
            const galleries = Object.values(state.watched || {}).sort((a, b) => (a.lastChecked || 0) - (b.lastChecked || 0));
            // Hoist settings above the loop — no need to re-read for every gallery
            const settings = await getSettings();
            const filterUsername = (settings.username || '').trim();

            if (galleries.length === 0) {
                await _GM_setValue(LAST_CHECK_KEY, now);
                await _GM_setValue(LOCK_KEY, 0);
            } else {

                let chunkUpdates = [];
                let firstNotification = null; // title/commenter for the first new-activity transition
                let processedInChunk = 0;
                const CHUNK_SIZE = 3;

                for (let i = 0; i < galleries.length; i++) {
                    const gallery = galleries[i];
                    const stagger = 500 + Math.random() * 1000;
                    await new Promise(resolve => setTimeout(resolve, stagger));

                    try {
                        await new Promise((resolve, reject) => {
                            _GM_xmlhttpRequest({
                                method: 'GET',
                                url: gallery.url,
                                onload: async (response) => {
                                    const parser = new DOMParser();
                                    const doc = parser.parseFromString(response.responseText, 'text/html');
                                    const newerUrl = findNewerVersion(doc);

                                    // Extract info statically
                                    let isUnavailable = false;
                                    let info = null;
                                    let newId = null;
                                    let newTitle = null;
                                    let docTitle = null;

                                    if (newerUrl) {
                                        newId = getGalleryId(newerUrl);
                                        const newerLink = doc.querySelector('#gnd a');
                                        newTitle = newerLink ? newerLink.textContent.trim() : null;
                                    } else {
                                        isUnavailable = !!doc.getElementById('continue');
                                        if (!isUnavailable) {
                                            info = extractLatestComments(doc, gallery.lastTimestamp, gallery.lastEditTimestamp || 0, filterUsername, settings.alertOnEdits, settings.ignoreUploaderComment);
                                            const titleEl = doc.getElementById('gn') || doc.getElementById('gj');
                                            if (titleEl) docTitle = titleEl.textContent.trim();
                                        }
                                    }

                                    chunkUpdates.push((currentState) => {
                                        const g = currentState.watched[gallery.id];
                                        if (!g) return false;

                                        let updated = false;

                                        if (newerUrl) {
                                            if (newId && newId !== g.id) {
                                                console.log(`[EH-CW] Switching gallery ${g.id} to newer version ${newId}`);
                                                delete currentState.watched[g.id];
                                                currentState.watched[newId] = {
                                                    ...g, id: newId, url: normalizeGalleryUrl(newerUrl), title: newTitle || g.title,
                                                    lastTimestamp: 0, hasNew: true, newComments: 1,
                                                    lastCommenter: 'System (Newer Version available)',
                                                    lastChecked: Date.now()
                                                };
                                                if (!firstNotification) firstNotification = { title: g.title, commenter: 'New version detected and swapped.', type: 'System', url: newerUrl, id: newId };
                                                updated = true;
                                            } else {
                                                g.lastChecked = Date.now();
                                            }
                                        } else {
                                            g.lastChecked = Date.now();

                                            if (docTitle && docTitle !== g.title) {
                                                g.title = docTitle;
                                                updated = true;
                                            }

                                            const wasUnavailable = !!g.unavailable;
                                            if (isUnavailable !== wasUnavailable) {
                                                g.unavailable = isUnavailable;
                                                updated = true;
                                            }

                                            if (!isUnavailable && info) {
                                                if (info.timestamp > g.lastTimestamp) {
                                                    g.lastTimestamp = info.timestamp;
                                                    g.lastCommenter = info.lastCommenter;
                                                    g.lastCommentId = info.lastCommentId;
                                                    g.count = info.count;
                                                    updated = true;
                                                    if (info.newCount > 0) {
                                                        const wasNew = g.hasNew;
                                                        g.newComments = (g.newComments || 0) + info.newCount;
                                                        g.hasNew = true;
                                                        if (!g.oldestUnreadId && info.oldestUnreadId) g.oldestUnreadId = info.oldestUnreadId;
                                                        if (!wasNew && !firstNotification) firstNotification = { title: g.title, commenter: info.lastCommenter, type: 'Comment', url: g.url, id: g.id, oldestUnreadId: info.oldestUnreadId };
                                                    }
                                                } else if (info.count !== g.count) {
                                                    g.count = info.count;
                                                    g.lastTimestamp = info.timestamp;
                                                    g.lastCommenter = info.lastCommenter;
                                                    g.lastCommentId = info.lastCommentId;
                                                    updated = true;
                                                }

                                                if (settings.alertOnEdits && info.latestEditTs > (g.lastEditTimestamp || 0)) {
                                                    g.lastEditTimestamp = info.latestEditTs;
                                                    updated = true;
                                                    if (info.newEditCount > 0) {
                                                        const wasNew = g.hasNew;
                                                        g.newEdits = (g.newEdits || 0) + info.newEditCount;
                                                        g.hasNew = true;
                                                        if (!g.oldestUnreadId && info.oldestUnreadId) g.oldestUnreadId = info.oldestUnreadId;
                                                        if (!wasNew && !firstNotification) firstNotification = { title: g.title, commenter: info.lastCommenter, type: 'Edit', url: g.url, id: g.id, oldestUnreadId: info.oldestUnreadId };
                                                    }
                                                }

                                                if (g.newComments > g.count) {
                                                    g.newComments = g.count;
                                                    updated = true;
                                                }
                                                if (g.newComments === 0 && !g.newEdits) {
                                                    if (g.hasNew) {
                                                        g.hasNew = false;
                                                        updated = true;
                                                    }
                                                    if (g.oldestUnreadId) {
                                                        delete g.oldestUnreadId;
                                                        updated = true;
                                                    }
                                                }
                                            }
                                        }
                                        return updated;
                                    });
                                    resolve();
                                },
                                onerror: reject
                            });
                        });
                    } catch (err) {
                        console.error(`[EH-CW] Failed to check gallery ${gallery.id}:`, err);
                    }

                    processedInChunk++;
                    if (processedInChunk >= CHUNK_SIZE && chunkUpdates.length > 0) {
                        const currentState = await getState();
                        let chunkChanged = false;
                        for (const fn of chunkUpdates) {
                            if (fn(currentState)) chunkChanged = true;
                        }
                        if (chunkChanged) {
                            if (firstNotification) {
                                await triggerNotification(firstNotification, settings);
                                firstNotification = null;
                            }
                            await saveState(currentState);
                            await updateUI(currentState);
                        }
                        chunkUpdates = [];
                        processedInChunk = 0;
                    }
                }

                // Final save + UI update for any remaining galleries in the last chunk
                if (chunkUpdates.length > 0) {
                    const currentState = await getState();
                    let chunkChanged = false;
                    for (const fn of chunkUpdates) {
                        if (fn(currentState)) chunkChanged = true;
                    }
                    if (chunkChanged) {
                        if (firstNotification) {
                            await triggerNotification(firstNotification, settings);
                            firstNotification = null;
                        }
                        await saveState(currentState);
                        await updateUI(currentState);
                    }
                }

            } // end of else (galleries.length > 0)

            await _GM_setValue(LAST_CHECK_KEY, Date.now());
            await _GM_setValue(LOCK_KEY, 0);
            console.log('[EH-CW] Poll complete. Lock released.');

        } finally {
            _ownsLock = false;
            setRefreshing(false);
        }
    }

    async function triggerNotification(notificationData, settingsObj = null) {
        const settings = settingsObj || await getSettings();
        if (!settings.enableNotifications) return;

        if (_GM_notification) {
            const isEdit = notificationData.type === 'Edit';
            let titleText = 'New Comment on E-Hentai';
            if (isEdit) titleText = 'New Edit on E-Hentai';
            else if (notificationData.type === 'System') titleText = 'New Version Detected';

            const targetUrl = notificationData.oldestUnreadId ? `${notificationData.url}#${notificationData.oldestUnreadId}` : notificationData.url;

            _GM_notification({
                title: titleText,
                text: `Gallery: ${notificationData.title}\nLatest by: ${notificationData.commenter}`,
                url: targetUrl,
                onclick: async (event) => {
                    event.preventDefault();
                    const settings = await getSettings();
                    const linkTarget = settings.openInNewTab !== false ? '_blank' : '_self';
                    if (notificationData.id) {
                        await markAsRead(notificationData.id);
                    }
                    if (targetUrl) {
                        window.open(targetUrl, linkTarget);
                    }
                }
            });
        }
    }

    // --- UI: Watch Button ---
    async function injectWatchButton() {
        try {
            if (!isGalleryPage()) return;

            const container = document.getElementById('gd5');
            if (!container) {
                setTimeout(injectWatchButton, 1000);
                return;
            }

            const id = getGalleryId(window.location.href);
            let btn = document.getElementById('eh-watch-btn');
            if (!btn) {
                btn = document.createElement('p');
                btn.className = 'g3';
                btn.id = 'eh-watch-btn';
                container.appendChild(btn);
            }

            const state = await getState();
            const newerUrl = findNewerVersion(document);

            if (newerUrl) {
                if (btn) btn.style.display = 'none';
                return;
            }

            let isWatched = !!(state && state.watched && state.watched[id]);
            let displayLabel = isWatched ? 'Unwatch comments' : 'Watch Comments';

            const img = container.querySelector('img');
            const imgSrc = img ? img.src : 'https://e-hentai.org/img/mr.gif';

            const linkStyle = `font-weight: bold; cursor: pointer; text-decoration: none; color: inherit`;
            btn.innerHTML = `<img src="${imgSrc}" style="vertical-align:middle; margin-right:5px"><a href="#" onclick="return false" style="${linkStyle}">${displayLabel}</a>`;

            btn.onclick = async (e) => {
                e.preventDefault();
                try {
                    const currentState = await getState();
                    if (currentState.watched && currentState.watched[id]) {
                        delete currentState.watched[id];
                    } else {
                        const settings = await getSettings();
                        const info = extractLatestComments(document, 0, 0, '', settings.alertOnEdits, settings.ignoreUploaderComment);
                        const titleEl = document.getElementById('gn') || document.getElementById('gj');
                        const title = titleEl ? titleEl.textContent.trim() : 'Unknown Gallery';

                        if (!currentState.watched) currentState.watched = {};
                        currentState.watched[id] = {
                            id: id, url: normalizeGalleryUrl(window.location.href), title: title,
                            lastTimestamp: info.timestamp, hasNew: false, newComments: 0,
                            count: info.count, lastCommenter: info.lastCommenter,
                            lastCommentId: info.lastCommentId,
                            lastChecked: Date.now()
                        };
                    }
                    await saveState(currentState);
                    await injectWatchButton();
                    await updateUI(currentState);
                } catch (clickErr) {
                    console.error('[EH-CW] Error during button click:', clickErr);
                }
            };
        } catch (err) {
            console.error('[EH-CW] Error in injectWatchButton:', err);
        }
    }

    // --- UI: Badge & Panel ---
    async function updateUI(state) {
        if (!state) state = await getState();
        const watchedList = Object.values(state.watched || {});
        const badge = document.getElementById('eh-watcher-badge') || createBadge();

        const newCount = watchedList.filter(g => g.hasNew).length;
        if (watchedList.length === 0) {
            badge.style.display = 'none';
        } else {
            badge.style.display = 'flex';
            const countEl = badge.querySelector('.count');
            if (countEl) countEl.textContent = newCount;
            badge.classList.toggle('has-new', newCount > 0);
        }
        const panel = document.getElementById('eh-watcher-panel');
        if (panel && panel.classList.contains('open')) {
            await renderPanel(state);
        } else {
            _needsRender = true;
        }
    }

    function createBadge() {
        const badge = document.createElement('div');
        badge.id = 'eh-watcher-badge';
        badge.innerHTML = `<span class="icon">💬</span><span class="count">0</span>`;
        document.body.appendChild(badge);

        _GM_addStyle(`
            #eh-watcher-badge {
                position: fixed; bottom: 20px; right: 20px; z-index: 10000;
                width: 50px; height: 50px; border-radius: 50%;
                background: ${THEME.bg}; border: 1px solid ${THEME.border};
                display: flex; align-items: center; justify-content: center;
                cursor: pointer; color: ${THEME.text}; font-weight: bold;
                box-shadow: ${THEME.shadow}; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                user-select: none;
            }
            #eh-watcher-badge.has-new { border-color: ${THEME.accent}; box-shadow: 0 0 15px ${THEME.accent}80; }
            #eh-watcher-badge .count { position: absolute; top: -5px; right: -5px; background: #3d852a; color: white; font-size: 10px; padding: 2px 6px; border-radius: 10px; min-width: 12px; text-align: center; }
            #eh-watcher-panel { position: fixed; bottom: 80px; right: 20px; z-index: 10001; width: 340px; max-height: 500px; background: ${THEME.bg}; border: 1px solid ${THEME.border}; border-radius: 12px; box-shadow: ${THEME.shadow}; display: none; flex-direction: column; padding: 0; color: ${THEME.text}; overflow: auto; text-align: left !important; }
            #eh-watcher-main-view { width: 100%; max-height: 500px; display: flex; flex-direction: column; padding: 15px; box-sizing: border-box; overflow-y: auto; }
            #eh-watcher-panel.open { display: flex; animation: ehSlideIn 0.3s ease; }
            @keyframes ehSlideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
            .eh-item { padding: 10px; border-bottom: 1px solid ${THEME.border}; transition: background 0.2s; }
            .eh-item:last-child { border-bottom: none; }
            .eh-item.has-new { background: rgba(92, 169, 69, 0.1); border-left: 3px solid ${THEME.accent}; }
            .eh-title { font-weight: bold; font-size: 13px; color: ${THEME.accent}; text-decoration: none; display: block; margin-bottom: 5px; }
            .eh-title-unavailable { color: #e74c3c !important; }
            .eh-meta { font-size: 11px; color: #aaa; }
            .eh-actions { margin-top: 8px; display: flex; gap: 8px; margin-left: 27px;}
            .eh-btn { background: #333; border: 1px solid #555; color: #ddd; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; }
            .eh-btn:hover { background: #444; }
            .eh-btn.read { border-color: ${THEME.accent}; color: ${THEME.accent}; }
            .eh-btn.danger { border-color: #a94442; color: #f2dede; background: #a9444220; }
            
            .eh-toolbar { display: flex; gap: 8px; padding-bottom: 15px; border-bottom: 2px solid ${THEME.border}; margin-bottom: 10px; position: sticky; top: 0; background: ${THEME.bg}; z-index: 10; }
            .eh-checkbox { cursor: pointer; width: 16px; height: 16px; margin-right: 10px; accent-color: ${THEME.accent}; }
            .eh-item-main { display: flex; align-items: flex-start; }
            .eh-item-content { flex-grow: 1; min-width: 0; }

            .eh-settings-group {
                display: flex;
                flex-direction: column;
                gap: 5px;
                margin-bottom: 12px;
            }
            .eh-settings-label {
                font-size: 12px;
                font-weight: bold;
                color: ${THEME.primary};
            }
            .eh-settings-input {
                background: rgba(255, 255, 255, 0.05) !important;
                border: 1px solid ${THEME.border} !important;
                border-radius: 6px !important;
                color: ${THEME.text} !important;
                padding: 6px 10px !important;
                font-size: 12px !important;
                transition: border-color 0.2s, box-shadow 0.2s !important;
                outline: none !important;
                box-sizing: border-box !important;
            }
            .eh-settings-input::placeholder {
                color: #8C8C8C !important;
                -webkit-text-fill-color: #8C8C8C !important;
                opacity: 1 !important;
            }
            .eh-settings-input:hover, .eh-settings-input:focus, .eh-settings-input:active {
                background: rgba(255, 255, 255, 0.08) !important;
                border-color: ${THEME.accent} !important;
                color: ${THEME.text} !important;
                box-shadow: 0 0 5px ${THEME.accent}80 !important;
                outline: none !important;
            }
            .eh-settings-description {
                font-size: 10px;
                color: #888;
                line-height: 1.4;
            }
            .eh-settings-checkbox-label {
                display: flex;
                align-items: center;
                gap: 8px;
                cursor: pointer;
                font-size: 12px;
                color: ${THEME.primary};
                user-select: none;
            }
            .eh-settings-checkbox {
                cursor: pointer;
                width: 16px;
                height: 16px;
                accent-color: ${THEME.accent};
            }

            #eh-settings-modal {
                position: absolute;
                top: 0; left: 0; right: 0; bottom: 0;
                background: ${THEME.bg};
                border-radius: 12px;
                padding: 15px;
                display: none;
                flex-direction: column;
                z-index: 10002;
                box-sizing: border-box;
            }
            #eh-settings-modal.open {
                display: flex;
                animation: ehFadeIn 0.2s ease;
            }
            @keyframes ehFadeIn {
                from { opacity: 0; }
                to { opacity: 1; }
            }
        `);

        const panel = document.createElement('div');
        panel.id = 'eh-watcher-panel';
        document.body.appendChild(panel);

        const mainView = document.createElement('div');
        mainView.id = 'eh-watcher-main-view';
        mainView.innerHTML = `
            <div class="eh-toolbar">
                <input type="checkbox" id="eh-select-all" class="eh-checkbox" title="Select All">
                <button class="eh-btn batch-read" title="Mark selected as read">Mark Read</button>
                <button class="eh-btn batch-unwatch danger" title="Unwatch selected">Unwatch</button>
                <button class="eh-btn refresh-all" id="eh-refresh-btn" title="Refresh all galleries">🔄</button>
                <button class="eh-btn settings-toggle" title="Settings">⚙️</button>
                <button class="eh-btn" style="margin-left:auto" onclick="document.getElementById('eh-watcher-panel').classList.remove('open')">✕</button>
            </div>
            <div id="eh-last-checked" style="font-size: 10px; color: #888; margin-bottom: 10px; padding-left: 5px;">
                Last checked: Never
            </div>
            <div id="eh-items-container"></div>
        `;
        panel.appendChild(mainView);

        mainView.onclick = async (e) => {
            const target = e.target;
            const id = target.getAttribute('data-id');
            const state = await getState();

            if (target.classList.contains('refresh-all')) {
                await checkGalleries(true);
                await renderPanel();
            } else if (target.classList.contains('settings-toggle')) {
                await openSettings();
            } else if (target.classList.contains('batch-read')) {
                const selectedIds = Array.from(mainView.querySelectorAll('.item-select:checked')).map(cb => cb.getAttribute('data-id'));
                for (const sid of selectedIds) {
                    if (state.watched[sid]) {
                        state.watched[sid].hasNew = false;
                        state.watched[sid].newComments = 0;
                        state.watched[sid].newEdits = 0;
                        delete state.watched[sid].oldestUnreadId;
                    }
                }
                await saveState(state);
                await updateUI(state);
            } else if (target.classList.contains('batch-unwatch')) {
                if (!confirm('Unwatch all selected galleries?')) return;
                const selectedIds = Array.from(mainView.querySelectorAll('.item-select:checked')).map(cb => cb.getAttribute('data-id'));
                for (const sid of selectedIds) delete state.watched[sid];
                await saveState(state);
                await updateUI(state);
                await injectWatchButton();
            } else if (target.classList.contains('eh-open-title')) {
                const linkId = target.getAttribute('data-id');
                if (linkId) await markAsRead(linkId);
            } else if (id) {
                if (target.classList.contains('read')) {
                    await markAsRead(id);
                } else if (target.classList.contains('unwatch')) {
                    delete state.watched[id];
                    await saveState(state);
                    await updateUI(state);
                    await injectWatchButton();
                }
            }
        };

        const selectAll = mainView.querySelector('#eh-select-all');
        if (selectAll) {
            selectAll.onclick = () => {
                const itemCheckboxes = mainView.querySelectorAll('.item-select');
                itemCheckboxes.forEach(cb => cb.checked = selectAll.checked);
            };
        }

        // Create settings modal overlay
        const settingsModal = document.createElement('div');
        settingsModal.id = 'eh-settings-modal';
        settingsModal.innerHTML = `
            <div class="eh-toolbar">
                <span style="font-weight: bold; font-size: 13px; display: flex; align-items: center; gap: 6px;">⚙️ Settings</span>
                <button class="eh-btn eh-settings-back" style="margin-left: auto;">Back</button>
            </div>
            <div id="eh-settings-container" style="display: flex; flex-direction: column; gap: 12px; padding: 10px 5px;">
                <div class="eh-settings-group">
                    <label class="eh-settings-label" for="eh-setting-interval">Auto-refresh interval (minutes)</label>
                    <input type="number" id="eh-setting-interval" class="eh-settings-input" min="5">
                    <div class="eh-settings-description">This value will determine how many minutes to wait between automatic refreshes (Minimum: 5 minutes).</div>
                </div>
                <div class="eh-settings-group">
                    <label class="eh-settings-label" for="eh-setting-username">Username</label>
                    <input type="text" id="eh-setting-username" class="eh-settings-input" placeholder="Optional">
                    <div class="eh-settings-description">Enter your username to avoid receiving alerts for your own comments.</div>
                </div>
                <div class="eh-settings-group" style="margin-top: 5px;">
                    <label class="eh-settings-checkbox-label">
                        <input type="checkbox" id="eh-setting-edits" class="eh-settings-checkbox">
                        Also notify me when existing comments are edited.
                    </label>
                </div>
                <div class="eh-settings-group">
                    <label class="eh-settings-checkbox-label">
                        <input type="checkbox" id="eh-setting-ignore-uploader" class="eh-settings-checkbox">
                        Ignore uploader comment
                    </label>
                </div>
                ${_GM_notification ? `
                <div class="eh-settings-group">
                    <label class="eh-settings-checkbox-label">
                        <input type="checkbox" id="eh-setting-notif" class="eh-settings-checkbox">
                        Enable browser desktop notifications.
                    </label>
                </div>` : ''}
                <div class="eh-settings-group">
                    <label class="eh-settings-checkbox-label">
                        <input type="checkbox" id="eh-setting-newtab" class="eh-settings-checkbox">
                        Open links in new tab.
                    </label>
                </div>
                <div class="eh-settings-group" style="margin-top: 5px;">
                    <label class="eh-settings-label">Watchlist Backup</label>
                    <div style="display: flex; flex-direction: column; gap: 6px;">
                        <button class="eh-btn" id="eh-export-btn">📤 Export Watchlist</button>
                        <button class="eh-btn" id="eh-import-replace-btn">📥 Import (Replace)</button>
                        <button class="eh-btn" id="eh-import-merge-btn">📥 Import (Merge)</button>
                    </div>
                </div>
            </div>
        `;
        panel.appendChild(settingsModal);


        // Prevent settings clicks from bubbling up and closing the panel or triggering watch panel events
        settingsModal.onclick = (e) => {
            e.stopPropagation();
        };


        const backBtn = settingsModal.querySelector('.eh-settings-back');
        if (backBtn) {
            backBtn.onclick = (e) => {
                e.stopPropagation();
                e.preventDefault();
                settingsModal.classList.remove('open');
                panel.classList.remove('settings-active');
            };
        }

        const intervalInput = settingsModal.querySelector('#eh-setting-interval');
        if (intervalInput) {
            intervalInput.onchange = async () => {
                let val = parseInt(intervalInput.value, 10);
                if (isNaN(val) || val < 5) {
                    val = 30;
                    intervalInput.value = 30;
                }
                await updateSetting('refreshInterval', val);
            };
        }

        const usernameInput = settingsModal.querySelector('#eh-setting-username');
        if (usernameInput) {
            usernameInput.oninput = async () => {
                await updateSetting('username', usernameInput.value.trim());
            };
        }

        const editsCheckbox = settingsModal.querySelector('#eh-setting-edits');
        if (editsCheckbox) {
            editsCheckbox.onchange = async () => {
                await updateSetting('alertOnEdits', editsCheckbox.checked);
            };
        }

        const ignoreUploaderCheckbox = settingsModal.querySelector('#eh-setting-ignore-uploader');
        if (ignoreUploaderCheckbox) {
            ignoreUploaderCheckbox.onchange = async () => {
                await updateSetting('ignoreUploaderComment', ignoreUploaderCheckbox.checked);
            };
        }

        const newTabCheckbox = settingsModal.querySelector('#eh-setting-newtab');
        if (newTabCheckbox) {
            newTabCheckbox.onchange = async () => {
                const enabled = newTabCheckbox.checked;
                await updateSetting('openInNewTab', enabled);
                applyOpenInNewTab(enabled);
            };
        }

        const notifCheckbox = settingsModal.querySelector('#eh-setting-notif');
        if (notifCheckbox) {
            notifCheckbox.onchange = async () => {
                await updateSetting('enableNotifications', notifCheckbox.checked);
            };
        }

        const exportBtn = settingsModal.querySelector('#eh-export-btn');
        if (exportBtn) {
            exportBtn.onclick = async (e) => {
                e.stopPropagation();
                await exportWatchlist();
            };
        }

        const importReplaceBtn = settingsModal.querySelector('#eh-import-replace-btn');
        if (importReplaceBtn) {
            importReplaceBtn.onclick = (e) => {
                e.stopPropagation();
                const confirmed = window.confirm(
                    'Import (Replace)\n\n' +
                    'WARNING: Your entire current watchlist will be permanently replaced by the imported file.\n' +
                    'All existing watched galleries and their stored state will be lost.\n' +
                    'This action cannot be undone.\n\n' +
                    'Continue?'
                );
                if (!confirmed) return;
                triggerFilePicker((file) => importWatchlist(file, 'replace'));
            };
        }

        const importMergeBtn = settingsModal.querySelector('#eh-import-merge-btn');
        if (importMergeBtn) {
            importMergeBtn.onclick = (e) => {
                e.stopPropagation();
                const confirmed = window.confirm(
                    'Import (Merge)\n\n' +
                    'Galleries from the imported file will be added to your existing watchlist.\n' +
                    'Galleries whose ID already exists in your watchlist will be skipped.\n' +
                    'No existing gallery will be removed or modified.\n\n' +
                    'Continue?'
                );
                if (!confirmed) return;
                triggerFilePicker((file) => importWatchlist(file, 'merge'));
            };
        }

        badge.onclick = (e) => {
            e.stopPropagation();
            panel.classList.toggle('open');
            if (panel.classList.contains('open')) {
                if (_needsRender) {
                    const itemsContainer = document.getElementById('eh-items-container');
                    if (itemsContainer && !itemsContainer.innerHTML.trim()) {
                        itemsContainer.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">Loading...</div>';
                    }
                    renderPanel();
                } else {
                    _GM_getValue(LAST_CHECK_KEY, 0).then(lastCheck => {
                        const lastCheckStr = lastCheck ? new Date(lastCheck).toLocaleTimeString() : 'Never';
                        const timeEl = document.getElementById('eh-last-checked');
                        if (timeEl) timeEl.textContent = `Last checked: ${lastCheckStr}`;
                    });
                }
            } else {
                settingsModal.classList.remove('open');
                panel.classList.remove('settings-active');
            }
        };

        // Prevent clicks inside the panel from bubbling to the document-level close handler.
        // Without this, Chrome+Tampermonkey's sandbox causes panel.contains(e.target) to
        // return false for dynamically rendered buttons, closing the panel on every click.
        panel.addEventListener('click', (e) => e.stopPropagation());

        // Close on click outside
        document.addEventListener('click', (e) => {
            if (panel.classList.contains('open') && !badge.contains(e.target)) {
                panel.classList.remove('open');
                settingsModal.classList.remove('open');
                panel.classList.remove('settings-active');
            }
        });

        return badge;
    }

    function applyOpenInNewTab(enabled) {
        document.querySelectorAll('.eh-open-title').forEach(a => {
            if (enabled) {
                a.setAttribute('target', '_blank');
                a.setAttribute('rel', 'noopener noreferrer');
            } else {
                a.removeAttribute('target');
                a.removeAttribute('rel');
            }
        });
    }

    // --- Import / Export ---

    function validateImportFile(parsed) {
        if (!parsed || typeof parsed !== 'object') {
            return { valid: false, reason: 'File is not a valid JSON object.', userMessage: 'The selected file is not a valid JSON file.' };
        }
        if (parsed.version !== 1) {
            return { valid: false, reason: `Unknown format version: ${parsed.version}. Expected version 1.`, userMessage: 'This file is not a valid watchlist export.' };
        }
        if (!Array.isArray(parsed.galleries)) {
            return { valid: false, reason: 'Missing or invalid \'galleries\' array.', userMessage: 'This file is not a valid watchlist export.' };
        }
        if (parsed.galleries.length === 0) {
            return { valid: false, reason: 'The file contains no galleries.', userMessage: 'This file does not contain any galleries.' };
        }
        for (const g of parsed.galleries) {
            if (!g.id || !g.url || !g.title) {
                return { valid: false, reason: `One or more galleries are missing required fields (id, url, title).`, userMessage: 'This file contains invalid or corrupted data.' };
            }
            if (!g.url.startsWith('https://e-hentai.org/') && !g.url.startsWith('https://exhentai.org/')) {
                return { valid: false, reason: `Invalid gallery URL: ${g.url}`, userMessage: 'This file contains invalid or corrupted data.' };
            }
        }
        return { valid: true };
    }

    async function exportWatchlist() {
        const state = await getState();
        const galleries = Object.values(state.watched || {});
        if (galleries.length === 0) {
            alert('Your watchlist is empty. Nothing to export.');
            return;
        }
        const payload = {
            version: 1,
            exportedAt: new Date().toISOString(),
            galleries
        };
        const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const dateStr = new Date().toISOString().substring(0, 10);
        const a = document.createElement('a');
        a.href = url;
        a.download = `eh-comment-watcher-export-${dateStr}.json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    async function importWatchlist(file, mode) {
        let parsed;
        try {
            const text = await file.text();
            parsed = JSON.parse(text);
        } catch (e) {
            console.log(`[EH-CW] Import failed: JSON parse error or file read error.`);
            alert('The selected file is not a valid JSON file.');
            return;
        }

        const validation = validateImportFile(parsed);
        if (!validation.valid) {
            console.log(`[EH-CW] Import failed: ${validation.reason}`);
            alert(validation.userMessage || 'Import failed: invalid file.');
            return;
        }

        const state = await getState();
        if (!state.watched) state.watched = {};

        if (mode === 'replace') {
            state.watched = {};
            for (const g of parsed.galleries) {
                state.watched[g.id] = g;
            }
            await saveState(state);
            await updateUI(state);
            alert(`Imported ${parsed.galleries.length} galleries.`);
        } else { // merge
            let imported = 0;
            let skipped = 0;
            for (const g of parsed.galleries) {
                if (state.watched[g.id]) {
                    skipped++;
                } else {
                    state.watched[g.id] = g;
                    imported++;
                }
            }
            await saveState(state);
            await updateUI(state);
            alert(`Imported ${imported} ${imported === 1 ? 'gallery' : 'galleries'}, skipped ${skipped} ${skipped === 1 ? 'duplicate' : 'duplicates'}.`);
        }
    }

    function triggerFilePicker(callback) {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json,application/json';
        input.style.display = 'none';
        document.body.appendChild(input);
        input.onchange = () => {
            const file = input.files[0];
            document.body.removeChild(input);
            if (file) callback(file);
        };
        input.click();
    }

    async function renderPanel(state) {
        const itemsContainer = document.getElementById('eh-items-container');
        if (!itemsContainer) return;

        if (!state) state = await getState();
        const lastCheck = await _GM_getValue(LAST_CHECK_KEY, 0);
        const lastCheckStr = lastCheck ? new Date(lastCheck).toLocaleTimeString() : 'Never';

        const timeEl = document.getElementById('eh-last-checked');
        if (timeEl) timeEl.textContent = `Last checked: ${lastCheckStr}`;

        const settings = await getSettings();
        const alertOnEdits = settings.alertOnEdits;

        const watchedList = Object.values(state.watched || {}).sort((a, b) => {
            const aTime = alertOnEdits ? Math.max(a.lastTimestamp || 0, a.lastEditTimestamp || 0) : (a.lastTimestamp || 0);
            const bTime = alertOnEdits ? Math.max(b.lastTimestamp || 0, b.lastEditTimestamp || 0) : (b.lastTimestamp || 0);
            return bTime - aTime;
        });

        let html = '';
        if (watchedList.length === 0) {
            html = `<p style="font-size: 12px; text-align: center; color: #888; padding: 20px;">No galleries watched yet.</p>`;
        } else {
            const linkTarget = settings.openInNewTab !== false ? '_blank' : '_self';
            watchedList.forEach(g => {
                const targetUrl = (g.hasNew && g.oldestUnreadId) ? `${g.url}#${g.oldestUnreadId}` : g.url;
                html += `
                <div class="eh-item ${g.hasNew ? 'has-new' : ''}" data-id="${g.id}">
                    <div class="eh-item-main">
                        <input type="checkbox" class="eh-checkbox item-select" data-id="${g.id}">
                        <div class="eh-item-content">
                            <a href="${targetUrl}" class="eh-title eh-open-title${g.unavailable ? ' eh-title-unavailable' : ''}" data-id="${g.id}" target="${linkTarget}">${escapeHtml(g.title)}</a>
                            <div class="eh-meta">
                                ${g.unavailable
                        ? 'Gallery Unavailable'
                        : g.hasNew ? (() => {
                            const parts = [];
                            if (g.newComments > 0) parts.push(`${g.newComments} new comment${g.newComments === 1 ? '' : 's'}`);
                            if (g.newEdits > 0) parts.push(`${g.newEdits} new edit${g.newEdits === 1 ? '' : 's'}`);
                            return `<b>${parts.join(', ')}</b>`;
                        })() : 'No new comments'}<br>
                                ${g.unavailable ? 'Last: N/A' : (() => {
                        const latestActivityTs = Math.max(g.lastTimestamp || 0, g.lastEditTimestamp || 0);
                        const isEdit = (g.lastEditTimestamp || 0) > (g.lastTimestamp || 0);
                        const tsStr = latestActivityTs ? `<span style="font-size: 0.9em; opacity: 0.7;">(${new Date(latestActivityTs).toISOString().replace('T', ' ').substring(0, 16)}${isEdit ? ', Edit' : ''})</span>` : '';
                        return `Last: ${escapeHtml(g.lastCommenter) || 'N/A'} ${tsStr}`;
                    })()}
                            </div>
                        </div>
                    </div>
                    <div class="eh-actions">
                        ${g.hasNew ? `<button class="eh-btn read" data-id="${g.id}">Mark Read</button>` : ''}
                        <button class="eh-btn unwatch" data-id="${g.id}">Unwatch</button>
                    </div>
                </div>`;
            });
        }
        itemsContainer.innerHTML = html;
        _needsRender = false;

        const selectAllCb = document.getElementById('eh-select-all');
        if (selectAllCb) selectAllCb.checked = false;
    }

    async function markAsRead(id) {
        const state = await getState();
        if (state.watched && state.watched[id]) {
            state.watched[id].hasNew = false;
            state.watched[id].newComments = 0;
            state.watched[id].newEdits = 0;
            delete state.watched[id].oldestUnreadId;
            await saveState(state);
            await updateUI(state);
            // Only update the watch button when actually on a gallery page
            if (isGalleryPage()) await injectWatchButton();
        }
    }

    async function runLoop() {
        await checkGalleries();
        setTimeout(runLoop, 60000);
    }

    async function init() {
        // Skip execution entirely on unavailable pages (they auto-redirect anyway)
        if (document.getElementById('continue')) {
            console.log('[EH-CW] Unavailable gallery detected, skipping init.');
            return;
        }

        console.log('[EH-CW] Initializing UI components...');
        const refreshState = await _GM_getValue(REFRESH_STATE_KEY);
        if (refreshState) {
            _isRefreshing = true;
            updateRefreshButton(true);
        }

        await injectWatchButton();
        const state = await getState();
        await updateUI(state);
        if (isGalleryPage()) {
            const currentId = getGalleryId(window.location.href);

            if (currentId && state.watched && state.watched[currentId]) {
                const g = state.watched[currentId];

                // Automatic migration to ExHentai if previously unavailable
                if (g.unavailable && window.location.hostname.includes('exhentai.org')) {
                    g.unavailable = false;
                    g.url = normalizeGalleryUrl(window.location.href);
                    await saveState(state);
                    await updateUI(state);
                }

                if (g.hasNew) {
                    await markAsRead(currentId);
                }
            }
        }
        runLoop();
    }

    init();
})();