YouTube Comment Enhancer

YouTube 评论增强:J/K 导航、大图展示评论、Z/X 点赞点踩、磨砂背景。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey 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 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.

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

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

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

// ==UserScript==
// @name         YouTube Comment Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  YouTube 评论增强:J/K 导航、大图展示评论、Z/X 点赞点踩、磨砂背景。
// @author       Antigravity
// @match        *://www.youtube.com/watch*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /**
     * [模块 0] 配置中心
     */
    const SITE_CONFIG = {
        name: "YouTube Comments",
        selectors: {
            item: 'ytd-comment-thread-renderer, ytd-comment-view-model',
            author: '#author-text',
            avatar: '#author-thumbnail img',
            content: '#content-text',
            replyContent: '#content-text',
            likeBtn: '#like-button',
            dislikeBtn: '#dislike-button',
            translateBtn: '.translate-button, #translate-button, ytd-tri-state-button-view-model',
            // 视频层级选择器
            videoLikeBtn: 'segmented-like-dislike-button-view-model like-button-view-model button',
            videoDislikeBtn: 'segmented-like-dislike-button-view-model dislike-button-view-model button',
            subscribeBtn: 'ytd-subscribe-button-renderer button, #subscribe-button button'
        },
        theme: { accent: '#ff0000', overlayBg: 'rgba(0,0,0,0.77)', borderRadius: '24px' }
    };

    let policy = { createHTML: (s) => s };
    if (window.trustedTypes?.createPolicy) {
        try { policy = window.trustedTypes.createPolicy('enhancer-policy-watch', { createHTML: (s) => s }); } catch (e) { }
    }
    const setHTML = (el, html) => {
        if (!el) return;
        try { el.innerHTML = policy.createHTML(html); } catch (e) { el.textContent = html.replace(/<[^>]*>/g, ''); }
    };

    /**
     * [模块 1] Core: 评论数据提取
     */
    const App_Core = (() => {
        return {
            getCommentData: (card) => {
                if (!card) return null;
                const authorEl = card.querySelector(SITE_CONFIG.selectors.author);
                const avatarEl = card.querySelector(SITE_CONFIG.selectors.avatar);
                const contentEl = card.querySelector(SITE_CONFIG.selectors.content);

                return {
                    row: card,
                    author: authorEl?.innerText.trim() || "User",
                    avatarUrl: avatarEl?.src || avatarEl?.getAttribute('src'),
                    content: contentEl?.innerText.trim() || ""
                };
            },
            findComments: () => {
                const all = Array.from(document.querySelectorAll(SITE_CONFIG.selectors.item));
                return all.filter(c => {
                    // 核心过滤:排除嵌套关系,确保每个评论只抓取一次最外层
                    let p = c.parentElement;
                    while (p) { if (p.matches?.(SITE_CONFIG.selectors.item)) return false; p = p.parentElement; }
                    return c.offsetHeight > 20;
                });
            }
        };
    })();

    /**
     * [模块 2] UI: 评论大图预览
     */
    const App_UI = (() => {
        let overlay, vCard, vText, vAvatar, vAuthor, counter;
        let isEnabled = true;

        const injectCSS = () => {
            if (document.getElementById('enhancer-watch-css')) return;
            const s = document.createElement('style'); s.id = 'enhancer-watch-css';
            s.textContent = `
                .eh-comment-active { outline: 3px solid ${SITE_CONFIG.theme.accent} !important; border-radius: 8px !important; z-index: 10 !important; position: relative !important; }
                .eh-comment-preview-card {
                    background: rgba(255,255,255,0.05);
                    padding: 40px;
                    border-radius: ${SITE_CONFIG.theme.borderRadius};
                    max-width: 800px;
                    width: 90%;
                    box-shadow: 0 40px 100px rgba(0,0,0,0.8);
                    border: 1px solid rgba(255,255,255,0.1);
                    display: flex;
                    flex-direction: column;
                    gap: 20px;
                }
                .eh-comment-avatar { width: 80px; height: 80px; border-radius: 50%; border: 3px solid rgba(255,255,255,0.2); }
                .eh-comment-author { font-size: 24px; font-weight: 900; color: #fff; }
                .eh-comment-text { font-size: 28px; line-height: 1.5; color: rgba(255,255,255,0.9); white-space: pre-wrap; word-break: break-all; }
            `;
            document.head.appendChild(s);
        };

        const update = (data, idx, total) => {
            if (!data) return;
            setHTML(vAvatar, `<img src="${data.avatarUrl || ''}" class="eh-comment-avatar">`);
            setHTML(vAuthor, data.author);
            setHTML(vText, data.content);
            counter.innerText = `${idx + 1} / ${total}`;
        };

        return {
            show: (data, idx, total) => {
                if (!isEnabled) return;
                if (!overlay) {
                    overlay = document.createElement('div');
                    Object.assign(overlay.style, {
                        position: 'fixed', inset: 0, backgroundColor: SITE_CONFIG.theme.overlayBg,
                        zIndex: 999999, display: 'flex', alignItems: 'center', justifyContent: 'center',
                        backdropFilter: 'blur(20px) saturate(160%)', webkitBackdropFilter: 'blur(20px) saturate(160%)'
                    });

                    vCard = document.createElement('div'); vCard.className = 'eh-comment-preview-card';
                    const head = document.createElement('div');
                    Object.assign(head.style, { display: 'flex', alignItems: 'center', gap: '20px' });
                    vAvatar = document.createElement('div');
                    vAuthor = document.createElement('div'); vAuthor.className = 'eh-comment-author';
                    head.append(vAvatar, vAuthor);

                    vText = document.createElement('div'); vText.className = 'eh-comment-text';

                    const info = document.createElement('div');
                    Object.assign(info.style, { marginTop: '20px', color: 'rgba(255,255,255,0.3)', fontSize: '14px', fontWeight: '700' });
                    info.innerText = '[Z] 点赞  [X] 不喜欢 [C] 翻译 [D] 开关预览  [ESC] 关闭';

                    counter = document.createElement('div');
                    Object.assign(counter.style, { position: 'absolute', top: '30px', left: '30px', color: '#fff', fontSize: '18px', fontWeight: '900', background: 'rgba(255,255,255,0.1)', padding: '8px 16px', borderRadius: '12px' });

                    vCard.append(head, vText, info); overlay.append(vCard, counter); document.body.appendChild(overlay);
                    overlay.onclick = (e) => { if (e.target === overlay) overlay.style.display = 'none'; };
                }
                overlay.style.display = 'flex';
                update(data, idx, total);
            },
            hide: () => { if (overlay) overlay.style.display = 'none'; },
            toggle: () => {
                isEnabled = !isEnabled; if (!isEnabled) App_UI.hide();
                App_UI.showToast(isEnabled ? '评论预览:开启' : '评论预览:关闭');
                return isEnabled;
            },
            updateData: update,
            isVisible: () => overlay && overlay.style.display === 'flex' && isEnabled,
            injectCSS,
            showToast: (msg) => {
                const old = document.getElementById('enhancer-toast'); if (old) old.remove();
                const t = document.createElement('div'); t.id = 'enhancer-toast';
                Object.assign(t.style, { position: 'fixed', bottom: '50px', left: '50%', transform: 'translateX(-50%)', padding: '12px 30px', background: '#fff', color: '#000', borderRadius: '30px', zIndex: 1000005, fontWeight: '900', boxShadow: '0 15px 30px rgba(0,0,0,0.4)', transition: '0.3s' });
                t.innerText = msg; document.body.appendChild(t);
                setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 1500);
            }
        };
    })();

    /**
     * [模块 3] Nav: 评论导航
     */
    const App_Nav = (() => {
        let curIdx = -1, comments = [];
        return {
            move: (step) => {
                comments = App_Core.findComments();
                if (!comments.length) {
                    // 核心优化:如果没有评论(YouTube 没加载),自动滚一下触发加载
                    window.scrollBy({ top: 600, behavior: 'smooth' });
                    App_UI.showToast('📄 正在加载评论区...');
                    return null;
                }
                if (curIdx === -1) {
                    const center = comments.sort((a, b) => Math.abs(a.getBoundingClientRect().top - window.innerHeight / 2) - Math.abs(b.getBoundingClientRect().top - window.innerHeight / 2))[0];
                    curIdx = comments.indexOf(center);
                } else curIdx = Math.max(0, Math.min(comments.length - 1, curIdx + step));
                return { data: App_Core.getCommentData(comments[curIdx]), index: curIdx, total: comments.length };
            },
            scroll: () => {
                document.querySelectorAll('.eh-comment-active').forEach(e => e.classList.remove('eh-comment-active'));
                const c = comments[curIdx];
                if (c) { c.classList.add('eh-comment-active'); c.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
            },
            getCard: () => comments[curIdx],
            getIndex: () => curIdx,
            getTotal: () => comments.length,
            reset: () => { curIdx = -1; document.querySelectorAll('.eh-comment-active').forEach(e => e.classList.remove('eh-comment-active')); }
        };
    })();

    /**
     * [模块 4] Init
     */
    const init = () => {
        window.addEventListener('keydown', (e) => {
            if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.hasAttribute('contenteditable')) return;
            const k = e.key.toLowerCase();

            if (k === 'd') {
                const enabled = App_UI.toggle();
                if (enabled) { const r = App_Nav.move(0); if (r) App_UI.show(r.data, r.index, r.total); }
                return;
            }

            if (k === 'j' || k === 'arrowdown') {
                e.preventDefault(); const r = App_Nav.move(1);
                if (r) { App_UI.show(r.data, r.index, r.total); App_Nav.scroll(); }
            } else if (k === 'k' || k === 'arrowup') {
                e.preventDefault(); const r = App_Nav.move(-1);
                if (r) { App_UI.show(r.data, r.index, r.total); App_Nav.scroll(); }
            } else if (k === 'z' || k === 'x') {
                const card = App_Nav.getCard();
                if (card) {
                    const btn = card.querySelector(k === 'z' ? SITE_CONFIG.selectors.likeBtn : SITE_CONFIG.selectors.dislikeBtn);
                    const innerBtn = btn?.querySelector('button') || btn;
                    if (innerBtn) {
                        innerBtn.click();
                        App_UI.showToast(k === 'z' ? '👍 已点赞评论' : '👎 已踩评论');
                    }
                } else {
                    // 视频层级操作
                    const btn = document.querySelector(k === 'z' ? SITE_CONFIG.selectors.videoLikeBtn : SITE_CONFIG.selectors.videoDislikeBtn);
                    if (btn) {
                        btn.click();
                        App_UI.showToast(k === 'z' ? '👍 已点赞视频' : '👎 已踩视频');
                    }
                }
            } else if (k === 's') {
                const btn = document.querySelector(SITE_CONFIG.selectors.subscribeBtn);
                if (btn) {
                    btn.click();
                    const isSubbed = btn.innerText.includes('已') || btn.getAttribute('aria-label')?.includes('取消');
                    App_UI.showToast(isSubbed ? '✅ 已订阅' : '🔔 订阅状态变更');
                }
            } else if (k === 'c') {
                const card = App_Nav.getCard();
                if (card) {
                    // 更加暴力地寻找翻译按钮(包括内部的点击目标)
                    const translateArea = card.querySelector('.translate-button, ytd-tri-state-button-view-model');
                    const clickTarget = translateArea?.querySelector('[role="button"], tp-yt-paper-button') || translateArea;

                    if (clickTarget) {
                        clickTarget.click();
                        App_UI.showToast('🌐 正在翻译...');

                        // 翻译是异步的,需要循环检查文字变化来更新大屏预览
                        let retry = 0;
                        const originalText = App_Core.getCommentData(card).content;
                        const checkTimer = setInterval(() => {
                            const newData = App_Core.getCommentData(card);
                            // 如果文字变了,或者是尝试了5次还没变(可能已经翻译完了或翻译失败)
                            if (newData.content !== originalText || retry > 5) {
                                if (App_UI.isVisible()) {
                                    App_UI.updateData(newData, App_Nav.getIndex(), App_Nav.getTotal());
                                }
                                clearInterval(checkTimer);
                            }
                            retry++;
                        }, 500);
                    } else {
                        App_UI.showToast('ℹ️ 该评论无法翻译');
                    }
                }
            } else if (k === 'escape' && App_UI.isVisible()) {
                App_UI.hide();
            }
        }, true);

        App_UI.injectCSS();
        // 监控路径变化
        let lp = window.location.href; setInterval(() => { if (window.location.href !== lp) { lp = window.location.href; App_Nav.reset(); App_UI.hide(); } }, 800);
    };

    setTimeout(init, 2000);
})();