YouTube Comment Enhancer

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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