E-Hentai Comment Watcher

Monitor new comments on e-hentai galleries.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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